feat: add durable ASR-to-patient extraction and guarded review

This commit is contained in:
2026-10-08 15:08:42 +08:00
parent 8bb4bd07ae
commit 13948f5982
34 changed files with 2377 additions and 56 deletions
@@ -1,4 +1,4 @@
import type { FollowupCapabilities, FollowupField, FollowupFields, FollowupReviewItem, FollowupValue } from '@/api/followupAudio'
import type { FollowupCapabilities, FollowupField, FollowupFields, FollowupReviewItem, FollowupTaskDetail, FollowupTaskSummary, FollowupValue } from '@/api/followupAudio'
export const FOLLOWUP_CHUNK_BYTES = 2097152
export const followupKindLabels = { diagnosis: '诊单资料', blood: '血糖 / 血压', diet: '饮食', exercise: '运动', tracking_note: '跟踪备注' }
@@ -6,6 +6,31 @@ export const followupStatusLabels: Record<string, string> = {
queued: '等待处理', running: '正在分析', review: '待人工审核', applied: '已采纳',
failed: '处理失败', needs_reconciliation: '结果待核对', expired: '已过保留期'
}
const followupStageLabels: Record<string, string> = { transcribing: '正在转写', extracting: '正在提炼', validating: '正在校验', review: '待人工审核' }
export function followupTaskStatus(task: Pick<FollowupTaskSummary, 'status' | 'stage'>): string {
return (task.status === 'running' && followupStageLabels[task.stage]) || followupStatusLabels[task.status] || task.status
}
export function followupProgressText(task: FollowupTaskSummary): string {
const progress = task.pipeline_progress
const valid = progress && Number.isSafeInteger(progress.transcribed) && Number.isSafeInteger(progress.total)
&& progress.total > 0 && progress.transcribed >= 0 && progress.transcribed <= progress.total
return `${followupTaskStatus(task)}${valid ? ` · 转写片段 ${progress.transcribed} / ${progress.total}` : ''};人工审核前不会写入业务记录。`
}
export function retainedTranscriptNotice(task: FollowupTaskDetail): string {
if (!task.transcript_available || !task.transcript.trim()) return ''
if (task.status === 'failed') return '处理失败,已完成片段的转写仍可查看。保留转写不代表提炼成功,不能直接写入病历。'
if (task.status === 'needs_reconciliation') return '结果待核对,已完成片段的转写仍可查看。请先核对上游结果,不要重复发送。'
return ''
}
export function evidencePlaybackLabel(quote: FollowupReviewItem['evidence'][number]): string {
if (typeof quote.start_ms !== 'number' || !Number.isFinite(quote.start_ms) || quote.start_ms < 0) return ''
const timestamp = (ms: number) => { const seconds = Math.floor(ms / 1000); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}` }
if (quote.position_type === 'segment') {
if (!quote.segment_id || typeof quote.end_ms !== 'number' || !Number.isFinite(quote.end_ms) || quote.end_ms <= quote.start_ms) return ''
return `回听片段 ${timestamp(quote.start_ms)}–${timestamp(quote.end_ms)}`
}
return `播放 ${timestamp(quote.start_ms)}`
}
export const cloneReviewItems = (items: FollowupReviewItem[]): FollowupReviewItem[] => JSON.parse(JSON.stringify(items))
export function fieldText(value: FollowupValue | undefined, field: FollowupField): string {
if (value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)) return '未记录'