export function buildHealthDocumentOcrPrompt() { return [ '你是医疗报告 OCR 助手。从体检单/化验单/影像报告照片中提取结构化信息。', '禁止诊断,只提取可见文字与数值。', '只输出严格 JSON,不要 Markdown:', '{"doc_type":"checkup|lab|imaging|prescription|discharge|other","report_date":"YYYY-MM-DD|null","institution":"","title":"","metrics":[{"name":"","value":"","unit":"","flag":""}],"summary":"","confidence":0.0}', '无法识别时 metrics 为空数组,summary 说明原因。', ].join('\n'); } export function parseHealthDocumentOcrJson(raw) { const text = Array.isArray(raw) ? raw.map((item) => (typeof item === 'string' ? item : item?.text ?? '')).join('') : String(raw ?? ''); const unfenced = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, ''); const start = unfenced.indexOf('{'); const end = unfenced.lastIndexOf('}'); if (start < 0 || end <= start) return null; try { return JSON.parse(unfenced.slice(start, end + 1)); } catch { return null; } } export function normalizeHealthDocumentOcr(parsed = {}) { const docType = String(parsed.doc_type ?? 'other').trim() || 'other'; const metrics = Array.isArray(parsed.metrics) ? parsed.metrics .filter((item) => item && (item.name || item.value)) .slice(0, 30) .map((item) => ({ name: String(item.name ?? '').trim(), value: String(item.value ?? '').trim(), unit: String(item.unit ?? '').trim() || null, flag: String(item.flag ?? '').trim() || null, })) : []; const summary = String(parsed.summary ?? '').trim(); const confidence = Number(parsed.confidence ?? 0); const hasSignal = metrics.length > 0 || summary.length > 20; return { ok: hasSignal, docType, reportDate: parsed.report_date ?? null, institution: String(parsed.institution ?? '').trim() || null, title: String(parsed.title ?? '').trim() || '健康报告', extractedMetrics: metrics, ocrText: summary, extractionStatus: metrics.length > 0 ? 'partial' : summary ? 'partial' : 'failed', confidence: Number.isFinite(confidence) ? confidence : null, }; } export async function extractHealthDocumentFromVision({ buffer, mimeType = 'image/jpeg', analyzeImagesWithVision, buildVisionThumbnailBuffer = null, } = {}) { if (!analyzeImagesWithVision || !Buffer.isBuffer(buffer) || !buffer.length) { return { ok: false, error: 'vision_unavailable', message: '报告 OCR 暂不可用' }; } let visionBuffer = buffer; let visionMimeType = mimeType; if (buildVisionThumbnailBuffer) { try { visionBuffer = await buildVisionThumbnailBuffer(buffer, mimeType); visionMimeType = 'image/jpeg'; } catch { visionBuffer = buffer; visionMimeType = mimeType; } } let raw; try { raw = await analyzeImagesWithVision( [{ mimeType, visionMimeType, data: visionBuffer.toString('base64'), }], buildHealthDocumentOcrPrompt(), ); } catch (error) { return { ok: false, error: 'vision_failed', message: error instanceof Error ? error.message : '报告 OCR 失败', }; } const parsed = parseHealthDocumentOcrJson(raw); if (!parsed) { return { ok: false, error: 'invalid_response', message: '未能解析报告内容' }; } const normalized = normalizeHealthDocumentOcr(parsed); if (!normalized.ok) { return { ok: false, error: 'no_content', message: '未能从报告中提取有效文字,原图已归档,关键数值请手输确认。', }; } return { ok: true, ...normalized }; } export async function extractHealthDocumentFromUrl(options = {}) { const { fetchHealthImageBuffer } = await import('./health-image-extract.mjs'); const { buffer, mimeType } = await fetchHealthImageBuffer(options); return extractHealthDocumentFromVision({ buffer, mimeType, analyzeImagesWithVision: options.analyzeImagesWithVision, buildVisionThumbnailBuffer: options.buildVisionThumbnailBuffer, }); }