feat(followup-audio): review full dialogue and bind roles before extraction
This commit is contained in:
@@ -189,7 +189,7 @@ test('upload actual panel sends actual recorded_at/model and chunks before compl
|
||||
f.panel.recordedAt.value = '2026-09-29 09:00:00'; f.panel.modelKey.value = 'openai'
|
||||
await f.panel.submitUpload(); await settle()
|
||||
assert.deepEqual(f.calls.filter(call => call[0] === 'chunk').map(call => call.slice(2)), [[0, 2097152], [1, 1]])
|
||||
assert.deepEqual(f.calls.find(call => call[0] === 'create')[1], { diagnosis_id: 10, upload_id: 'private-id', recorded_at: '2026-09-29 09:00:00', model_key: 'openai' })
|
||||
assert.deepEqual(f.calls.find(call => call[0] === 'create')[1], { diagnosis_id: 10, upload_id: 'private-id', recorded_at: '2026-09-29 09:00:00', model_key: 'openai', channel_roles: 'unconfirmed' })
|
||||
assert.ok(f.calls.findIndex(call => call[0] === 'complete') < f.calls.findIndex(call => call[0] === 'create'))
|
||||
assert.equal(f.panel.createUncertain.value, false)
|
||||
f.dispose()
|
||||
@@ -738,7 +738,7 @@ test('old-patient retained-transcript completion cannot expose text or reopen cu
|
||||
|
||||
test('segment playback uses trusted original-audio boundaries and never claims word-level alignment', () => {
|
||||
const quote = { text: '今天早晨测了6.5', segment_id: 'chunk-2', start_ms: 60000, end_ms: 120000, position_type: 'segment' }
|
||||
assert.equal(state.evidencePlaybackLabel(quote), '回听片段 1:00–2:00')
|
||||
assert.equal(state.evidencePlaybackLabel(quote), '回听对话 1:00–2:00')
|
||||
assert.equal(state.evidencePlaybackLabel(item().evidence[0]), '播放 0:05')
|
||||
for (const patch of [{ start_ms: -1 }, { start_ms: Infinity }, { end_ms: NaN }, { end_ms: 60000 }, { segment_id: '' }, { end_ms: undefined }]) assert.equal(state.evidencePlaybackLabel({ ...quote, ...patch }), '')
|
||||
const f = setupReview({ items: [item({ evidence: [quote] })] })
|
||||
@@ -914,7 +914,7 @@ test('stereo transcript order is window/channel, roles are unconfirmed until exp
|
||||
assert.equal(f.calls.filter(call => call[0] === 'apply').length, 0)
|
||||
f.dispose()
|
||||
const source = fs.readFileSync(path.join(componentDir, 'FollowupAudioPanel.vue'), 'utf8')
|
||||
assert.match(source, /同一区间两侧可能同时发言,不表示发言先后/)
|
||||
assert.match(source, /双方转写按同一时间区间并列展示,不表示发言先后/)
|
||||
assert.match(source, /<pre v-else class="transcript-text">/)
|
||||
})
|
||||
|
||||
@@ -1126,12 +1126,12 @@ test('late stereo blob and routing resume do not play or relabel another patient
|
||||
g.dispose()
|
||||
})
|
||||
|
||||
test('channel evidence is immutable through editor/draft and seek emits channel without guessing a role', async () => {
|
||||
test('channel evidence stays immutable while seek defaults to full dialogue', async () => {
|
||||
const quote = { text: '合成右侧原话', segment_id: 'right-1', start_ms: 15000, end_ms: 30000, position_type: 'segment', channel: 1 }
|
||||
const f = setupReview({ items: [item({ evidence: [quote] })], channelRoles: 'unconfirmed' })
|
||||
f.review.openReview('blood-1'); f.review.seekEvidence(f.review.editor.value.evidence[0])
|
||||
assert.deepEqual(f.events, [['seek', 15000, 1]])
|
||||
assert.equal(state.evidencePlaybackLabel(quote), '回听片段 0:15–0:30 · 右声道')
|
||||
assert.deepEqual(f.events, [['seek', 15000]])
|
||||
assert.equal(state.evidencePlaybackLabel(quote), '回听对话 0:15–0:30')
|
||||
f.review.editor.value.evidence[0].channel = 0
|
||||
f.review.setValue('fasting_blood_sugar', 7); f.review.confirmReview(true); f.review.saveReview(true)
|
||||
assert.deepEqual(f.props.items[0].evidence, [quote])
|
||||
@@ -1459,3 +1459,63 @@ test('manual human-reviewed business mode exposes upload and explicit Apply with
|
||||
await f.panel.applySelected(); assert.equal(f.calls.filter(x => x[0] === 'apply').length, 1)
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
|
||||
test('dialogue windows retain both channels, empty replies and coarse times without mutating evidence', () => {
|
||||
const segments = stereoSegments().reverse()
|
||||
const before = JSON.stringify(segments)
|
||||
const windows = state.dialogueWindows(segments)
|
||||
assert.deepEqual(windows.map(row => [row.start_ms, row.end_ms, row.segments.map(segment => segment.channel)]), [[0, 15000, [0, 1]], [15000, 30000, [0, 1]]])
|
||||
assert.equal(JSON.stringify(segments), before)
|
||||
assert.equal(state.dialogueWindows([{ ...segments[0], text: '' }])[0].segments[0].text, '')
|
||||
assert.equal(state.dialoguePlaybackStart(15000), 12000)
|
||||
assert.equal(state.dialoguePlaybackStart(1000), 0)
|
||||
assert.equal(state.dialoguePlaybackStart(NaN), 0)
|
||||
assert.equal(state.channelPlaybackLabel(1, 'left_service'), '只听患者/家属')
|
||||
assert.equal(state.channelPlaybackLabel(0, 'right_service'), '只听患者/家属')
|
||||
assert.equal(state.channelPlaybackLabel(1, 'unconfirmed'), '只听右声道')
|
||||
})
|
||||
|
||||
test('full dialogue evidence restores both channels after isolated playback and adds bounded context', async () => {
|
||||
const audio = fakeAudioContext()
|
||||
const f = setupPanel({ followupAudioDetail: async () => stereoTask({ channel_roles: 'left_service' }) }, {}, { playback: audio.playback })
|
||||
await settle(); await f.panel.openTask(1); f.panel.audioElement.value = audio.element
|
||||
await f.panel.loadAudio(15000, 0)
|
||||
assert.equal(f.panel.playbackChannel.value, 0)
|
||||
await f.panel.loadDialogue(15000)
|
||||
assert.equal(f.panel.playbackChannel.value, 'original')
|
||||
assert.equal(audio.element.currentTime, 12)
|
||||
assert.deepEqual(audio.nodes.find(node => node.name === 'media').edges.map(edge => edge.target.name), ['destination'])
|
||||
assert.deepEqual(audio.nodes.find(node => node.name === 'splitter').edges, [])
|
||||
assert.equal(audio.element.paused, false)
|
||||
assert.equal(f.panel.conversationWindows.value.length, 2)
|
||||
f.dispose()
|
||||
const source = fs.readFileSync(path.join(componentDir, 'FollowupAudioPanel.vue'), 'utf8')
|
||||
assert.match(source, /@seek="loadDialogue"/)
|
||||
assert.match(source, /@click="loadDialogue\(window.start_ms\)"/)
|
||||
assert.doesNotMatch(source, /@click="loadAudio\(segment.start_ms, segment.channel\)"/)
|
||||
})
|
||||
|
||||
test('upload sends explicitly selected pre-extraction roles and clears them on patient switch', async () => {
|
||||
for (const roles of ['left_service', 'right_service']) {
|
||||
const f = setupPanel()
|
||||
await settle(); f.panel.openUpload()
|
||||
f.panel.file.value = new File(['synthetic'], 'test.wav')
|
||||
f.panel.recordedAt.value = '2026-09-29 09:00:00'
|
||||
f.panel.uploadChannelRoles.value = roles
|
||||
await f.panel.submitUpload()
|
||||
assert.equal(f.calls.find(call => call[0] === 'create')[1].channel_roles, roles)
|
||||
f.props.diagnosisId = 20; await settle()
|
||||
assert.equal(f.panel.uploadChannelRoles.value, 'unconfirmed')
|
||||
f.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
test('a new recording never inherits the previous file role assignment', async () => {
|
||||
const f = setupPanel(); await settle()
|
||||
f.panel.uploadChannelRoles.value = 'left_service'
|
||||
f.panel.chooseFile({ target: { files: [new File(['synthetic'], 'new.wav')] } })
|
||||
assert.equal(f.panel.uploadChannelRoles.value, 'unconfirmed')
|
||||
f.dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user