4 Commits

Author SHA1 Message Date
6bd672da61 v1.14.2
Some checks failed
Docker (tags) / security (push) Successful in 28s
Docker (tags) / test (push) Failing after 40s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-19 21:28:26 +00:00
44d6dc3336 fix(readme): update README to document Nanonets-OCR2-3B (replaces Nanonets-OCR-s), adjust VRAM and context defaults, expand feature docs, and update examples/test command 2026-01-19 21:28:26 +00:00
d1ff95bd94 v1.14.1
Some checks failed
Docker (tags) / security (push) Successful in 26s
Docker (tags) / test (push) Failing after 39s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-19 21:19:37 +00:00
09770d3177 fix(extraction): improve JSON extraction prompts and model options for invoice and bank statement tests 2026-01-19 21:19:37 +00:00
6 changed files with 102 additions and 63 deletions

View File

@@ -1,5 +1,24 @@
# Changelog
## 2026-01-19 - 1.14.2 - fix(readme)
update README to document Nanonets-OCR2-3B (replaces Nanonets-OCR-s), adjust VRAM and context defaults, expand feature docs, and update examples/test command
- Renamed Nanonets-OCR-s -> Nanonets-OCR2-3B throughout README and examples
- Updated Nanonets VRAM guidance from ~10GB to ~12-16GB and documented 30K context
- Changed documented MAX_MODEL_LEN default from 8192 to 30000
- Updated example model identifiers (model strings and curl/example snippets) to nanonets/Nanonets-OCR2-3B
- Added MiniCPM and Qwen feature bullets (multilingual, multi-image, flowchart support, expanded context notes)
- Replaced README test command from ./test-images.sh to pnpm test
## 2026-01-19 - 1.14.1 - fix(extraction)
improve JSON extraction prompts and model options for invoice and bank statement tests
- Refactor JSON extraction prompts to be sent after the document text and add explicit 'WHERE TO FIND DATA' and 'RULES' sections for clearer extraction guidance
- Change chat message flow to: send document, assistant acknowledgement, then the JSON extraction prompt (avoids concatenating large prompts into one message)
- Add model options (num_ctx: 32768, temperature: 0) to give larger context windows and deterministic JSON output
- Simplify logging to avoid printing full prompt contents; log document and prompt lengths instead
- Increase timeouts for large documents to 600000ms (10 minutes) where applicable
## 2026-01-19 - 1.14.0 - feat(docker-images)
add vLLM-based Nanonets-OCR2-3B image, Qwen3-VL Ollama image and refactor build/docs/tests to use new runtime/layout

View File

@@ -1,6 +1,6 @@
{
"name": "@host.today/ht-docker-ai",
"version": "1.14.0",
"version": "1.14.2",
"type": "module",
"private": false,
"description": "Docker images for AI vision-language models including MiniCPM-V 4.5",

View File

@@ -2,7 +2,7 @@
Production-ready Docker images for state-of-the-art AI Vision-Language Models. Run powerful multimodal AI locally with GPU acceleration—**no cloud API keys required**.
> 🔥 **Three VLMs, one registry.** From lightweight document OCR to GPT-4o-level vision understanding—pick the right tool for your task.
> 🔥 **Three VLMs, one registry.** From high-performance document OCR to GPT-4o-level vision understanding—pick the right tool for your task.
## Issue Reporting and Security
@@ -15,7 +15,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
| Model | Parameters | Best For | API | Port | VRAM |
|-------|-----------|----------|-----|------|------|
| **MiniCPM-V 4.5** | 8B | General vision understanding, multi-image analysis | Ollama-compatible | 11434 | ~9GB |
| **Nanonets-OCR-s** | ~4B | Document OCR with semantic markdown output | OpenAI-compatible | 8000 | ~10GB |
| **Nanonets-OCR2-3B** | ~3B | Document OCR with semantic markdown, LaTeX, flowcharts | OpenAI-compatible | 8000 | ~12-16GB |
| **Qwen3-VL-30B** | 30B (A3B) | Advanced visual agents, code generation from images | Ollama-compatible | 11434 | ~20GB |
---
@@ -29,7 +29,7 @@ code.foss.global/host.today/ht-docker-ai:<tag>
| Tag | Model | Runtime | Port | VRAM |
|-----|-------|---------|------|------|
| `minicpm45v` / `latest` | MiniCPM-V 4.5 | Ollama | 11434 | ~9GB |
| `nanonets-ocr` | Nanonets-OCR-s | vLLM | 8000 | ~10GB |
| `nanonets-ocr` | Nanonets-OCR2-3B | vLLM | 8000 | ~12-16GB |
| `qwen3vl` | Qwen3-VL-30B-A3B | Ollama | 11434 | ~20GB |
---
@@ -38,6 +38,13 @@ code.foss.global/host.today/ht-docker-ai:<tag>
A GPT-4o level multimodal LLM from OpenBMB—handles image understanding, OCR, multi-image analysis, and visual reasoning across **30+ languages**.
### ✨ Key Features
- 🌍 **Multilingual:** 30+ languages supported
- 🖼️ **Multi-image:** Analyze multiple images in one request
- 📊 **Versatile:** Charts, documents, photos, diagrams
-**Efficient:** Runs on consumer GPUs (9GB VRAM)
### Quick Start
```bash
@@ -83,21 +90,22 @@ curl http://localhost:11434/api/chat -d '{
| Mode | VRAM Required |
|------|---------------|
| int4 quantized | 9GB |
| Full precision (bf16) | 18GB |
| int4 quantized | ~9GB |
| Full precision (bf16) | ~18GB |
---
## 🔍 Nanonets-OCR-s
## 🔍 Nanonets-OCR2-3B
A **Qwen2.5-VL-3B** model fine-tuned specifically for document OCR. Outputs structured markdown with semantic HTML tags—perfect for preserving document structure.
The **latest Nanonets document OCR model** (October 2025 release)—based on Qwen2.5-VL-3B, fine-tuned specifically for document extraction with significant improvements over the original OCR-s.
### Key Features
### Key Features
- 📝 **Semantic output:** Tables → HTML, equations → LaTeX, watermarks/page numbers → tagged
- 📝 **Semantic output:** Tables → HTML, equations → LaTeX, flowcharts → structured markup
- 🌍 **Multilingual:** Inherits Qwen's broad language support
- **Efficient:** ~10GB VRAM, runs great on consumer GPUs
- 📄 **30K context:** Handle large, multi-page documents
- 🔌 **OpenAI-compatible:** Drop-in replacement for existing pipelines
- 🎯 **Improved accuracy:** Better semantic tagging and LaTeX equation extraction vs. OCR-s
### Quick Start
@@ -116,7 +124,7 @@ docker run -d \
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "nanonets/Nanonets-OCR-s",
"model": "nanonets/Nanonets-OCR2-3B",
"messages": [{
"role": "user",
"content": [
@@ -131,7 +139,7 @@ curl http://localhost:8000/v1/chat/completions \
### Output Format
Nanonets-OCR-s returns markdown with semantic tags:
Nanonets-OCR2-3B returns markdown with semantic tags:
| Element | Output Format |
|---------|---------------|
@@ -140,13 +148,14 @@ Nanonets-OCR-s returns markdown with semantic tags:
| Images | `<img>description</img>` |
| Watermarks | `<watermark>OFFICIAL COPY</watermark>` |
| Page numbers | `<page_number>14</page_number>` |
| Flowcharts | Structured markup |
### Performance
### Hardware Requirements
| Metric | Value |
|--------|-------|
| Speed | 38 seconds per page |
| VRAM | ~10GB |
| Config | VRAM |
|--------|------|
| 30K context (default) | ~12-16GB |
| Speed | ~3-8 seconds per page |
---
@@ -154,7 +163,7 @@ Nanonets-OCR-s returns markdown with semantic tags:
The **most powerful** Qwen vision model—30B parameters with 3B active (MoE architecture). Handles complex visual reasoning, code generation from screenshots, and visual agent capabilities.
### Key Features
### Key Features
- 🚀 **256K context** (expandable to 1M tokens!)
- 🤖 **Visual agent capabilities** — can plan and execute multi-step tasks
@@ -204,7 +213,6 @@ curl http://localhost:11434/api/chat -d '{
Run multiple VLMs together for maximum flexibility:
```yaml
version: '3.8'
services:
# General vision tasks
minicpm:
@@ -259,10 +267,10 @@ volumes:
| Variable | Default | Description |
|----------|---------|-------------|
| `MODEL_NAME` | `nanonets/Nanonets-OCR-s` | HuggingFace model ID |
| `MODEL_NAME` | `nanonets/Nanonets-OCR2-3B` | HuggingFace model ID |
| `HOST` | `0.0.0.0` | API bind address |
| `PORT` | `8000` | API port |
| `MAX_MODEL_LEN` | `8192` | Maximum sequence length |
| `MAX_MODEL_LEN` | `30000` | Maximum sequence length |
| `GPU_MEMORY_UTILIZATION` | `0.9` | GPU memory usage (0-1) |
---
@@ -283,7 +291,7 @@ This dual-VLM approach catches extraction errors that single models miss.
### Why Multi-Model Works
- **Different architectures:** Independent models cross-validate each other
- **Specialized strengths:** Nanonets-OCR-s excels at document structure; MiniCPM-V handles general vision
- **Specialized strengths:** Nanonets-OCR2-3B excels at document structure; MiniCPM-V handles general vision
- **Native processing:** All VLMs see original images—no intermediate structure loss
### Model Selection Guide
@@ -291,10 +299,11 @@ This dual-VLM approach catches extraction errors that single models miss.
| Task | Recommended Model |
|------|-------------------|
| General image understanding | MiniCPM-V 4.5 |
| Document OCR with structure preservation | Nanonets-OCR-s |
| Document OCR with structure preservation | Nanonets-OCR2-3B |
| Complex visual reasoning / code generation | Qwen3-VL-30B |
| Multi-image analysis | MiniCPM-V 4.5 |
| Visual agent tasks | Qwen3-VL-30B |
| Large documents (30K+ tokens) | Nanonets-OCR2-3B |
---
@@ -309,7 +318,7 @@ cd ht-docker-ai
./build-images.sh
# Run tests
./test-images.sh
pnpm test
```
---

View File

@@ -51,11 +51,21 @@ If there is an image in the document and image caption is not present, add a sma
Watermarks should be wrapped in brackets. Ex: <watermark>OFFICIAL COPY</watermark>.
Page numbers should be wrapped in brackets. Ex: <page_number>14</page_number>.`;
// JSON extraction prompt for GPT-OSS 20B
const JSON_EXTRACTION_PROMPT = `Extract ALL transactions from this bank statement as JSON array. Each transaction: {"date": "YYYY-MM-DD", "counterparty": "NAME", "amount": -25.99}. Amount negative for debits, positive for credits. Only include actual transactions, not balances. Return ONLY JSON array, no explanation.
// JSON extraction prompt for GPT-OSS 20B (sent AFTER the statement text is provided)
const JSON_EXTRACTION_PROMPT = `Extract ALL transactions from the bank statement. Return ONLY valid JSON array.
STATEMENT:
`;
WHERE TO FIND DATA:
- Transactions are typically in TABLES with columns: Date, Description/Counterparty, Debit, Credit, Balance
- Look for rows with actual money movements, NOT header rows or summary totals
RULES:
1. date: Convert to YYYY-MM-DD format
2. counterparty: The name/description of who the money went to/from
3. amount: NEGATIVE for debits/withdrawals, POSITIVE for credits/deposits
4. Only include actual transactions, NOT opening/closing balances
JSON array only:
[{"date":"YYYY-MM-DD","counterparty":"NAME","amount":-25.99}]`;
// Constants for smart batching
const MAX_VISUAL_TOKENS = 28000; // ~32K context minus prompt/output headroom
@@ -246,12 +256,8 @@ async function ensureExtractionModel(): Promise<boolean> {
*/
async function extractTransactionsFromMarkdown(markdown: string, queryId: string): Promise<ITransaction[]> {
const startTime = Date.now();
const fullPrompt = JSON_EXTRACTION_PROMPT + markdown;
// Log exact prompt
console.log(`\n [${queryId}] ===== PROMPT =====`);
console.log(fullPrompt);
console.log(` [${queryId}] ===== END PROMPT (${fullPrompt.length} chars) =====\n`);
console.log(` [${queryId}] Statement: ${markdown.length} chars, Prompt: ${JSON_EXTRACTION_PROMPT.length} chars`);
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
@@ -261,9 +267,15 @@ async function extractTransactionsFromMarkdown(markdown: string, queryId: string
messages: [
{ role: 'user', content: 'Hi there, how are you?' },
{ role: 'assistant', content: 'Good, how can I help you today?' },
{ role: 'user', content: fullPrompt },
{ role: 'user', content: `Here is a bank statement document:\n\n${markdown}` },
{ role: 'assistant', content: 'I have read the bank statement document you provided. I can see all the transaction data. What would you like me to do with it?' },
{ role: 'user', content: JSON_EXTRACTION_PROMPT },
],
stream: true,
options: {
num_ctx: 32768, // Larger context for long statements + thinking
temperature: 0, // Deterministic for JSON extraction
},
}),
signal: AbortSignal.timeout(600000), // 10 minute timeout
});

View File

@@ -197,6 +197,10 @@ async function extractInvoiceFromMarkdown(markdown: string, queryId: string): Pr
{ role: 'user', content: JSON_EXTRACTION_PROMPT },
],
stream: true,
options: {
num_ctx: 32768, // Larger context for long invoices + thinking
temperature: 0, // Deterministic for JSON extraction
},
}),
signal: AbortSignal.timeout(120000), // 2 min timeout
});

View File

@@ -54,31 +54,24 @@ If there is an image in the document and image caption is not present, add a sma
Watermarks should be wrapped in brackets. Ex: <watermark>OFFICIAL COPY</watermark>.
Page numbers should be wrapped in brackets. Ex: <page_number>14</page_number>.`;
// JSON extraction prompt for GPT-OSS 20B
const JSON_EXTRACTION_PROMPT = `You are an invoice data extractor. Below is an invoice document converted to text/markdown. Extract the key invoice fields as JSON.
// JSON extraction prompt for GPT-OSS 20B (sent AFTER the invoice text is provided)
const JSON_EXTRACTION_PROMPT = `Extract key fields from the invoice. Return ONLY valid JSON.
IMPORTANT RULES:
1. invoice_number: The unique invoice/document number (NOT VAT ID, NOT customer ID)
2. invoice_date: Format as YYYY-MM-DD
3. vendor_name: The company that issued the invoice
WHERE TO FIND DATA:
- invoice_number, invoice_date, vendor_name: Look in the HEADER section at the TOP of PAGE 1 (near "Invoice no.", "Invoice date:", "Rechnungsnummer")
- net_amount, vat_amount, total_amount: Look in the SUMMARY section at the BOTTOM (look for "Total", "Amount due", "Gesamtbetrag")
RULES:
1. invoice_number: Extract ONLY the value (e.g., "R0015632540"), NOT the label "Invoice no."
2. invoice_date: Convert to YYYY-MM-DD format (e.g., "14/04/2022" → "2022-04-14")
3. vendor_name: The company issuing the invoice
4. currency: EUR, USD, or GBP
5. net_amount: Amount before tax
6. vat_amount: Tax/VAT amount
7. total_amount: Final total (gross amount)
5. net_amount: Total before tax
6. vat_amount: Tax amount
7. total_amount: Final total with tax
Return ONLY this JSON format, no explanation:
{
"invoice_number": "INV-2024-001",
"invoice_date": "2024-01-15",
"vendor_name": "Company Name",
"currency": "EUR",
"net_amount": 100.00,
"vat_amount": 19.00,
"total_amount": 119.00
}
INVOICE TEXT:
`;
JSON only:
{"invoice_number":"X","invoice_date":"YYYY-MM-DD","vendor_name":"X","currency":"EUR","net_amount":0,"vat_amount":0,"total_amount":0}`;
// Constants for smart batching
const MAX_VISUAL_TOKENS = 28000; // ~32K context minus prompt/output headroom
@@ -370,12 +363,8 @@ function parseJsonToInvoice(response: string): IInvoice | null {
*/
async function extractInvoiceFromMarkdown(markdown: string, queryId: string): Promise<IInvoice | null> {
const startTime = Date.now();
const fullPrompt = JSON_EXTRACTION_PROMPT + markdown;
// Log exact prompt
console.log(`\n [${queryId}] ===== PROMPT =====`);
console.log(fullPrompt);
console.log(` [${queryId}] ===== END PROMPT (${fullPrompt.length} chars) =====\n`);
console.log(` [${queryId}] Invoice: ${markdown.length} chars, Prompt: ${JSON_EXTRACTION_PROMPT.length} chars`);
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
@@ -385,9 +374,15 @@ async function extractInvoiceFromMarkdown(markdown: string, queryId: string): Pr
messages: [
{ role: 'user', content: 'Hi there, how are you?' },
{ role: 'assistant', content: 'Good, how can I help you today?' },
{ role: 'user', content: fullPrompt },
{ role: 'user', content: `Here is an invoice document:\n\n${markdown}` },
{ role: 'assistant', content: 'I have read the invoice document you provided. I can see all the text content. What would you like me to do with it?' },
{ role: 'user', content: JSON_EXTRACTION_PROMPT },
],
stream: true,
options: {
num_ctx: 32768, // Larger context for long invoices + thinking
temperature: 0, // Deterministic for JSON extraction
},
}),
signal: AbortSignal.timeout(600000), // 10 minute timeout for large documents
});