feat: DeepSeek-OCR endpoint + GPU request queue for AI calls
Mizan ve DeepSeek-OCR aynı self-hosted GPU'yu paylaşıyor; backend'de hiçbir eşzamanlılık sınırı yoktu, yoğun anlarda istekler Ollama kuyruğunda 90sn'yi aşıp sessizce "başarısız" görünüyordu. aiQueue.ts, GPU'ya giden istek sayısını sınırlayıp fazlasını backend'de bekletiyor. Yeni POST /documents/ocr-image, tek bir görseli DeepSeek-OCR ile metne çeviriyor (Electron tarafındaki çok sayfalı TIFF/taranmış PDF işleme adımının kullanacağı uç, ayrı bir PR'da bağlanacak).
This commit is contained in:
+92
-17
@@ -1,4 +1,5 @@
|
||||
import { getAiConfig } from './aiConfig';
|
||||
import { withAiQueue } from './aiQueue';
|
||||
|
||||
export interface ChatMsg { role: string; content: string; }
|
||||
|
||||
@@ -35,24 +36,29 @@ export async function callOllama(
|
||||
messages: ChatMsg[],
|
||||
opts: { numPredict?: number; timeoutMs?: number } = {}
|
||||
): Promise<string> {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const ctrl = new AbortController();
|
||||
const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildBody(messages, aiModel, false, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||
// withAiQueue'nun İÇİNDE: zaman aşımı sayacı, istek GPU sırasında beklerken
|
||||
// değil, gerçekten gönderilmeye başladığında işlemeye başlasın — yoksa kuyrukta
|
||||
// uzun bekleyen bir istek, sırası gelir gelmez anında "zaman aşımı" olurdu.
|
||||
return withAiQueue(async () => {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const ctrl = new AbortController();
|
||||
const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildBody(messages, aiModel, false, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return (data.message?.content || '').toString().trim();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const data = await response.json();
|
||||
return (data.message?.content || '').toString().trim();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Akışlı (streaming) çağrı — kullanıcının yanıtı token token canlı gördüğü
|
||||
@@ -66,6 +72,14 @@ export async function streamOllama(
|
||||
messages: ChatMsg[],
|
||||
onToken: (delta: string) => void,
|
||||
opts: { numPredict?: number; stallTimeoutMs?: number } = {}
|
||||
): Promise<string> {
|
||||
return withAiQueue(() => streamOllamaInner(messages, onToken, opts));
|
||||
}
|
||||
|
||||
async function streamOllamaInner(
|
||||
messages: ChatMsg[],
|
||||
onToken: (delta: string) => void,
|
||||
opts: { numPredict?: number; stallTimeoutMs?: number } = {}
|
||||
): Promise<string> {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const stallMs = opts.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
||||
@@ -131,3 +145,64 @@ export async function streamOllama(
|
||||
clearTimeout(stallTimer!);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_OCR_NUM_PREDICT = 1200;
|
||||
const DEFAULT_OCR_TIMEOUT_MS = 60_000;
|
||||
|
||||
// DeepSeek-OCR ("deepseek-ocr-fixed") ile bir görseli metne çevirir. Mizan'dan
|
||||
// (sohbet/analiz) tamamen ayrı bir model — aynı Ollama sunucusunda, farklı
|
||||
// model adıyla çağrılıyor, withAiQueue ikisini de aynı GPU sırasında bekletiyor.
|
||||
//
|
||||
// NOT — "deepseek-ocr-fixed" modeli Ollama kütüphanesindeki resmi paketten
|
||||
// FARKLI: resmi paketin şablonu ("template": "{{ .Prompt }}") görsel için hiçbir
|
||||
// yer tutucu içermiyordu, bu yüzden model gönderilen görseli hiç görmüyordu.
|
||||
// Sunucuda elle şu şekilde düzeltilmiş bir kopya oluşturuldu:
|
||||
//
|
||||
// curl -X POST $RUNPOD_API_BASE_URL/api/create -d '{
|
||||
// "model": "deepseek-ocr-fixed",
|
||||
// "from": "deepseek-ocr",
|
||||
// "template": "{{ if .Images }}{{ range .Images }}[img-0]{{ end }}\n{{ end }}{{ .Prompt }}",
|
||||
// "parameters": { "temperature": 0 }
|
||||
// }'
|
||||
//
|
||||
// Ollama'nın model verisi silinir/sıfırlanırsa (örn. volume resetlenirse) bu
|
||||
// komut tekrar çalıştırılmalı — yoksa OCR istekleri sessizce boş metin döner.
|
||||
export async function callOllamaVisionOcr(
|
||||
imageBase64: string,
|
||||
opts: { numPredict?: number; timeoutMs?: number; prompt?: string } = {}
|
||||
): Promise<string> {
|
||||
return withAiQueue(async () => {
|
||||
const { runpodUrl, ocrModel } = getAiConfig();
|
||||
const ctrl = new AbortController();
|
||||
const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_OCR_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: ocrModel,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: opts.prompt || '<image>\nFree OCR.',
|
||||
images: [imageBase64],
|
||||
},
|
||||
],
|
||||
think: false,
|
||||
stream: false,
|
||||
keep_alive: KEEP_ALIVE,
|
||||
options: { num_predict: opts.numPredict ?? DEFAULT_OCR_NUM_PREDICT },
|
||||
}),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errText = await response.text().catch(() => '');
|
||||
throw new Error(`OCR model request failed: ${response.statusText} ${errText}`.trim());
|
||||
}
|
||||
const data = await response.json();
|
||||
return (data.message?.content || '').toString().trim();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user