import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch } from '../utils/proxy-fetch'; import { getValidWorksSquareAccessToken } from './works-square-session'; const MAX_AUDIO_BYTES = 26_214_400; export type LearningSpeechRequest = { audio: Uint8Array; fileName: string; mimeType: string; language?: string; }; type Dependencies = { fetchImpl?: typeof fetch; getAccessToken?: typeof getValidWorksSquareAccessToken; apiBaseUrl?: string; }; export function createLearningSpeechClient(dependencies: Dependencies = {}) { const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch; const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken; const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); async function transcribe(input: LearningSpeechRequest): Promise<{ text: string }> { const audio = input.audio instanceof Uint8Array ? input.audio : new Uint8Array(input.audio); if (!audio.byteLength || audio.byteLength > MAX_AUDIO_BYTES) throw new Error('语音长度不符合要求'); const token = await getAccessToken({ fetchImpl }); if (!token) throw new Error('请先登录'); const form = new FormData(); form.set('audio', new Blob([audio], { type: input.mimeType || 'audio/webm' }), input.fileName || 'voice.webm'); if (input.language?.trim()) form.set('language', input.language.trim()); const request = (accessToken: string) => fetchImpl(`${apiBaseUrl}/api/speech/transcriptions`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }); let response = await request(token); if (response.status === 401) { await response.body?.cancel().catch(() => undefined); const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true }); if (refreshed) response = await request(refreshed); } const payload = await response.json().catch(() => null) as { text?: unknown; detail?: unknown } | null; if (!response.ok || typeof payload?.text !== 'string' || !payload.text.trim()) { throw new Error(typeof payload?.detail === 'string' ? payload.detail : '语音识别失败'); } return { text: payload.text.trim() }; } return { transcribe }; }