feat(followup-audio): review full dialogue and bind roles before extraction

This commit is contained in:
2026-10-10 14:25:03 +08:00
parent f015b26ae1
commit b4acad8510
14 changed files with 360 additions and 43 deletions
@@ -27,7 +27,7 @@ export function evidencePlaybackLabel(quote: FollowupReviewItem['evidence'][numb
if (quote.channel !== undefined && quote.channel !== 0 && quote.channel !== 1) return ''
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 `回听片段 ${audioTimestamp(quote.start_ms)}–${audioTimestamp(quote.end_ms)}${quote.channel === undefined ? '' : ` · ${quote.channel === 0 ? '左' : '右'}声道`}`
return `回听对话 ${audioTimestamp(quote.start_ms)}–${audioTimestamp(quote.end_ms)}`
}
return `播放 ${audioTimestamp(quote.start_ms)}`
}
@@ -42,6 +42,25 @@ export function followupChannelLabel(channel?: FollowupAudioChannel, roles: Foll
const service = confirmed === 'left_service' ? channel === 0 : channel === 1
return `${side} · ${service ? '客服端' : '患者/家属端'}(人工标注)`
}
/** Preserve coarse ASR windows; never invent speaker turn order or word-level timestamps. */
export function dialogueWindows(segments: FollowupTranscriptSegment[]) {
const windows = new Map<string, { id: string; start_ms: number; end_ms: number; segments: FollowupTranscriptSegment[] }>()
for (const segment of segments) {
const id = `${segment.start_ms}:${segment.end_ms}`
if (!windows.has(id)) windows.set(id, { id, start_ms: segment.start_ms, end_ms: segment.end_ms, segments: [] })
windows.get(id)!.segments.push(segment)
}
return [...windows.values()].sort((a, b) => a.start_ms - b.start_ms || a.end_ms - b.end_ms)
.map(window => ({ ...window, segments: [...window.segments].sort((a, b) => (a.channel ?? -1) - (b.channel ?? -1)) }))
}
export function dialoguePlaybackStart(milliseconds: number): number {
return Number.isFinite(milliseconds) ? Math.max(0, milliseconds - 3000) : 0
}
export function channelPlaybackLabel(channel: FollowupAudioChannel, roles: FollowupChannelRoles): string {
if (roles === 'unconfirmed') return channel === 0 ? '只听左声道' : '只听右声道'
const service = roles === 'left_service' ? channel === 0 : channel === 1
return service ? '只听客服' : '只听患者/家属'
}
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 '未记录'