690604:1008 ADR-034-134 #04
CI / CD Pipeline / build (push) Successful in 4m58s
CI / CD Pipeline / deploy (push) Successful in 7m33s

This commit is contained in:
2026-06-04 10:08:22 +07:00
parent fb224a116c
commit b79895e6fb
7 changed files with 160 additions and 36 deletions
+31 -3
View File
@@ -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,