120 lines
4.0 KiB
JavaScript
120 lines
4.0 KiB
JavaScript
|
|
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,
|
|||
|
|
});
|
|||
|
|
}
|