|
@@ -11,6 +11,7 @@ import type {
|
|
|
WordAnalysisItem,
|
|
WordAnalysisItem,
|
|
|
} from '@/types/englishSpeaking'
|
|
} from '@/types/englishSpeaking'
|
|
|
import { SPEAKING_DIALOGUE_API_BASE_URL } from './speakingApiConfig'
|
|
import { SPEAKING_DIALOGUE_API_BASE_URL } from './speakingApiConfig'
|
|
|
|
|
+import { parseSSEStream } from './sseStream'
|
|
|
|
|
|
|
|
const API_BASE = SPEAKING_DIALOGUE_API_BASE_URL
|
|
const API_BASE = SPEAKING_DIALOGUE_API_BASE_URL
|
|
|
|
|
|
|
@@ -23,64 +24,6 @@ export class DialogueApiError extends Error {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ==================== SSE 解析 ====================
|
|
|
|
|
-
|
|
|
|
|
-async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<SSEEvent> {
|
|
|
|
|
- const decoder = new TextDecoder()
|
|
|
|
|
- let buffer = ''
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- while (true) {
|
|
|
|
|
- const { done, value } = await reader.read()
|
|
|
|
|
- if (done) break
|
|
|
|
|
-
|
|
|
|
|
- buffer += decoder.decode(value, { stream: true })
|
|
|
|
|
- const lines = buffer.split('\n')
|
|
|
|
|
- buffer = lines.pop() || ''
|
|
|
|
|
-
|
|
|
|
|
- let eventType = ''
|
|
|
|
|
- let data = ''
|
|
|
|
|
-
|
|
|
|
|
- for (const line of lines) {
|
|
|
|
|
- if (line.startsWith('event:')) {
|
|
|
|
|
- eventType = line.slice(6).trim()
|
|
|
|
|
- } else if (line.startsWith('data:')) {
|
|
|
|
|
- data = line.slice(5).trim()
|
|
|
|
|
- } else if (line === '' && eventType && data) {
|
|
|
|
|
- try {
|
|
|
|
|
- const parsed = JSON.parse(data)
|
|
|
|
|
- if (eventType === 'transcript') {
|
|
|
|
|
- yield {
|
|
|
|
|
- type: 'transcript',
|
|
|
|
|
- text: parsed.text,
|
|
|
|
|
- audioDuration: parsed.audioDuration ?? null,
|
|
|
|
|
- }
|
|
|
|
|
- } else if (eventType === 'token') {
|
|
|
|
|
- yield { type: 'token', text: parsed.content ?? parsed.text }
|
|
|
|
|
- } else if (eventType === 'done') {
|
|
|
|
|
- yield { type: 'done', isComplete: parsed.isComplete }
|
|
|
|
|
- } else if (eventType === 'image') {
|
|
|
|
|
- yield {
|
|
|
|
|
- type: 'image',
|
|
|
|
|
- url: parsed.url,
|
|
|
|
|
- round: parsed.round,
|
|
|
|
|
- }
|
|
|
|
|
- } else if (eventType === 'error') {
|
|
|
|
|
- yield { type: 'error', message: parsed.message }
|
|
|
|
|
- }
|
|
|
|
|
- } catch {
|
|
|
|
|
- // skip malformed JSON
|
|
|
|
|
- }
|
|
|
|
|
- eventType = ''
|
|
|
|
|
- data = ''
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } finally {
|
|
|
|
|
- reader.releaseLock()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// ==================== Backend shape types ====================
|
|
// ==================== Backend shape types ====================
|
|
|
|
|
|
|
|
interface BackendWordAnalysisItemRaw {
|
|
interface BackendWordAnalysisItemRaw {
|