Bläddra i källkod

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 3 månader sedan
förälder
incheckning
65c89bd54b

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

@@ -4,6 +4,7 @@ import { synthesize } from '../services/speechService'
 export type PlaySource =
 export type PlaySource =
   | { kind: 'tts'; text: string }
   | { kind: 'tts'; text: string }
   | { kind: 'blob'; blob: Blob }
   | { kind: 'blob'; blob: Blob }
+  | { kind: 'url'; url: string }
 
 
 export interface AudioPlayer {
 export interface AudioPlayer {
   /** Id of the message currently playing (audio element fired `playing`). */
   /** Id of the message currently playing (audio element fired `playing`). */
@@ -70,6 +71,14 @@ export function useAudioPlayer(): AudioPlayer {
       if (source.kind === 'blob') {
       if (source.kind === 'blob') {
         blob = source.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 {
       else {
         const cached = ttsCache.get(id)
         const cached = ttsCache.get(id)
         if (cached) {
         if (cached) {

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

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