Files
memind/health-document-ocr.mjs
T

120 lines
4.0 KiB
JavaScript
Raw Normal View History

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,
});
}