Просмотр исходного кода

feat(speaking): audio player URL mode + record audioDuration in engine

useAudioPlayer gains a url PlaySource so DetailedReport can replay the
S3 audioUrl. useDialogueEngine writes audioDuration into the student
PreviewChatMessage from both HTTP SSE and WS transcript events, and
toPreviewMessage forwards it from history on refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 2 месяцев назад
Родитель
Сommit
65c89bd54b

+ 9 - 0
src/views/Editor/EnglishSpeaking/composables/useAudioPlayer.ts

@@ -4,6 +4,7 @@ import { synthesize } from '../services/speechService'
 export type PlaySource =
   | { kind: 'tts'; text: string }
   | { kind: 'blob'; blob: Blob }
+  | { kind: 'url'; url: string }
 
 export interface AudioPlayer {
   /** Id of the message currently playing (audio element fired `playing`). */
@@ -70,6 +71,14 @@ export function useAudioPlayer(): AudioPlayer {
       if (source.kind === 'blob') {
         blob = source.blob
       }
+      else if (source.kind === 'url') {
+        synthAbort = new AbortController()
+        const res = await fetch(source.url, { signal: synthAbort.signal })
+        synthAbort = null
+        if (loadingId.value !== id) return
+        if (!res.ok) throw new Error(`fetch audio failed: ${res.status}`)
+        blob = await res.blob()
+      }
       else {
         const cached = ttsCache.get(id)
         if (cached) {

+ 7 - 0
src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts

@@ -39,6 +39,7 @@ export function useDialogueEngine() {
       status: 'done',
       turnId: message.clientTurnId ?? undefined,
       audioUrl: message.audioUrl ?? undefined,
+      audioDuration: message.audioDuration ?? undefined,
     }
   }
 
@@ -144,6 +145,9 @@ export function useDialogueEngine() {
         if (event.type === 'transcript') {
           studentMsg.content = event.text
           studentMsg.status = 'done'
+          if (event.audioDuration != null) {
+            studentMsg.audioDuration = event.audioDuration
+          }
           if (!isFinalRound.value) {
             messages.value.push(aiMsg)
           }
@@ -426,6 +430,9 @@ export function useDialogueEngine() {
         if (data.type === 'transcript' && studentMsg) {
           studentMsg.content = data.text
           studentMsg.status = 'done'
+          if (data.audioDuration != null) {
+            studentMsg.audioDuration = data.audioDuration
+          }
           if (aiMsg && !messages.value.includes(aiMsg)) {
             messages.value.push(aiMsg)
           }