690604:1008 ADR-034-134 #04
This commit is contained in:
@@ -545,9 +545,24 @@ export class AiController {
|
||||
},
|
||||
engineType: {
|
||||
type: 'string',
|
||||
enum: ['auto', 'tesseract', 'typhoon-ocr-3b', 'typhoon-ocr1.5-3b'],
|
||||
enum: ['auto', 'tesseract', 'typhoon-np-dms-ocr'],
|
||||
description: 'OCR engine ที่ต้องการใช้ (default: auto)',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Typhoon OCR temperature (0.0-1.0) — override Modelfile default (0.1)',
|
||||
},
|
||||
topP: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Typhoon OCR top_p (0.0-1.0) — override Modelfile default (0.1)',
|
||||
},
|
||||
repeatPenalty: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Typhoon OCR repeat_penalty — override Modelfile default (1.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -562,6 +577,9 @@ export class AiController {
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
@Body('engineType') engineType: string | undefined,
|
||||
@Body('temperature') temperature: string | undefined,
|
||||
@Body('topP') topP: string | undefined,
|
||||
@Body('repeatPenalty') repeatPenalty: string | undefined,
|
||||
@CurrentUser() user: User
|
||||
): Promise<{ requestPublicId: string; jobId: string; status: string }> {
|
||||
const attachment = await this.fileStorageService.upload(file, user.user_id);
|
||||
@@ -570,20 +588,30 @@ export class AiController {
|
||||
const validEngineTypes = [
|
||||
'auto',
|
||||
'tesseract',
|
||||
'typhoon-ocr-3b',
|
||||
'typhoon-ocr1.5-3b',
|
||||
'typhoon-np-dms-ocr',
|
||||
] as const;
|
||||
const resolvedEngineType: SandboxOcrEngineType = validEngineTypes.includes(
|
||||
engineType as SandboxOcrEngineType
|
||||
)
|
||||
? (engineType as SandboxOcrEngineType)
|
||||
: 'auto';
|
||||
// แปลง string จาก multipart form เป็น number (optional override)
|
||||
const typhoonOptions = {
|
||||
...(temperature !== undefined && {
|
||||
temperature: parseFloat(temperature),
|
||||
}),
|
||||
...(topP !== undefined && { topP: parseFloat(topP) }),
|
||||
...(repeatPenalty !== undefined && {
|
||||
repeatPenalty: parseFloat(repeatPenalty),
|
||||
}),
|
||||
};
|
||||
const jobId = await this.aiQueueService.enqueueSandboxJob(
|
||||
'sandbox-ocr-only',
|
||||
{
|
||||
idempotencyKey: requestPublicId,
|
||||
pdfPath: attachment.filePath,
|
||||
engineType: resolvedEngineType,
|
||||
...(Object.keys(typhoonOptions).length > 0 && { typhoonOptions }),
|
||||
}
|
||||
);
|
||||
return { requestPublicId, jobId, status: 'queued' };
|
||||
|
||||
@@ -17,6 +17,7 @@ import { VramMonitorService } from '../services/vram-monitor.service';
|
||||
import {
|
||||
SandboxOcrEngineService,
|
||||
SandboxOcrEngineType,
|
||||
OcrTyphoonOptions,
|
||||
} from '../services/sandbox-ocr-engine.service';
|
||||
|
||||
/** ชื่อ queue สำหรับ Typhoon OCR jobs */
|
||||
@@ -26,12 +27,14 @@ export const QUEUE_TYPHOON_OCR = 'typhoon-ocr';
|
||||
export interface TyphoonOcrJobData {
|
||||
/** public path ของไฟล์ PDF ที่ต้องการ OCR */
|
||||
pdfPath: string;
|
||||
/** engineType: เสมอเป็น 'typhoon-ocr-3b' สำหรับ queue นี้ */
|
||||
/** engineType: 'typhoon-np-dms-ocr' สำหรับ queue นี้ */
|
||||
engineType: SandboxOcrEngineType;
|
||||
/** idempotencyKey สำหรับ Redis result key */
|
||||
idempotencyKey: string;
|
||||
/** documentPublicId สำหรับ audit log (optional) */
|
||||
documentPublicId?: string;
|
||||
/** Typhoon OCR options จาก sandbox UI เพื่อ override Modelfile defaults (optional) */
|
||||
typhoonOptions?: OcrTyphoonOptions;
|
||||
}
|
||||
|
||||
// VRAM ที่ Typhoon OCR-3B ต้องการ (MB) — ตาม ADR-032
|
||||
@@ -59,7 +62,13 @@ export class TyphoonOcrProcessor extends WorkerHost {
|
||||
|
||||
/** ประมวลผล Typhoon OCR job ทีละงาน */
|
||||
async process(job: Job<TyphoonOcrJobData>): Promise<void> {
|
||||
const { pdfPath, engineType, idempotencyKey, documentPublicId } = job.data;
|
||||
const {
|
||||
pdfPath,
|
||||
engineType,
|
||||
idempotencyKey,
|
||||
documentPublicId,
|
||||
typhoonOptions,
|
||||
} = job.data;
|
||||
const startTime = Date.now();
|
||||
this.logger.log(
|
||||
`Typhoon OCR job started — idempotencyKey=${idempotencyKey}, engine=${engineType}`
|
||||
@@ -106,7 +115,8 @@ export class TyphoonOcrProcessor extends WorkerHost {
|
||||
try {
|
||||
const result = await this.sandboxOcrEngineService.detectAndExtract(
|
||||
pdfPath,
|
||||
engineType
|
||||
engineType,
|
||||
typhoonOptions
|
||||
);
|
||||
const processingTimeMs = Date.now() - startTime;
|
||||
// บันทึกผลลัพธ์ใน Redis cache (24h TTL)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// - 2026-06-01: ปรับปรุง remapPath ให้รองรับ Windows absolute และ relative path ได้แม่นยำ 100%
|
||||
// - 2026-06-01: เปลี่ยน processWithTesseract/processWithTyphoon ให้ส่ง file content ผ่าน multipart ไปยัง /ocr-upload แทนการส่ง path
|
||||
// - 2026-06-02: ส่งค่า X-API-Key ใน request headers ไปยัง ocr-sidecar เพื่อความมั่นคงปลอดภัยสูงสุด (ADR-033, Suggestion 2)
|
||||
// - 2026-06-04: ADR-034 — เปลี่ยน TYPHOON_ENGINE.engineName เป็น typhoon-np-dms-ocr:latest ตรงกับชื่อโมเดลใน Ollama
|
||||
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -77,7 +78,7 @@ const TESSERACT_ENGINE: OcrEngineConfiguration = {
|
||||
|
||||
const TYPHOON_ENGINE: OcrEngineConfiguration = {
|
||||
engineId: TYPHOON_ENGINE_ID,
|
||||
engineName: 'Typhoon OCR-3B',
|
||||
engineName: 'typhoon-np-dms-ocr:latest',
|
||||
engineType: OcrEngineType.TYPHOON_OCR,
|
||||
isActive: true,
|
||||
vramRequirementMB: TYPHOON_OCR_REQUIRED_VRAM_MB,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// - 2026-05-30: แยก SandboxOcrEngineService ออกจาก OcrService เพื่อรองรับการเลือก Typhoon OCR เฉพาะ sandbox โดยไม่กระทบ core OCR flow
|
||||
// - 2026-06-01: เปลี่ยนจาก remapPath + pdfPath ไปเป็น multipart file upload ไปยัง /ocr-upload (แก้ปัญหา Docker WSL2 mount)
|
||||
// - 2026-06-02: ส่งค่า X-API-Key ใน request headers ไปยัง ocr-sidecar เพื่อความมั่นคงปลอดภัยสูงสุด (ADR-033, Suggestion 2)
|
||||
// - 2026-06-04: ADR-034 — เพิ่ม 'typhoon-np-dms-ocr' เป็น canonical SandboxOcrEngineType; legacy aliases ยังรองรับ
|
||||
// - 2026-06-04: เพิ่ม OcrTyphoonOptions interface; รับ temperature/topP/repeatPenalty จาก frontend sandbox เพื่อ override Modelfile defaults
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -10,11 +12,14 @@ import axios from 'axios';
|
||||
import * as fs from 'fs';
|
||||
import { OcrService } from './ocr.service';
|
||||
|
||||
export type SandboxOcrEngineType =
|
||||
| 'auto'
|
||||
| 'tesseract'
|
||||
| 'typhoon-ocr-3b'
|
||||
| 'typhoon-ocr1.5-3b';
|
||||
export type SandboxOcrEngineType = 'auto' | 'tesseract' | 'typhoon-np-dms-ocr';
|
||||
|
||||
/** ค่า parameter สำหรับ Typhoon OCR ที่ override Modelfile defaults ได้จาก sandbox UI */
|
||||
export interface OcrTyphoonOptions {
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
repeatPenalty?: number;
|
||||
}
|
||||
|
||||
interface SandboxOcrSidecarResponse {
|
||||
text?: string;
|
||||
@@ -52,7 +57,8 @@ export class SandboxOcrEngineService {
|
||||
/** รัน OCR ตาม engine ที่เลือก โดย fallback กลับไป Tesseract baseline เมื่อ Typhoon ล้มเหลว */
|
||||
async detectAndExtract(
|
||||
pdfPath: string,
|
||||
engineType: SandboxOcrEngineType = 'auto'
|
||||
engineType: SandboxOcrEngineType = 'auto',
|
||||
typhoonOptions?: OcrTyphoonOptions
|
||||
): Promise<SandboxOcrResult> {
|
||||
if (engineType === 'auto' || engineType === 'tesseract') {
|
||||
const result = await this.ocrService.detectAndExtract({ pdfPath });
|
||||
@@ -73,6 +79,15 @@ export class SandboxOcrEngineService {
|
||||
'upload.pdf'
|
||||
);
|
||||
form.append('engine', engineType);
|
||||
if (typhoonOptions?.temperature !== undefined) {
|
||||
form.append('temperature', String(typhoonOptions.temperature));
|
||||
}
|
||||
if (typhoonOptions?.topP !== undefined) {
|
||||
form.append('topP', String(typhoonOptions.topP));
|
||||
}
|
||||
if (typhoonOptions?.repeatPenalty !== undefined) {
|
||||
form.append('repeatPenalty', String(typhoonOptions.repeatPenalty));
|
||||
}
|
||||
const response = await axios.post<SandboxOcrSidecarResponse>(
|
||||
`${this.ocrApiUrl}/ocr-upload`,
|
||||
form,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// - 2026-05-30: Refactor เป็น 2-step flow (Step 1: OCR-only → Step 2: AI Extraction) ตาม spec 231
|
||||
// - 2026-06-02: ปรับปรุงลำดับปุ่มแท็บเริ่มต้นให้เริ่มที่ OCR Sandbox และเปลี่ยน dropdown labels ของตัวเลือกโมเดล Typhoon OCR ให้แสดงหน่วยความจำ VRAM แม่นยำ (T012, T013, ADR-033)
|
||||
// - 2026-06-04: เปลี่ยน OCR Engine dropdown จาก hardcoded เป็น dynamic โดยดึงจาก getOcrEngines() API และ map engineType → SandboxOcrEngineType
|
||||
// - 2026-06-04: เพิ่ม UI sliders (temperature/topP/repeatPenalty) สำหรับ typhoon-np-dms-ocr engine; ส่งเป็น optional override ไปยัง sidecar
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
@@ -111,6 +112,9 @@ export default function OcrSandboxPromptManager() {
|
||||
// 2-step flow states
|
||||
const [sandboxStep, setSandboxStep] = useState<'ocr' | 'ai'>('ocr');
|
||||
const [selectedOcrEngine, setSelectedOcrEngine] = useState<string>('auto');
|
||||
const [typhoonTemperature, setTyphoonTemperature] = useState<number>(0.1);
|
||||
const [typhoonTopP, setTyphoonTopP] = useState<number>(0.1);
|
||||
const [typhoonRepeatPenalty, setTyphoonRepeatPenalty] = useState<number>(1.1);
|
||||
const { data: ocrEnginesData } = useQuery<OcrEngineResponse[]>({
|
||||
queryKey: ['ocr-engines'],
|
||||
queryFn: () => adminAiService.getOcrEngines(),
|
||||
@@ -225,9 +229,13 @@ export default function OcrSandboxPromptManager() {
|
||||
try {
|
||||
resetSandbox();
|
||||
setSandboxStep('ocr');
|
||||
const typhoonOptions = selectedOcrEngine === 'typhoon-np-dms-ocr'
|
||||
? { temperature: typhoonTemperature, topP: typhoonTopP, repeatPenalty: typhoonRepeatPenalty }
|
||||
: undefined;
|
||||
const { requestPublicId } = await adminAiService.submitSandboxOcr(
|
||||
ocrFile,
|
||||
selectedOcrEngine
|
||||
selectedOcrEngine,
|
||||
typhoonOptions
|
||||
);
|
||||
toast.success(t('ai.prompt.uploadSuccess'));
|
||||
// Poll สำหรับผลลัพธ์ OCR
|
||||
@@ -306,6 +314,9 @@ export default function OcrSandboxPromptManager() {
|
||||
setOcrResult(null);
|
||||
setSelectedPromptVersion(undefined);
|
||||
setSelectedOcrEngine('auto');
|
||||
setTyphoonTemperature(0.1);
|
||||
setTyphoonTopP(0.1);
|
||||
setTyphoonRepeatPenalty(1.1);
|
||||
setOcrFile(null);
|
||||
resetSandbox();
|
||||
};
|
||||
@@ -419,6 +430,44 @@ export default function OcrSandboxPromptManager() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{selectedOcrEngine === 'typhoon-np-dms-ocr' && (
|
||||
<div className="space-y-3 rounded-md border border-dashed border-amber-500/30 bg-amber-500/5 p-3">
|
||||
<p className="text-xs font-medium text-amber-600 dark:text-amber-400">Typhoon OCR Options <span className="font-normal text-muted-foreground">(override Modelfile defaults)</span></p>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<label>Temperature</label>
|
||||
<span className="font-mono text-muted-foreground">{typhoonTemperature.toFixed(2)}</span>
|
||||
</div>
|
||||
<input type="range" min={0} max={1} step={0.01}
|
||||
value={typhoonTemperature}
|
||||
onChange={(e) => setTyphoonTemperature(parseFloat(e.target.value))}
|
||||
className="w-full h-1.5 accent-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<label>Top-P</label>
|
||||
<span className="font-mono text-muted-foreground">{typhoonTopP.toFixed(2)}</span>
|
||||
</div>
|
||||
<input type="range" min={0} max={1} step={0.01}
|
||||
value={typhoonTopP}
|
||||
onChange={(e) => setTyphoonTopP(parseFloat(e.target.value))}
|
||||
className="w-full h-1.5 accent-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<label>Repeat Penalty</label>
|
||||
<span className="font-mono text-muted-foreground">{typhoonRepeatPenalty.toFixed(2)}</span>
|
||||
</div>
|
||||
<input type="range" min={1} max={2} step={0.01}
|
||||
value={typhoonRepeatPenalty}
|
||||
onChange={(e) => setTyphoonRepeatPenalty(parseFloat(e.target.value))}
|
||||
className="w-full h-1.5 accent-amber-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center rounded-lg border border-dashed p-8 transition-all',
|
||||
|
||||
@@ -228,11 +228,21 @@ export const adminAiService = {
|
||||
|
||||
submitSandboxOcr: async (
|
||||
file: File,
|
||||
engineType: string = 'auto'
|
||||
engineType: string = 'auto',
|
||||
typhoonOptions?: { temperature?: number; topP?: number; repeatPenalty?: number }
|
||||
): Promise<{ requestPublicId: string; jobId: string; status: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('engineType', engineType);
|
||||
if (typhoonOptions?.temperature !== undefined) {
|
||||
formData.append('temperature', String(typhoonOptions.temperature));
|
||||
}
|
||||
if (typhoonOptions?.topP !== undefined) {
|
||||
formData.append('topP', String(typhoonOptions.topP));
|
||||
}
|
||||
if (typhoonOptions?.repeatPenalty !== undefined) {
|
||||
formData.append('repeatPenalty', String(typhoonOptions.repeatPenalty));
|
||||
}
|
||||
const { data } = await api.post('/ai/admin/sandbox/ocr', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
# - 2026-06-01: เพิ่ม POST /ocr-upload รับ multipart file โดยตรง ไม่ต้องพึ่ง shared volume mount
|
||||
# - 2026-06-01: เปลี่ยน TYPHOON_OCR_MODEL default เป็น scb10x/typhoon-ocr1.5-3b
|
||||
# - 2026-06-02: เพิ่มตัวเลือกสลับโมเดลใน process_with_typhoon_ocr ตามพารามิเตอร์ engine และตั้ง engineUsed ให้ตรงตามจริง (T015, ADR-033)
|
||||
# - 2026-06-04: ADR-034 — เพิ่ม typhoon-np-dms-ocr เป็น canonical engine key; default TYPHOON_OCR_MODEL เปวน typhoon-np-dms-ocr:latest; alias โมเดลเก่ายังคงไว้
|
||||
# - 2026-06-04: ให้ SYSTEM ใน Modelfile ทำงานแทน — ลบ prompt ซ้าซ้อน; sync options ให้ตรงกับ Modelfile (temperature 0.1, top_p 0.1, repeat_penalty 1.1)
|
||||
# - 2026-06-04: รับค่า temperature/top_p/repeat_penalty จาก frontend sandbox ได้ (optional override)
|
||||
# - 2026-06-02: เพิ่มการตรวจสอบ API Key (X-API-Key Header) สำหรับ endpoints หลัก เพื่อความมั่นคงปลอดภัยตามข้อเสนอแนะ Code Review
|
||||
|
||||
import os
|
||||
@@ -51,7 +54,7 @@ OCR_CHAR_THRESHOLD = int(os.getenv("OCR_CHAR_THRESHOLD", "100"))
|
||||
MAX_PAGES = int(os.getenv("OCR_MAX_PAGES", "0")) # 0 = ทุกหน้า
|
||||
OCR_LANG = os.getenv("OCR_LANG", "tha+eng") # Tesseract language code (tha+eng = Thai + English)
|
||||
OLLAMA_API_URL = os.getenv("OLLAMA_API_URL", "http://host.docker.internal:11434")
|
||||
TYPHOON_OCR_MODEL = os.getenv("TYPHOON_OCR_MODEL", "scb10x/typhoon-ocr1.5-3b")
|
||||
TYPHOON_OCR_MODEL = os.getenv("TYPHOON_OCR_MODEL", "typhoon-np-dms-ocr:latest")
|
||||
TYPHOON_OCR_TIMEOUT = int(os.getenv("TYPHOON_OCR_TIMEOUT", "120"))
|
||||
# PSM 3 = Fully automatic page segmentation (เหมาะกับเอกสารที่มี layout หลายส่วน เช่น วันที่/เลขที่)
|
||||
# OEM 1 = LSTM only (ดีกว่า legacy engine)
|
||||
@@ -137,7 +140,7 @@ def health():
|
||||
return {"status": "ok", "engine": "tesseract"}
|
||||
|
||||
|
||||
def _process_pdf_doc(doc: fitz.Document, selected_engine: str, max_pages: int) -> OcrResponse:
|
||||
def _process_pdf_doc(doc: fitz.Document, selected_engine: str, max_pages: int, typhoon_options: dict = {}) -> OcrResponse:
|
||||
"""ประมวลผล fitz.Document ด้วย engine ที่เลือก — shared logic สำหรับ /ocr และ /ocr-upload"""
|
||||
pages_to_process = list(range(min(len(doc), max_pages) if max_pages > 0 else len(doc)))
|
||||
page_count = len(pages_to_process)
|
||||
@@ -160,7 +163,7 @@ def _process_pdf_doc(doc: fitz.Document, selected_engine: str, max_pages: int) -
|
||||
engineUsed="fast-path",
|
||||
)
|
||||
|
||||
if selected_engine in ("typhoon-ocr-3b", "typhoon-ocr1.5-3b"):
|
||||
if selected_engine == "typhoon-np-dms-ocr":
|
||||
typhoon_text_parts = []
|
||||
for i in pages_to_process:
|
||||
page = doc[i]
|
||||
@@ -169,7 +172,7 @@ def _process_pdf_doc(doc: fitz.Document, selected_engine: str, max_pages: int) -
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
cropped_img = crop_header_footer(img, CROP_TOP_RATIO, CROP_BOTTOM_RATIO)
|
||||
processed_img = preprocess_image(cropped_img)
|
||||
typhoon_text_parts.append(process_with_typhoon_ocr(processed_img, selected_engine))
|
||||
typhoon_text_parts.append(process_with_typhoon_ocr(processed_img, typhoon_options))
|
||||
typhoon_text = filter_ocr_noise("\n".join(typhoon_text_parts).strip())
|
||||
return OcrResponse(
|
||||
text=typhoon_text,
|
||||
@@ -202,28 +205,25 @@ def _process_pdf_doc(doc: fitz.Document, selected_engine: str, max_pages: int) -
|
||||
)
|
||||
|
||||
|
||||
def process_with_typhoon_ocr(pil_image: Image.Image, engine_type: str = "typhoon-ocr1.5-3b") -> str:
|
||||
"""เรียก Typhoon OCR ผ่าน Ollama สำหรับ sandbox option โดยเลือก model ตาม engine ที่ระบุ"""
|
||||
model_name = "scb10x/typhoon-ocr1.5-3b"
|
||||
if engine_type == "typhoon-ocr-3b":
|
||||
model_name = "scb10x/typhoon-ocr-3b"
|
||||
elif engine_type == "typhoon-ocr1.5-3b":
|
||||
model_name = "scb10x/typhoon-ocr1.5-3b"
|
||||
else:
|
||||
model_name = TYPHOON_OCR_MODEL
|
||||
def process_with_typhoon_ocr(pil_image: Image.Image, options_override: dict = {}) -> str:
|
||||
"""เรียก Typhoon OCR ผ่าน Ollama — ใช้ SYSTEM ใน Modelfile เป็น instruction หลัก; options_override ยัง override ค่า Modelfile ได้"""
|
||||
model_name = TYPHOON_OCR_MODEL
|
||||
img_buffer = io.BytesIO()
|
||||
pil_image.save(img_buffer, format="PNG")
|
||||
image_base64 = base64.b64encode(img_buffer.getvalue()).decode("utf-8")
|
||||
# ค่า default ตาม Modelfile; frontend override ได้บางส่วนหรือทั้งหมด
|
||||
options = {
|
||||
"temperature": 0.1,
|
||||
"top_p": 0.1,
|
||||
"repeat_penalty": 1.1,
|
||||
**options_override,
|
||||
}
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"prompt": "สกัดข้อความภาษาไทยและอังกฤษทั้งหมดจากภาพนี้อย่างถูกต้อง รักษาโครงสร้างบรรทัดและการเว้นวรรคให้ใกล้เคียงต้นฉบับมากที่สุด ห้ามเพิ่มคำอธิบายใดๆ",
|
||||
"prompt": "",
|
||||
"images": [image_base64],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0.0,
|
||||
"top_p": 0.9,
|
||||
"repeat_penalty": 1.0,
|
||||
},
|
||||
"options": options,
|
||||
"keep_alive": 0,
|
||||
}
|
||||
with httpx.Client(timeout=TYPHOON_OCR_TIMEOUT) as client:
|
||||
@@ -253,17 +253,28 @@ def ocr_upload(
|
||||
file: UploadFile = File(...),
|
||||
engine: str = Form(default="auto"),
|
||||
maxPages: int = Form(default=0),
|
||||
temperature: Optional[float] = Form(default=None),
|
||||
topP: Optional[float] = Form(default=None),
|
||||
repeatPenalty: Optional[float] = Form(default=None),
|
||||
):
|
||||
"""OCR จาก multipart file upload — ไม่ต้องการ shared volume mount"""
|
||||
selected_engine = engine.strip().lower()
|
||||
max_pages = maxPages or MAX_PAGES
|
||||
# รวม options override สำหรับ Typhoon OCR (ถ้า frontend ส่งมา)
|
||||
typhoon_options: dict = {}
|
||||
if temperature is not None:
|
||||
typhoon_options["temperature"] = temperature
|
||||
if topP is not None:
|
||||
typhoon_options["top_p"] = topP
|
||||
if repeatPenalty is not None:
|
||||
typhoon_options["repeat_penalty"] = repeatPenalty
|
||||
pdf_bytes = file.file.read()
|
||||
try:
|
||||
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=422, detail=f"เปิดไฟล์ PDF ล้มเหลว: {e}")
|
||||
logger.info(f"OCR upload: {file.filename} engine={selected_engine}")
|
||||
return _process_pdf_doc(doc, selected_engine, max_pages)
|
||||
logger.info(f"OCR upload: {file.filename} engine={selected_engine} options={typhoon_options or 'modelfile-defaults'}")
|
||||
return _process_pdf_doc(doc, selected_engine, max_pages, typhoon_options)
|
||||
|
||||
|
||||
class NormalizeRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user