Files
memind/health-document-ocr.mjs
T
john 2baf29b3ae feat(health): complete P0 health channel — baseline engine, page-data, MindSpace UI
Deliver encrypted health zone, observation API, baseline maturity pipeline,
page-data bindings, and H5/WeChat channel integration for health P0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:57 +08:00

120 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
}