@amaster.ai/asr-client 1.0.0-alpha.2 → 1.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +42 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +42 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -369,7 +369,9 @@ async function createWebRecorder(props) {
|
|
|
369
369
|
}
|
|
370
370
|
});
|
|
371
371
|
ctx = new AudioContext();
|
|
372
|
-
const blob = new Blob([RECORDER_WORKLET], {
|
|
372
|
+
const blob = new Blob([RECORDER_WORKLET], {
|
|
373
|
+
type: "application/javascript"
|
|
374
|
+
});
|
|
373
375
|
const url = URL.createObjectURL(blob);
|
|
374
376
|
await ctx.audioWorklet.addModule(url);
|
|
375
377
|
URL.revokeObjectURL(url);
|
|
@@ -387,7 +389,9 @@ async function createWebRecorder(props) {
|
|
|
387
389
|
source.connect(node);
|
|
388
390
|
props?.onStart?.();
|
|
389
391
|
} catch (error) {
|
|
390
|
-
props?.onError?.(
|
|
392
|
+
props?.onError?.(
|
|
393
|
+
error instanceof Error ? error : new Error(String(error))
|
|
394
|
+
);
|
|
391
395
|
cleanup();
|
|
392
396
|
}
|
|
393
397
|
},
|
|
@@ -401,7 +405,9 @@ async function createWebRecorder(props) {
|
|
|
401
405
|
offset += c.length;
|
|
402
406
|
}
|
|
403
407
|
const result = { pcm, sampleRate: ctx?.sampleRate ?? 16e3 };
|
|
404
|
-
const base64 = await blobToBase64(
|
|
408
|
+
const base64 = await blobToBase64(
|
|
409
|
+
pcmToWav(result.pcm, result.sampleRate)
|
|
410
|
+
);
|
|
405
411
|
props?.onStop?.(base64);
|
|
406
412
|
}
|
|
407
413
|
};
|
|
@@ -445,30 +451,52 @@ var AsrHttpClient = class {
|
|
|
445
451
|
constructor(config, path) {
|
|
446
452
|
this.recorder = null;
|
|
447
453
|
this.path = "";
|
|
454
|
+
this.recognizing = false;
|
|
448
455
|
this.http = config.http ?? (0, import_http_client.createHttpClient)();
|
|
449
456
|
this.config = config;
|
|
450
457
|
this.path = path;
|
|
451
458
|
}
|
|
452
459
|
async startRecording() {
|
|
460
|
+
if (this.recorder) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
453
463
|
const options = {
|
|
454
|
-
onStart:
|
|
464
|
+
onStart: () => {
|
|
465
|
+
this.config.onRecordingStart?.();
|
|
466
|
+
this.config.onStatusChange?.("recording");
|
|
467
|
+
},
|
|
455
468
|
onStop: async (base64) => {
|
|
469
|
+
this.config.onStatusChange?.("recognizing");
|
|
456
470
|
const text = await this.recognizeFile(base64);
|
|
457
471
|
this.config.onResult?.(text);
|
|
458
472
|
this.config.onRecordingStop?.();
|
|
473
|
+
this.config.onStatusChange?.("idle");
|
|
474
|
+
this.recorder = null;
|
|
459
475
|
},
|
|
460
|
-
onError:
|
|
476
|
+
onError: (err) => {
|
|
477
|
+
this.config.onError?.(err);
|
|
478
|
+
this.config.onStatusChange?.("idle");
|
|
479
|
+
this.recorder = null;
|
|
480
|
+
}
|
|
461
481
|
};
|
|
462
|
-
debugger;
|
|
463
482
|
this.recorder = await (this.config.createRecorder?.(options) ?? createWebRecorder(options));
|
|
464
483
|
await this.recorder.start();
|
|
465
484
|
}
|
|
466
485
|
async stopRecording() {
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
486
|
+
if (this.recorder) {
|
|
487
|
+
await this.recorder.stop();
|
|
488
|
+
this.recorder = null;
|
|
489
|
+
} else {
|
|
490
|
+
this.config.onResult?.("");
|
|
491
|
+
this.config.onRecordingStop?.();
|
|
492
|
+
this.config.onStatusChange?.("idle");
|
|
493
|
+
}
|
|
470
494
|
}
|
|
471
495
|
async recognizeFile(base64) {
|
|
496
|
+
if (this.recognizing) {
|
|
497
|
+
return "";
|
|
498
|
+
}
|
|
499
|
+
this.recognizing = true;
|
|
472
500
|
try {
|
|
473
501
|
const response = await this.http.request({
|
|
474
502
|
url: this.path,
|
|
@@ -491,7 +519,10 @@ var AsrHttpClient = class {
|
|
|
491
519
|
});
|
|
492
520
|
return response?.data?.choices?.[0]?.message?.content || "";
|
|
493
521
|
} catch (e) {
|
|
522
|
+
console.error("ASR recognition error:", e);
|
|
494
523
|
return "";
|
|
524
|
+
} finally {
|
|
525
|
+
this.recognizing = false;
|
|
495
526
|
}
|
|
496
527
|
}
|
|
497
528
|
async recordAndRecognize(ms) {
|
|
@@ -517,6 +548,8 @@ var AsrHttpClient = class {
|
|
|
517
548
|
});
|
|
518
549
|
return res?.data?.choices?.[0]?.message?.content || "";
|
|
519
550
|
} catch (e) {
|
|
551
|
+
console.error("ASR recognition error:", e);
|
|
552
|
+
return "";
|
|
520
553
|
}
|
|
521
554
|
}
|
|
522
555
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/asr-client.ts","../src/http-asr-client.ts"],"sourcesContent":["import createASRClient, {\n type ASRClientConfig,\n type ASRClient,\n type ASRLanguage,\n type SessionConfig,\n type TurnDetectionConfig,\n type InputAudioTranscriptionConfig,\n type ClientEvent,\n type ServerEvent,\n type SessionUpdateEvent,\n type InputAudioBufferAppendEvent,\n type InputAudioBufferCommitEvent,\n type SessionFinishEvent,\n type SessionCreatedEvent,\n type SessionUpdatedEvent,\n type TranscriptionTextEvent,\n type TranscriptionCompletedEvent,\n type SessionFinishedEvent,\n type ErrorEvent,\n} from \"./asr-client\";\n\nexport type {\n ASRClient,\n ASRClientConfig,\n ASRLanguage,\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n\nexport { createASRClient };\n\nimport createASRHttpClient, {\n type ASRHttpClientConfig,\n type ASRHttpClient,\n} from \"./http-asr-client\";\nexport type { ASRHttpClient, ASRHttpClientConfig };\nexport { createASRHttpClient };\n","/**\n * ASR Realtime WebSocket Client for Qwen-ASR Realtime API\n *\n * WebSocket-based real-time speech recognition for streaming transcription.\n * Follows the Qwen-ASR Realtime API protocol with proper event handling.\n *\n * @example\n * ```typescript\n * const client = createASRClient({\n * language: \"zh\",\n * enableVAD: true,\n * onReady() {\n * console.log(\"ASR connected\");\n * },\n * onTranscript(text, isFinal) {\n * console.log(isFinal ? \"[Final]\" : \"[Interim]\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * await client.connect();\n * await client.startRecording();\n * // ... stop ...\n * await client.stopRecording();\n * await client.close();\n * ```\n */\n\nconst ASR_PATH = \"/api/proxy/builtin/platform/qwen-asr-realtime/api-ws/v1/realtime\";\n\nexport type ASRLanguage =\n | \"zh\" // 中文(普通话、四川话、闽南语、吴语)\n | \"yue\" // 粤语\n | \"en\" // 英文\n | \"ja\" // 日语\n | \"de\" // 德语\n | \"ko\" // 韩语\n | \"ru\" // 俄语\n | \"fr\" // 法语\n | \"pt\" // 葡萄牙语\n | \"ar\" // 阿拉伯语\n | \"it\" // 意大利语\n | \"es\" // 西班牙语\n | \"hi\" // 印地语\n | \"id\" // 印尼语\n | \"th\" // 泰语\n | \"tr\" // 土耳其语\n | \"uk\" // 乌克兰语\n | \"vi\" // 越南语\n | \"cs\" // 捷克语\n | \"da\" // 丹麦语\n | \"fil\" // 菲律宾语\n | \"fi\" // 芬兰语\n | \"is\" // 冰岛语\n | \"ms\" // 马来语\n | \"no\" // 挪威语\n | \"pl\" // 波兰语\n | \"sv\"; // 瑞典语\n\n// Client event types (sent to server)\ntype ClientEventType =\n | \"session.update\"\n | \"input_audio_buffer.append\"\n | \"input_audio_buffer.commit\"\n | \"session.finish\";\n\n// Server event types (received from server)\ntype ServerEventType =\n | \"session.created\"\n | \"session.updated\"\n | \"input_audio_buffer.speech_started\"\n | \"input_audio_buffer.speech_stopped\"\n | \"input_audio_buffer.committed\"\n | \"conversation.item.input_audio_transcription.text\"\n | \"conversation.item.input_audio_transcription.completed\"\n | \"session.finished\"\n | \"error\";\n\n// Base event interface\ninterface BaseEvent {\n event_id: string;\n type: ClientEventType | ServerEventType;\n}\n\n// Client events\ninterface SessionUpdateEvent extends BaseEvent {\n type: \"session.update\";\n session: SessionConfig;\n}\n\ninterface InputAudioBufferAppendEvent extends BaseEvent {\n type: \"input_audio_buffer.append\";\n audio: string;\n}\n\ninterface InputAudioBufferCommitEvent extends BaseEvent {\n type: \"input_audio_buffer.commit\";\n}\n\ninterface SessionFinishEvent extends BaseEvent {\n type: \"session.finish\";\n}\n\ntype ClientEvent =\n | SessionUpdateEvent\n | InputAudioBufferAppendEvent\n | InputAudioBufferCommitEvent\n | SessionFinishEvent;\n\n// Server events\ninterface SessionCreatedEvent extends BaseEvent {\n type: \"session.created\";\n session: { id: string };\n}\n\ninterface SessionUpdatedEvent extends BaseEvent {\n type: \"session.updated\";\n session: SessionConfig;\n}\n\ninterface SpeechStartedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_started\";\n}\n\ninterface SpeechStoppedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_stopped\";\n}\n\ninterface InputAudioBufferCommittedEvent extends BaseEvent {\n type: \"input_audio_buffer.committed\";\n}\n\ninterface TranscriptionTextEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.text\";\n text?: string;\n stash?: string;\n transcript?: string;\n}\n\ninterface TranscriptionCompletedEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.completed\";\n text?: string;\n transcript?: string;\n}\n\ninterface SessionFinishedEvent extends BaseEvent {\n type: \"session.finished\";\n}\n\ninterface ErrorEvent extends BaseEvent {\n type: \"error\";\n error: {\n message: string;\n code?: string;\n };\n}\n\ntype ServerEvent =\n | SessionCreatedEvent\n | SessionUpdatedEvent\n | SpeechStartedEvent\n | SpeechStoppedEvent\n | InputAudioBufferCommittedEvent\n | TranscriptionTextEvent\n | TranscriptionCompletedEvent\n | SessionFinishedEvent\n | ErrorEvent;\n\n// Session configuration\ninterface TurnDetectionConfig {\n type: \"server_vad\";\n /** VAD检测阈值,推荐设为 0.0,默认值 0.2,范围 [-1, 1] */\n threshold?: number;\n /** VAD断句检测阈值(ms),推荐设为 400,默认值 800,范围 [200, 6000] */\n silence_duration_ms?: number;\n}\n\ninterface InputAudioTranscriptionConfig {\n language?: ASRLanguage;\n}\n\ninterface SessionConfig {\n input_audio_format?: \"pcm\" | \"opus\";\n sample_rate?: 16000 | 8000;\n input_audio_transcription?: InputAudioTranscriptionConfig;\n turn_detection?: TurnDetectionConfig | null;\n}\n\ninterface RealtimeRecorder {\n start(onAudio: (base64: string) => void): Promise<void>;\n stop(): Promise<void>;\n}\n\nasync function createRealtimeRecorder(): Promise<RealtimeRecorder> {\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n let source: MediaStreamAudioSourceNode | null = null;\n let processor: ScriptProcessorNode | null = null;\n\n return {\n async start(onAudio) {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },\n });\n\n log(\"✅ 麦克风已启动\", \"success\");\n log(\"💬 请对着麦克风说话,实时识别中...\", \"success\");\n\n ctx = new AudioContext({ sampleRate: 16000 });\n source = ctx.createMediaStreamSource(stream);\n processor = ctx.createScriptProcessor(4096, 1, 1);\n\n processor.onaudioprocess = (e) => {\n const inputData = e.inputBuffer.getChannelData(0);\n const pcm = new Int16Array(inputData.length);\n for (let i = 0; i < inputData.length; i++) {\n const s = Math.max(-1, Math.min(1, inputData[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n\n const bytes = new Uint8Array(pcm.buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] || 0);\n }\n\n onAudio(btoa(binary));\n };\n\n source.connect(processor);\n processor.connect(ctx.destination);\n },\n\n async stop() {\n stream?.getTracks().forEach((t) => t.stop());\n source?.disconnect();\n processor?.disconnect();\n if (ctx) {\n await ctx.close();\n }\n stream = null;\n ctx = null;\n source = null;\n processor = null;\n },\n };\n}\n\nconst log = (message: string, type = \"\") => {\n console.log(`[${type}]`, message);\n};\n\n// Generate unique event ID\nlet eventIdCounter = 0;\nfunction generateEventId(): string {\n return `event_${Date.now()}_${++eventIdCounter}`;\n}\n\nexport interface ASRClientConfig {\n /**\n * Audio format\n * @default \"pcm\"\n */\n audioFormat?: \"pcm\" | \"opus\";\n /**\n * Sample rate in Hz\n * @default 16000\n * @description 支持 16000 和 8000。设置为 8000 时,服务端会先升采样到16000Hz再进行识别,可能引入微小延迟。\n */\n sampleRate?: 16000 | 8000;\n /**\n * Audio source language\n * @default \"zh\"\n * @description 支持多种语言,包括 zh(中文)、yue(粤语)、en(英文)、ja(日语)等\n */\n language?: ASRLanguage;\n /**\n * Enable VAD (Voice Activity Detection) mode\n * @default true\n * @description true = VAD模式(服务端自动检测语音开始/结束),false = Manual模式(客户端手动控制)\n */\n enableVAD?: boolean;\n /**\n * VAD detection threshold\n * @default 0.2\n * @description 推荐设为 0.0。取值范围 [-1, 1]。较低的阈值会提高 VAD 的灵敏度。\n */\n vadThreshold?: number;\n /**\n * VAD silence duration threshold in milliseconds\n * @default 800\n * @description 推荐设为 400。取值范围 [200, 6000]。静音持续时长超过该阈值将被认为是语句结束。\n */\n vadSilenceDurationMs?: number;\n /**\n * Get access token for WebSocket authentication\n */\n getAccessToken?: () => string | null;\n /**\n * Called when connection is ready (session.created received and session.update sent)\n */\n onReady?: () => void;\n /**\n * Called when speech is detected (VAD mode only)\n */\n onSpeechStart?: () => void;\n /**\n * Called when speech stops (VAD mode only)\n */\n onSpeechEnd?: () => void;\n /**\n * Called on transcript result\n * @param text - Transcribed text\n * @param isFinal - Whether this is the final result\n */\n onTranscript?: (text: string, isFinal: boolean) => void;\n /**\n * Called when audio buffer is committed (non-VAD mode only)\n */\n onAudioBufferCommitted?: () => void;\n /**\n * Called when session is finished\n */\n onSessionFinished?: () => void;\n /**\n * Called on error\n */\n onError?: (error: Error) => void;\n /**\n * Called on close\n */\n onClose?: () => void;\n}\n\nexport interface ASRClient {\n /** Connect to ASR service and establish session */\n connect(): Promise<void>;\n /** Start recording from microphone */\n startRecording(): Promise<void>;\n /**\n * Stop recording\n * @description In non-VAD mode, this triggers recognition by sending input_audio_buffer.commit\n */\n stopRecording(): Promise<void>;\n /**\n * Close connection gracefully\n * @description Sends session.finish and waits for session.finished before closing\n */\n close(): Promise<void>;\n /**\n * Check if currently recording\n */\n isRecording(): boolean;\n /**\n * Check if connected to server\n */\n isConnected(): boolean;\n}\n\nfunction createASRClient(config: ASRClientConfig): ASRClient {\n const {\n onReady,\n onSpeechStart,\n onSpeechEnd,\n onTranscript,\n onAudioBufferCommitted,\n onSessionFinished,\n onError,\n onClose,\n getAccessToken,\n audioFormat = \"pcm\",\n sampleRate = 16000,\n language = \"zh\",\n enableVAD = true,\n vadThreshold = 0.2,\n vadSilenceDurationMs = 400,\n } = config;\n\n let ws: WebSocket | null = null;\n let recorder: RealtimeRecorder | null = null;\n let isRecordingFlag = false;\n let isClosing = false;\n\n const path = ASR_PATH;\n\n /**\n * Send event to server\n */\n function sendEvent(event: ClientEvent): void {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n ws.send(JSON.stringify(event));\n }\n\n /**\n * Build session configuration\n */\n function buildSessionConfig(): SessionConfig {\n const sessionConfig: SessionConfig = {\n input_audio_format: audioFormat,\n sample_rate: sampleRate,\n input_audio_transcription: {\n language,\n },\n };\n\n if (enableVAD) {\n sessionConfig.turn_detection = {\n type: \"server_vad\",\n threshold: vadThreshold,\n silence_duration_ms: vadSilenceDurationMs,\n };\n } else {\n // Non-VAD mode: set turn_detection to null\n sessionConfig.turn_detection = null;\n }\n\n return sessionConfig;\n }\n\n /**\n * Send session.update event\n */\n function sendSessionUpdate(): void {\n const event: SessionUpdateEvent = {\n event_id: generateEventId(),\n type: \"session.update\",\n session: buildSessionConfig(),\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.append event\n */\n function sendAudioBufferAppend(audio: string): void {\n const event: InputAudioBufferAppendEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.append\",\n audio,\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.commit event (for non-VAD mode)\n */\n function sendAudioBufferCommit(): void {\n const event: InputAudioBufferCommitEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.commit\",\n };\n sendEvent(event);\n }\n\n /**\n * Send session.finish event\n */\n function sendSessionFinish(): void {\n const event: SessionFinishEvent = {\n event_id: generateEventId(),\n type: \"session.finish\",\n };\n sendEvent(event);\n }\n\n /**\n * Handle server events\n */\n function handleServerEvent(data: ServerEvent): void {\n switch (data.type) {\n case \"session.created\":\n // Send session.update immediately after session.created\n try {\n sendSessionUpdate();\n } catch (err) {\n onError?.(\n new Error(\n \"Failed to send session.update: \" + (err instanceof Error ? err.message : String(err))\n )\n );\n }\n break;\n\n case \"session.updated\":\n // Server confirmed session configuration\n onReady?.();\n break;\n\n case \"input_audio_buffer.speech_started\":\n onSpeechStart?.();\n break;\n\n case \"input_audio_buffer.speech_stopped\":\n onSpeechEnd?.();\n break;\n\n case \"input_audio_buffer.committed\":\n onAudioBufferCommitted?.();\n break;\n\n case \"conversation.item.input_audio_transcription.text\":\n onTranscript?.(data.text || data.stash || data.transcript || \"\", false);\n break;\n\n case \"conversation.item.input_audio_transcription.completed\":\n onTranscript?.(data.text || data.transcript || \"\", true);\n break;\n\n case \"session.finished\":\n onSessionFinished?.();\n // Close WebSocket after receiving session.finished\n close();\n break;\n\n case \"error\":\n const err = new Error(data.error?.message || \"ASR error\");\n onError?.(err);\n break;\n\n default:\n // Unknown event type\n console.warn(\"[ASR] Unknown server event:\", (data as ServerEvent).type);\n }\n }\n\n async function connect(): Promise<void> {\n // Build WebSocket URL with optional token parameter\n let wsUrl = path;\n if (getAccessToken) {\n const token = getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n wsUrl = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n // Support both ws:// and wss:// protocols\n if (typeof window !== \"undefined\" && window.location) {\n const protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n if (!wsUrl.startsWith(\"ws://\") && !wsUrl.startsWith(\"wss://\")) {\n wsUrl = `${protocol}//${window.location.host}${wsUrl}`;\n }\n }\n\n ws = new WebSocket(wsUrl);\n\n return new Promise((resolve, reject) => {\n if (!ws) {\n reject(new Error(\"Failed to create WebSocket\"));\n return;\n }\n\n ws.onopen = () => {\n log(\"WebSocket connected\", \"success\");\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as ServerEvent;\n handleServerEvent(data);\n\n // Resolve on session.updated (ready state)\n if (data.type === \"session.updated\") {\n resolve();\n }\n } catch (err) {\n const error = new Error(\n \"Failed to parse server message: \" + (err instanceof Error ? err.message : String(err))\n );\n onError?.(error);\n reject(error);\n }\n };\n\n ws.onerror = (error) => {\n console.error(\"WebSocket error:\", error);\n const err = new Error(\"WebSocket error\");\n onError?.(err);\n reject(err);\n };\n\n ws.onclose = () => {\n isRecordingFlag = false;\n ws = null;\n onClose?.();\n };\n });\n }\n\n async function startRecording(): Promise<void> {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n\n if (isRecordingFlag) {\n throw new Error(\"Already recording\");\n }\n\n recorder = await createRealtimeRecorder();\n isRecordingFlag = true;\n\n await recorder.start((audio) => {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n try {\n sendAudioBufferAppend(audio);\n } catch (err) {\n console.error(\"[ASR] Failed to send audio:\", err);\n }\n });\n }\n\n async function stopRecording(): Promise<void> {\n if (!isRecordingFlag) {\n return;\n }\n\n try {\n await recorder?.stop();\n } catch (err) {\n console.error(\"[ASR] Error stopping recorder:\", err);\n }\n recorder = null;\n isRecordingFlag = false;\n\n // In non-VAD mode, send commit to trigger recognition\n if (!enableVAD && ws?.readyState === WebSocket.OPEN) {\n try {\n sendAudioBufferCommit();\n } catch (err) {\n console.error(\"[ASR] Failed to send commit:\", err);\n }\n }\n }\n\n async function close(): Promise<void> {\n if (isClosing) {\n return;\n }\n\n isClosing = true;\n\n // Stop recording first\n await stopRecording();\n\n // Send session.finish if connected\n if (ws?.readyState === WebSocket.OPEN) {\n try {\n sendSessionFinish();\n // Wait a bit for session.finished response\n await new Promise((resolve) => setTimeout(resolve, 1000));\n } catch (err) {\n console.error(\"[ASR] Failed to send session.finish:\", err);\n }\n }\n\n // Close WebSocket\n if (ws && ws?.readyState !== WebSocket.CLOSING && ws?.readyState !== WebSocket.CLOSED) {\n ws?.close();\n }\n ws = null;\n isClosing = false;\n }\n\n function isRecording(): boolean {\n return isRecordingFlag;\n }\n\n function isConnected(): boolean {\n return ws !== null && ws.readyState === WebSocket.OPEN;\n }\n\n return {\n connect,\n startRecording,\n stopRecording,\n close,\n isRecording,\n isConnected,\n };\n}\n\nexport default (authConfig: Pick<ASRClientConfig, \"getAccessToken\">) => (config: ASRClientConfig) =>\n createASRClient({ ...authConfig, ...config });\n\nexport type {\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n *\n * HTTP-based speech recognition suitable for press-to-talk scenarios where you hold to speak\n * and release to recognize. Good for voice messages, voice search, etc.\n *\n * @example\n * ```typescript\n * const client = createASRHttpClient({\n * onRecordingStart() {\n * console.log(\"Recording started\");\n * },\n * onRecordingStop() {\n * console.log(\"Recording stopped\");\n * },\n * onResult(text) {\n * console.log(\"Recognized:\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * // Hold to speak, release to recognize\n * await client.startRecording();\n * // ... stop ...\n * const result = await client.stopRecording();\n * ```\n */\n\nimport { createHttpClient, type HttpClient } from \"@amaster.ai/http-client\";\n\nconst ASR_HTTP_PATH = \"/api/proxy/builtin/platform/qwen-asr/compatible-mode/v1/chat/completions\";\n\n// 录音内核\nconst RECORDER_WORKLET = `\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0].slice(0));\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n`;\n\ninterface Recorder {\n /** Start recording */\n start(): Promise<void>;\n /**\n * Stop recording and get base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @returns Base64-encoded WAV audio data\n */\n stop(): Promise<void>;\n}\n\ninterface RecorderOptions {\n /** Called when recording starts */\n onStart?: () => void;\n /**\n * Called when recording stops, with base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @param base64 - Base64-encoded WAV audio data\n * @returns void\n */\n onStop?: (base64: string) => void;\n onError?: (error: Error) => void;\n}\n\n// 创建录音器\nasync function createWebRecorder(props?: RecorderOptions): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n const cleanup = () => {\n try {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n } catch (e) {}\n };\n\n return {\n async start() {\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n ctx = new AudioContext();\n\n const blob = new Blob([RECORDER_WORKLET], { type: \"application/javascript\" });\n const url = URL.createObjectURL(blob);\n await ctx.audioWorklet.addModule(url);\n URL.revokeObjectURL(url);\n\n source = ctx.createMediaStreamSource(stream);\n node = new AudioWorkletNode(ctx, \"recorder-processor\");\n\n node.port.onmessage = (e) => {\n const input = e.data as Float32Array;\n const pcm = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n\n props?.onStart?.();\n } catch (error) {\n props?.onError?.(error instanceof Error ? error : new Error(String(error)));\n cleanup();\n }\n },\n\n async stop() {\n cleanup();\n\n const total = chunks.reduce((s, c) => s + c.length, 0);\n const pcm = new Int16Array(total);\n let offset = 0;\n for (const c of chunks) {\n pcm.set(c, offset);\n offset += c.length;\n }\n\n const result = { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\n const base64 = await blobToBase64(pcmToWav(result.pcm, result.sampleRate));\n props?.onStop?.(base64);\n },\n };\n}\n\n/**\n * Convert PCM to WAV Blob\n * @param pcm - PCM data\n * @param sampleRate - Sample rate\n * @returns WAV Blob\n */\nfunction pcmToWav(pcm: Int16Array, sampleRate: number): Blob {\n const buffer = new ArrayBuffer(44 + pcm.length * 2);\n const view = new DataView(buffer);\n\n const write = (o: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(o + i, s.charCodeAt(i));\n };\n\n write(0, \"RIFF\");\n view.setUint32(4, 36 + pcm.length * 2, true);\n write(8, \"WAVE\");\n write(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n write(36, \"data\");\n view.setUint32(40, pcm.length * 2, true);\n\n for (let i = 0; i < pcm.length; i++) {\n view.setInt16(44 + i * 2, pcm[i] || 0, true);\n }\n\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const result = reader.result as string;\n resolve(result.split(\",\")[1] || \"\");\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\nexport interface ASRHttpClientConfig {\n http?: HttpClient;\n /** Get access token */\n getAccessToken?(): string | null;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\n /** Create custom recorder */\n createRecorder?(options?: RecorderOptions): Promise<Recorder>;\n /** Called when recording starts */\n onRecordingStart?: () => void;\n /** Called when recording stops */\n onRecordingStop?: () => void;\n /** Called with recognition result */\n onResult?: (text: string) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\nexport interface ASRHttpClient {\n /** Start recording (press-to-talk) */\n startRecording(): Promise<void>;\n /** Stop recording and get result */\n stopRecording(): Promise<void>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<void>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(base64: string): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nclass AsrHttpClient {\n http: HttpClient;\n recorder: Recorder | null = null;\n path: string = \"\";\n config: ASRHttpClientConfig;\n constructor(config: ASRHttpClientConfig, path: string) {\n this.http = config.http ?? createHttpClient();\n this.config = config;\n this.path = path;\n }\n async startRecording() {\n const options = {\n onStart: this.config.onRecordingStart,\n onStop: async (base64: string) => {\n const text = await this.recognizeFile(base64);\n this.config.onResult?.(text);\n this.config.onRecordingStop?.();\n },\n onError: this.config.onError,\n };\n debugger\n this.recorder = await (this.config.createRecorder?.(options) ?? createWebRecorder(options));\n await this.recorder.start();\n }\n\n async stopRecording() {\n if (!this.recorder) throw new Error(\"Not recording\");\n await this.recorder.stop();\n this.recorder = null;\n }\n\n async recognizeFile(base64: string): Promise<string> {\n try {\n const response = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [\n {\n type: \"input_audio\",\n input_audio: { data: `data:audio/wav;base64,${base64}` },\n },\n ],\n },\n ],\n }),\n });\n\n return (response as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n return \"\";\n }\n }\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n await this.stopRecording();\n }\n\n async recognizeUrl(url: string) {\n try {\n const res = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [{ type: \"input_audio\", input_audio: { url } }],\n },\n ],\n }),\n });\n return (res as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {}\n }\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let path = ASR_HTTP_PATH;\n\n if (config.getAccessToken) {\n const token = config.getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n path = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n return new AsrHttpClient(config, path);\n}\n\nexport default (authConfig: Pick<ASRHttpClientConfig, \"getAccessToken\">) =>\n (config: ASRHttpClientConfig) =>\n createASRHttpClient({ ...authConfig, ...config });\n"],"mappings":"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","names":["err"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/asr-client.ts","../src/http-asr-client.ts"],"sourcesContent":["import createASRClient, {\n type ASRClientConfig,\n type ASRClient,\n type ASRLanguage,\n type SessionConfig,\n type TurnDetectionConfig,\n type InputAudioTranscriptionConfig,\n type ClientEvent,\n type ServerEvent,\n type SessionUpdateEvent,\n type InputAudioBufferAppendEvent,\n type InputAudioBufferCommitEvent,\n type SessionFinishEvent,\n type SessionCreatedEvent,\n type SessionUpdatedEvent,\n type TranscriptionTextEvent,\n type TranscriptionCompletedEvent,\n type SessionFinishedEvent,\n type ErrorEvent,\n} from \"./asr-client\";\n\nexport type {\n ASRClient,\n ASRClientConfig,\n ASRLanguage,\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n\nexport { createASRClient };\n\nimport createASRHttpClient, {\n type ASRHttpClientConfig,\n type ASRHttpClient,\n} from \"./http-asr-client\";\nexport type { ASRHttpClient, ASRHttpClientConfig };\nexport { createASRHttpClient };\n","/**\n * ASR Realtime WebSocket Client for Qwen-ASR Realtime API\n *\n * WebSocket-based real-time speech recognition for streaming transcription.\n * Follows the Qwen-ASR Realtime API protocol with proper event handling.\n *\n * @example\n * ```typescript\n * const client = createASRClient({\n * language: \"zh\",\n * enableVAD: true,\n * onReady() {\n * console.log(\"ASR connected\");\n * },\n * onTranscript(text, isFinal) {\n * console.log(isFinal ? \"[Final]\" : \"[Interim]\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * await client.connect();\n * await client.startRecording();\n * // ... stop ...\n * await client.stopRecording();\n * await client.close();\n * ```\n */\n\nconst ASR_PATH = \"/api/proxy/builtin/platform/qwen-asr-realtime/api-ws/v1/realtime\";\n\nexport type ASRLanguage =\n | \"zh\" // 中文(普通话、四川话、闽南语、吴语)\n | \"yue\" // 粤语\n | \"en\" // 英文\n | \"ja\" // 日语\n | \"de\" // 德语\n | \"ko\" // 韩语\n | \"ru\" // 俄语\n | \"fr\" // 法语\n | \"pt\" // 葡萄牙语\n | \"ar\" // 阿拉伯语\n | \"it\" // 意大利语\n | \"es\" // 西班牙语\n | \"hi\" // 印地语\n | \"id\" // 印尼语\n | \"th\" // 泰语\n | \"tr\" // 土耳其语\n | \"uk\" // 乌克兰语\n | \"vi\" // 越南语\n | \"cs\" // 捷克语\n | \"da\" // 丹麦语\n | \"fil\" // 菲律宾语\n | \"fi\" // 芬兰语\n | \"is\" // 冰岛语\n | \"ms\" // 马来语\n | \"no\" // 挪威语\n | \"pl\" // 波兰语\n | \"sv\"; // 瑞典语\n\n// Client event types (sent to server)\ntype ClientEventType =\n | \"session.update\"\n | \"input_audio_buffer.append\"\n | \"input_audio_buffer.commit\"\n | \"session.finish\";\n\n// Server event types (received from server)\ntype ServerEventType =\n | \"session.created\"\n | \"session.updated\"\n | \"input_audio_buffer.speech_started\"\n | \"input_audio_buffer.speech_stopped\"\n | \"input_audio_buffer.committed\"\n | \"conversation.item.input_audio_transcription.text\"\n | \"conversation.item.input_audio_transcription.completed\"\n | \"session.finished\"\n | \"error\";\n\n// Base event interface\ninterface BaseEvent {\n event_id: string;\n type: ClientEventType | ServerEventType;\n}\n\n// Client events\ninterface SessionUpdateEvent extends BaseEvent {\n type: \"session.update\";\n session: SessionConfig;\n}\n\ninterface InputAudioBufferAppendEvent extends BaseEvent {\n type: \"input_audio_buffer.append\";\n audio: string;\n}\n\ninterface InputAudioBufferCommitEvent extends BaseEvent {\n type: \"input_audio_buffer.commit\";\n}\n\ninterface SessionFinishEvent extends BaseEvent {\n type: \"session.finish\";\n}\n\ntype ClientEvent =\n | SessionUpdateEvent\n | InputAudioBufferAppendEvent\n | InputAudioBufferCommitEvent\n | SessionFinishEvent;\n\n// Server events\ninterface SessionCreatedEvent extends BaseEvent {\n type: \"session.created\";\n session: { id: string };\n}\n\ninterface SessionUpdatedEvent extends BaseEvent {\n type: \"session.updated\";\n session: SessionConfig;\n}\n\ninterface SpeechStartedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_started\";\n}\n\ninterface SpeechStoppedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_stopped\";\n}\n\ninterface InputAudioBufferCommittedEvent extends BaseEvent {\n type: \"input_audio_buffer.committed\";\n}\n\ninterface TranscriptionTextEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.text\";\n text?: string;\n stash?: string;\n transcript?: string;\n}\n\ninterface TranscriptionCompletedEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.completed\";\n text?: string;\n transcript?: string;\n}\n\ninterface SessionFinishedEvent extends BaseEvent {\n type: \"session.finished\";\n}\n\ninterface ErrorEvent extends BaseEvent {\n type: \"error\";\n error: {\n message: string;\n code?: string;\n };\n}\n\ntype ServerEvent =\n | SessionCreatedEvent\n | SessionUpdatedEvent\n | SpeechStartedEvent\n | SpeechStoppedEvent\n | InputAudioBufferCommittedEvent\n | TranscriptionTextEvent\n | TranscriptionCompletedEvent\n | SessionFinishedEvent\n | ErrorEvent;\n\n// Session configuration\ninterface TurnDetectionConfig {\n type: \"server_vad\";\n /** VAD检测阈值,推荐设为 0.0,默认值 0.2,范围 [-1, 1] */\n threshold?: number;\n /** VAD断句检测阈值(ms),推荐设为 400,默认值 800,范围 [200, 6000] */\n silence_duration_ms?: number;\n}\n\ninterface InputAudioTranscriptionConfig {\n language?: ASRLanguage;\n}\n\ninterface SessionConfig {\n input_audio_format?: \"pcm\" | \"opus\";\n sample_rate?: 16000 | 8000;\n input_audio_transcription?: InputAudioTranscriptionConfig;\n turn_detection?: TurnDetectionConfig | null;\n}\n\ninterface RealtimeRecorder {\n start(onAudio: (base64: string) => void): Promise<void>;\n stop(): Promise<void>;\n}\n\nasync function createRealtimeRecorder(): Promise<RealtimeRecorder> {\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n let source: MediaStreamAudioSourceNode | null = null;\n let processor: ScriptProcessorNode | null = null;\n\n return {\n async start(onAudio) {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },\n });\n\n log(\"✅ 麦克风已启动\", \"success\");\n log(\"💬 请对着麦克风说话,实时识别中...\", \"success\");\n\n ctx = new AudioContext({ sampleRate: 16000 });\n source = ctx.createMediaStreamSource(stream);\n processor = ctx.createScriptProcessor(4096, 1, 1);\n\n processor.onaudioprocess = (e) => {\n const inputData = e.inputBuffer.getChannelData(0);\n const pcm = new Int16Array(inputData.length);\n for (let i = 0; i < inputData.length; i++) {\n const s = Math.max(-1, Math.min(1, inputData[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n\n const bytes = new Uint8Array(pcm.buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] || 0);\n }\n\n onAudio(btoa(binary));\n };\n\n source.connect(processor);\n processor.connect(ctx.destination);\n },\n\n async stop() {\n stream?.getTracks().forEach((t) => t.stop());\n source?.disconnect();\n processor?.disconnect();\n if (ctx) {\n await ctx.close();\n }\n stream = null;\n ctx = null;\n source = null;\n processor = null;\n },\n };\n}\n\nconst log = (message: string, type = \"\") => {\n console.log(`[${type}]`, message);\n};\n\n// Generate unique event ID\nlet eventIdCounter = 0;\nfunction generateEventId(): string {\n return `event_${Date.now()}_${++eventIdCounter}`;\n}\n\nexport interface ASRClientConfig {\n /**\n * Audio format\n * @default \"pcm\"\n */\n audioFormat?: \"pcm\" | \"opus\";\n /**\n * Sample rate in Hz\n * @default 16000\n * @description 支持 16000 和 8000。设置为 8000 时,服务端会先升采样到16000Hz再进行识别,可能引入微小延迟。\n */\n sampleRate?: 16000 | 8000;\n /**\n * Audio source language\n * @default \"zh\"\n * @description 支持多种语言,包括 zh(中文)、yue(粤语)、en(英文)、ja(日语)等\n */\n language?: ASRLanguage;\n /**\n * Enable VAD (Voice Activity Detection) mode\n * @default true\n * @description true = VAD模式(服务端自动检测语音开始/结束),false = Manual模式(客户端手动控制)\n */\n enableVAD?: boolean;\n /**\n * VAD detection threshold\n * @default 0.2\n * @description 推荐设为 0.0。取值范围 [-1, 1]。较低的阈值会提高 VAD 的灵敏度。\n */\n vadThreshold?: number;\n /**\n * VAD silence duration threshold in milliseconds\n * @default 800\n * @description 推荐设为 400。取值范围 [200, 6000]。静音持续时长超过该阈值将被认为是语句结束。\n */\n vadSilenceDurationMs?: number;\n /**\n * Get access token for WebSocket authentication\n */\n getAccessToken?: () => string | null;\n /**\n * Called when connection is ready (session.created received and session.update sent)\n */\n onReady?: () => void;\n /**\n * Called when speech is detected (VAD mode only)\n */\n onSpeechStart?: () => void;\n /**\n * Called when speech stops (VAD mode only)\n */\n onSpeechEnd?: () => void;\n /**\n * Called on transcript result\n * @param text - Transcribed text\n * @param isFinal - Whether this is the final result\n */\n onTranscript?: (text: string, isFinal: boolean) => void;\n /**\n * Called when audio buffer is committed (non-VAD mode only)\n */\n onAudioBufferCommitted?: () => void;\n /**\n * Called when session is finished\n */\n onSessionFinished?: () => void;\n /**\n * Called on error\n */\n onError?: (error: Error) => void;\n /**\n * Called on close\n */\n onClose?: () => void;\n}\n\nexport interface ASRClient {\n /** Connect to ASR service and establish session */\n connect(): Promise<void>;\n /** Start recording from microphone */\n startRecording(): Promise<void>;\n /**\n * Stop recording\n * @description In non-VAD mode, this triggers recognition by sending input_audio_buffer.commit\n */\n stopRecording(): Promise<void>;\n /**\n * Close connection gracefully\n * @description Sends session.finish and waits for session.finished before closing\n */\n close(): Promise<void>;\n /**\n * Check if currently recording\n */\n isRecording(): boolean;\n /**\n * Check if connected to server\n */\n isConnected(): boolean;\n}\n\nfunction createASRClient(config: ASRClientConfig): ASRClient {\n const {\n onReady,\n onSpeechStart,\n onSpeechEnd,\n onTranscript,\n onAudioBufferCommitted,\n onSessionFinished,\n onError,\n onClose,\n getAccessToken,\n audioFormat = \"pcm\",\n sampleRate = 16000,\n language = \"zh\",\n enableVAD = true,\n vadThreshold = 0.2,\n vadSilenceDurationMs = 400,\n } = config;\n\n let ws: WebSocket | null = null;\n let recorder: RealtimeRecorder | null = null;\n let isRecordingFlag = false;\n let isClosing = false;\n\n const path = ASR_PATH;\n\n /**\n * Send event to server\n */\n function sendEvent(event: ClientEvent): void {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n ws.send(JSON.stringify(event));\n }\n\n /**\n * Build session configuration\n */\n function buildSessionConfig(): SessionConfig {\n const sessionConfig: SessionConfig = {\n input_audio_format: audioFormat,\n sample_rate: sampleRate,\n input_audio_transcription: {\n language,\n },\n };\n\n if (enableVAD) {\n sessionConfig.turn_detection = {\n type: \"server_vad\",\n threshold: vadThreshold,\n silence_duration_ms: vadSilenceDurationMs,\n };\n } else {\n // Non-VAD mode: set turn_detection to null\n sessionConfig.turn_detection = null;\n }\n\n return sessionConfig;\n }\n\n /**\n * Send session.update event\n */\n function sendSessionUpdate(): void {\n const event: SessionUpdateEvent = {\n event_id: generateEventId(),\n type: \"session.update\",\n session: buildSessionConfig(),\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.append event\n */\n function sendAudioBufferAppend(audio: string): void {\n const event: InputAudioBufferAppendEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.append\",\n audio,\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.commit event (for non-VAD mode)\n */\n function sendAudioBufferCommit(): void {\n const event: InputAudioBufferCommitEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.commit\",\n };\n sendEvent(event);\n }\n\n /**\n * Send session.finish event\n */\n function sendSessionFinish(): void {\n const event: SessionFinishEvent = {\n event_id: generateEventId(),\n type: \"session.finish\",\n };\n sendEvent(event);\n }\n\n /**\n * Handle server events\n */\n function handleServerEvent(data: ServerEvent): void {\n switch (data.type) {\n case \"session.created\":\n // Send session.update immediately after session.created\n try {\n sendSessionUpdate();\n } catch (err) {\n onError?.(\n new Error(\n \"Failed to send session.update: \" + (err instanceof Error ? err.message : String(err))\n )\n );\n }\n break;\n\n case \"session.updated\":\n // Server confirmed session configuration\n onReady?.();\n break;\n\n case \"input_audio_buffer.speech_started\":\n onSpeechStart?.();\n break;\n\n case \"input_audio_buffer.speech_stopped\":\n onSpeechEnd?.();\n break;\n\n case \"input_audio_buffer.committed\":\n onAudioBufferCommitted?.();\n break;\n\n case \"conversation.item.input_audio_transcription.text\":\n onTranscript?.(data.text || data.stash || data.transcript || \"\", false);\n break;\n\n case \"conversation.item.input_audio_transcription.completed\":\n onTranscript?.(data.text || data.transcript || \"\", true);\n break;\n\n case \"session.finished\":\n onSessionFinished?.();\n // Close WebSocket after receiving session.finished\n close();\n break;\n\n case \"error\":\n const err = new Error(data.error?.message || \"ASR error\");\n onError?.(err);\n break;\n\n default:\n // Unknown event type\n console.warn(\"[ASR] Unknown server event:\", (data as ServerEvent).type);\n }\n }\n\n async function connect(): Promise<void> {\n // Build WebSocket URL with optional token parameter\n let wsUrl = path;\n if (getAccessToken) {\n const token = getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n wsUrl = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n // Support both ws:// and wss:// protocols\n if (typeof window !== \"undefined\" && window.location) {\n const protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n if (!wsUrl.startsWith(\"ws://\") && !wsUrl.startsWith(\"wss://\")) {\n wsUrl = `${protocol}//${window.location.host}${wsUrl}`;\n }\n }\n\n ws = new WebSocket(wsUrl);\n\n return new Promise((resolve, reject) => {\n if (!ws) {\n reject(new Error(\"Failed to create WebSocket\"));\n return;\n }\n\n ws.onopen = () => {\n log(\"WebSocket connected\", \"success\");\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as ServerEvent;\n handleServerEvent(data);\n\n // Resolve on session.updated (ready state)\n if (data.type === \"session.updated\") {\n resolve();\n }\n } catch (err) {\n const error = new Error(\n \"Failed to parse server message: \" + (err instanceof Error ? err.message : String(err))\n );\n onError?.(error);\n reject(error);\n }\n };\n\n ws.onerror = (error) => {\n console.error(\"WebSocket error:\", error);\n const err = new Error(\"WebSocket error\");\n onError?.(err);\n reject(err);\n };\n\n ws.onclose = () => {\n isRecordingFlag = false;\n ws = null;\n onClose?.();\n };\n });\n }\n\n async function startRecording(): Promise<void> {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n\n if (isRecordingFlag) {\n throw new Error(\"Already recording\");\n }\n\n recorder = await createRealtimeRecorder();\n isRecordingFlag = true;\n\n await recorder.start((audio) => {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n try {\n sendAudioBufferAppend(audio);\n } catch (err) {\n console.error(\"[ASR] Failed to send audio:\", err);\n }\n });\n }\n\n async function stopRecording(): Promise<void> {\n if (!isRecordingFlag) {\n return;\n }\n\n try {\n await recorder?.stop();\n } catch (err) {\n console.error(\"[ASR] Error stopping recorder:\", err);\n }\n recorder = null;\n isRecordingFlag = false;\n\n // In non-VAD mode, send commit to trigger recognition\n if (!enableVAD && ws?.readyState === WebSocket.OPEN) {\n try {\n sendAudioBufferCommit();\n } catch (err) {\n console.error(\"[ASR] Failed to send commit:\", err);\n }\n }\n }\n\n async function close(): Promise<void> {\n if (isClosing) {\n return;\n }\n\n isClosing = true;\n\n // Stop recording first\n await stopRecording();\n\n // Send session.finish if connected\n if (ws?.readyState === WebSocket.OPEN) {\n try {\n sendSessionFinish();\n // Wait a bit for session.finished response\n await new Promise((resolve) => setTimeout(resolve, 1000));\n } catch (err) {\n console.error(\"[ASR] Failed to send session.finish:\", err);\n }\n }\n\n // Close WebSocket\n if (ws && ws?.readyState !== WebSocket.CLOSING && ws?.readyState !== WebSocket.CLOSED) {\n ws?.close();\n }\n ws = null;\n isClosing = false;\n }\n\n function isRecording(): boolean {\n return isRecordingFlag;\n }\n\n function isConnected(): boolean {\n return ws !== null && ws.readyState === WebSocket.OPEN;\n }\n\n return {\n connect,\n startRecording,\n stopRecording,\n close,\n isRecording,\n isConnected,\n };\n}\n\nexport default (authConfig: Pick<ASRClientConfig, \"getAccessToken\">) => (config: ASRClientConfig) =>\n createASRClient({ ...authConfig, ...config });\n\nexport type {\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n *\n * HTTP-based speech recognition suitable for press-to-talk scenarios where you hold to speak\n * and release to recognize. Good for voice messages, voice search, etc.\n *\n * @example\n * ```typescript\n * const client = createASRHttpClient({\n * onRecordingStart() {\n * console.log(\"Recording started\");\n * },\n * onRecordingStop() {\n * console.log(\"Recording stopped\");\n * },\n * onResult(text) {\n * console.log(\"Recognized:\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * // Hold to speak, release to recognize\n * await client.startRecording();\n * // ... stop ...\n * const result = await client.stopRecording();\n * ```\n */\n\nimport { createHttpClient, type HttpClient } from \"@amaster.ai/http-client\";\n\nconst ASR_HTTP_PATH =\n \"/api/proxy/builtin/platform/qwen-asr/compatible-mode/v1/chat/completions\";\n\n// 录音内核\nconst RECORDER_WORKLET = `\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0].slice(0));\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n`;\n\ntype Status = \"idle\" | \"recording\" | \"recognizing\";\n\ninterface Recorder {\n /** Start recording */\n start(): Promise<void>;\n /**\n * Stop recording and get base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @returns Base64-encoded WAV audio data\n */\n stop(): Promise<void>;\n}\n\ninterface RecorderOptions {\n /** Called when recording starts */\n onStart?: () => void;\n /**\n * Called when recording stops, with base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @param base64 - Base64-encoded WAV audio data\n * @returns void\n */\n onStop?: (base64: string) => void;\n onError?: (error: Error) => void;\n}\n\n// 创建录音器\nasync function createWebRecorder(props?: RecorderOptions): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n const cleanup = () => {\n try {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n } catch (e) {}\n };\n\n return {\n async start() {\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n ctx = new AudioContext();\n\n const blob = new Blob([RECORDER_WORKLET], {\n type: \"application/javascript\",\n });\n const url = URL.createObjectURL(blob);\n await ctx.audioWorklet.addModule(url);\n URL.revokeObjectURL(url);\n\n source = ctx.createMediaStreamSource(stream);\n node = new AudioWorkletNode(ctx, \"recorder-processor\");\n\n node.port.onmessage = (e) => {\n const input = e.data as Float32Array;\n const pcm = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n\n props?.onStart?.();\n } catch (error) {\n props?.onError?.(\n error instanceof Error ? error : new Error(String(error)),\n );\n cleanup();\n }\n },\n\n async stop() {\n cleanup();\n\n const total = chunks.reduce((s, c) => s + c.length, 0);\n const pcm = new Int16Array(total);\n let offset = 0;\n for (const c of chunks) {\n pcm.set(c, offset);\n offset += c.length;\n }\n\n const result = { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\n const base64 = await blobToBase64(\n pcmToWav(result.pcm, result.sampleRate),\n );\n props?.onStop?.(base64);\n },\n };\n}\n\n/**\n * Convert PCM to WAV Blob\n * @param pcm - PCM data\n * @param sampleRate - Sample rate\n * @returns WAV Blob\n */\nfunction pcmToWav(pcm: Int16Array, sampleRate: number): Blob {\n const buffer = new ArrayBuffer(44 + pcm.length * 2);\n const view = new DataView(buffer);\n\n const write = (o: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(o + i, s.charCodeAt(i));\n };\n\n write(0, \"RIFF\");\n view.setUint32(4, 36 + pcm.length * 2, true);\n write(8, \"WAVE\");\n write(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n write(36, \"data\");\n view.setUint32(40, pcm.length * 2, true);\n\n for (let i = 0; i < pcm.length; i++) {\n view.setInt16(44 + i * 2, pcm[i] || 0, true);\n }\n\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const result = reader.result as string;\n resolve(result.split(\",\")[1] || \"\");\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\nexport interface ASRHttpClientConfig {\n http?: HttpClient;\n /** Get access token */\n getAccessToken?(): string | null;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\n /** Create custom recorder */\n createRecorder?(options?: RecorderOptions): Promise<Recorder>;\n /** Called when recording starts */\n onRecordingStart?: () => void;\n /** Called when recording stops */\n onRecordingStop?: () => void;\n /** Called with recognition result */\n onResult?: (text: string) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n /** Called when status changes */\n onStatusChange?: (status: Status) => void;\n}\n\nexport interface ASRHttpClient {\n /** Start recording (press-to-talk) */\n startRecording(): Promise<void>;\n /** Stop recording and get result */\n stopRecording(): Promise<void>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<void>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(base64: string): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nclass AsrHttpClient {\n http: HttpClient;\n recorder: Recorder | null = null;\n path: string = \"\";\n config: ASRHttpClientConfig;\n recognizing = false;\n constructor(config: ASRHttpClientConfig, path: string) {\n this.http = config.http ?? createHttpClient();\n this.config = config;\n this.path = path;\n }\n async startRecording() {\n if (this.recorder) {\n return;\n }\n const options = {\n onStart: () => {\n this.config.onRecordingStart?.();\n this.config.onStatusChange?.(\"recording\");\n },\n onStop: async (base64: string) => {\n this.config.onStatusChange?.(\"recognizing\");\n const text = await this.recognizeFile(base64);\n this.config.onResult?.(text);\n this.config.onRecordingStop?.();\n this.config.onStatusChange?.(\"idle\");\n this.recorder = null;\n },\n onError: (err: any) => {\n this.config.onError?.(err);\n this.config.onStatusChange?.(\"idle\");\n this.recorder = null;\n },\n };\n this.recorder = await (this.config.createRecorder?.(options) ??\n createWebRecorder(options));\n await this.recorder.start();\n }\n\n async stopRecording() {\n if (this.recorder) {\n await this.recorder.stop();\n this.recorder = null;\n } else {\n this.config.onResult?.(\"\");\n this.config.onRecordingStop?.();\n this.config.onStatusChange?.(\"idle\");\n }\n }\n\n async recognizeFile(base64: string): Promise<string> {\n if (this.recognizing) {\n return \"\";\n }\n this.recognizing = true;\n try {\n const response = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [\n {\n type: \"input_audio\",\n input_audio: { data: `data:audio/wav;base64,${base64}` },\n },\n ],\n },\n ],\n }),\n });\n\n return (response as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n console.error(\"ASR recognition error:\", e);\n return \"\";\n } finally {\n this.recognizing = false;\n }\n }\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n await this.stopRecording();\n }\n\n async recognizeUrl(url: string) {\n try {\n const res = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [{ type: \"input_audio\", input_audio: { url } }],\n },\n ],\n }),\n });\n return (res as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n console.error(\"ASR recognition error:\", e);\n return \"\";\n }\n }\n}\n\nexport function createASRHttpClient(\n config: ASRHttpClientConfig,\n): ASRHttpClient {\n let path = ASR_HTTP_PATH;\n\n if (config.getAccessToken) {\n const token = config.getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n path = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n return new AsrHttpClient(config, path);\n}\n\nexport default (authConfig: Pick<ASRHttpClientConfig, \"getAccessToken\">) =>\n (config: ASRHttpClientConfig) =>\n createASRHttpClient({ ...authConfig, ...config });\n"],"mappings":"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","names":["err"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -239,6 +239,7 @@ declare const _default$1: (authConfig: Pick<ASRClientConfig, "getAccessToken">)
|
|
|
239
239
|
* ```
|
|
240
240
|
*/
|
|
241
241
|
|
|
242
|
+
type Status = "idle" | "recording" | "recognizing";
|
|
242
243
|
interface Recorder {
|
|
243
244
|
/** Start recording */
|
|
244
245
|
start(): Promise<void>;
|
|
@@ -279,6 +280,8 @@ interface ASRHttpClientConfig {
|
|
|
279
280
|
onResult?: (text: string) => void;
|
|
280
281
|
/** Called on error */
|
|
281
282
|
onError?: (error: Error) => void;
|
|
283
|
+
/** Called when status changes */
|
|
284
|
+
onStatusChange?: (status: Status) => void;
|
|
282
285
|
}
|
|
283
286
|
interface ASRHttpClient {
|
|
284
287
|
/** Start recording (press-to-talk) */
|
package/dist/index.d.ts
CHANGED
|
@@ -239,6 +239,7 @@ declare const _default$1: (authConfig: Pick<ASRClientConfig, "getAccessToken">)
|
|
|
239
239
|
* ```
|
|
240
240
|
*/
|
|
241
241
|
|
|
242
|
+
type Status = "idle" | "recording" | "recognizing";
|
|
242
243
|
interface Recorder {
|
|
243
244
|
/** Start recording */
|
|
244
245
|
start(): Promise<void>;
|
|
@@ -279,6 +280,8 @@ interface ASRHttpClientConfig {
|
|
|
279
280
|
onResult?: (text: string) => void;
|
|
280
281
|
/** Called on error */
|
|
281
282
|
onError?: (error: Error) => void;
|
|
283
|
+
/** Called when status changes */
|
|
284
|
+
onStatusChange?: (status: Status) => void;
|
|
282
285
|
}
|
|
283
286
|
interface ASRHttpClient {
|
|
284
287
|
/** Start recording (press-to-talk) */
|
package/dist/index.js
CHANGED
|
@@ -342,7 +342,9 @@ async function createWebRecorder(props) {
|
|
|
342
342
|
}
|
|
343
343
|
});
|
|
344
344
|
ctx = new AudioContext();
|
|
345
|
-
const blob = new Blob([RECORDER_WORKLET], {
|
|
345
|
+
const blob = new Blob([RECORDER_WORKLET], {
|
|
346
|
+
type: "application/javascript"
|
|
347
|
+
});
|
|
346
348
|
const url = URL.createObjectURL(blob);
|
|
347
349
|
await ctx.audioWorklet.addModule(url);
|
|
348
350
|
URL.revokeObjectURL(url);
|
|
@@ -360,7 +362,9 @@ async function createWebRecorder(props) {
|
|
|
360
362
|
source.connect(node);
|
|
361
363
|
props?.onStart?.();
|
|
362
364
|
} catch (error) {
|
|
363
|
-
props?.onError?.(
|
|
365
|
+
props?.onError?.(
|
|
366
|
+
error instanceof Error ? error : new Error(String(error))
|
|
367
|
+
);
|
|
364
368
|
cleanup();
|
|
365
369
|
}
|
|
366
370
|
},
|
|
@@ -374,7 +378,9 @@ async function createWebRecorder(props) {
|
|
|
374
378
|
offset += c.length;
|
|
375
379
|
}
|
|
376
380
|
const result = { pcm, sampleRate: ctx?.sampleRate ?? 16e3 };
|
|
377
|
-
const base64 = await blobToBase64(
|
|
381
|
+
const base64 = await blobToBase64(
|
|
382
|
+
pcmToWav(result.pcm, result.sampleRate)
|
|
383
|
+
);
|
|
378
384
|
props?.onStop?.(base64);
|
|
379
385
|
}
|
|
380
386
|
};
|
|
@@ -418,30 +424,52 @@ var AsrHttpClient = class {
|
|
|
418
424
|
constructor(config, path) {
|
|
419
425
|
this.recorder = null;
|
|
420
426
|
this.path = "";
|
|
427
|
+
this.recognizing = false;
|
|
421
428
|
this.http = config.http ?? createHttpClient();
|
|
422
429
|
this.config = config;
|
|
423
430
|
this.path = path;
|
|
424
431
|
}
|
|
425
432
|
async startRecording() {
|
|
433
|
+
if (this.recorder) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
426
436
|
const options = {
|
|
427
|
-
onStart:
|
|
437
|
+
onStart: () => {
|
|
438
|
+
this.config.onRecordingStart?.();
|
|
439
|
+
this.config.onStatusChange?.("recording");
|
|
440
|
+
},
|
|
428
441
|
onStop: async (base64) => {
|
|
442
|
+
this.config.onStatusChange?.("recognizing");
|
|
429
443
|
const text = await this.recognizeFile(base64);
|
|
430
444
|
this.config.onResult?.(text);
|
|
431
445
|
this.config.onRecordingStop?.();
|
|
446
|
+
this.config.onStatusChange?.("idle");
|
|
447
|
+
this.recorder = null;
|
|
432
448
|
},
|
|
433
|
-
onError:
|
|
449
|
+
onError: (err) => {
|
|
450
|
+
this.config.onError?.(err);
|
|
451
|
+
this.config.onStatusChange?.("idle");
|
|
452
|
+
this.recorder = null;
|
|
453
|
+
}
|
|
434
454
|
};
|
|
435
|
-
debugger;
|
|
436
455
|
this.recorder = await (this.config.createRecorder?.(options) ?? createWebRecorder(options));
|
|
437
456
|
await this.recorder.start();
|
|
438
457
|
}
|
|
439
458
|
async stopRecording() {
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
459
|
+
if (this.recorder) {
|
|
460
|
+
await this.recorder.stop();
|
|
461
|
+
this.recorder = null;
|
|
462
|
+
} else {
|
|
463
|
+
this.config.onResult?.("");
|
|
464
|
+
this.config.onRecordingStop?.();
|
|
465
|
+
this.config.onStatusChange?.("idle");
|
|
466
|
+
}
|
|
443
467
|
}
|
|
444
468
|
async recognizeFile(base64) {
|
|
469
|
+
if (this.recognizing) {
|
|
470
|
+
return "";
|
|
471
|
+
}
|
|
472
|
+
this.recognizing = true;
|
|
445
473
|
try {
|
|
446
474
|
const response = await this.http.request({
|
|
447
475
|
url: this.path,
|
|
@@ -464,7 +492,10 @@ var AsrHttpClient = class {
|
|
|
464
492
|
});
|
|
465
493
|
return response?.data?.choices?.[0]?.message?.content || "";
|
|
466
494
|
} catch (e) {
|
|
495
|
+
console.error("ASR recognition error:", e);
|
|
467
496
|
return "";
|
|
497
|
+
} finally {
|
|
498
|
+
this.recognizing = false;
|
|
468
499
|
}
|
|
469
500
|
}
|
|
470
501
|
async recordAndRecognize(ms) {
|
|
@@ -490,6 +521,8 @@ var AsrHttpClient = class {
|
|
|
490
521
|
});
|
|
491
522
|
return res?.data?.choices?.[0]?.message?.content || "";
|
|
492
523
|
} catch (e) {
|
|
524
|
+
console.error("ASR recognition error:", e);
|
|
525
|
+
return "";
|
|
493
526
|
}
|
|
494
527
|
}
|
|
495
528
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/asr-client.ts","../src/http-asr-client.ts"],"sourcesContent":["/**\n * ASR Realtime WebSocket Client for Qwen-ASR Realtime API\n *\n * WebSocket-based real-time speech recognition for streaming transcription.\n * Follows the Qwen-ASR Realtime API protocol with proper event handling.\n *\n * @example\n * ```typescript\n * const client = createASRClient({\n * language: \"zh\",\n * enableVAD: true,\n * onReady() {\n * console.log(\"ASR connected\");\n * },\n * onTranscript(text, isFinal) {\n * console.log(isFinal ? \"[Final]\" : \"[Interim]\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * await client.connect();\n * await client.startRecording();\n * // ... stop ...\n * await client.stopRecording();\n * await client.close();\n * ```\n */\n\nconst ASR_PATH = \"/api/proxy/builtin/platform/qwen-asr-realtime/api-ws/v1/realtime\";\n\nexport type ASRLanguage =\n | \"zh\" // 中文(普通话、四川话、闽南语、吴语)\n | \"yue\" // 粤语\n | \"en\" // 英文\n | \"ja\" // 日语\n | \"de\" // 德语\n | \"ko\" // 韩语\n | \"ru\" // 俄语\n | \"fr\" // 法语\n | \"pt\" // 葡萄牙语\n | \"ar\" // 阿拉伯语\n | \"it\" // 意大利语\n | \"es\" // 西班牙语\n | \"hi\" // 印地语\n | \"id\" // 印尼语\n | \"th\" // 泰语\n | \"tr\" // 土耳其语\n | \"uk\" // 乌克兰语\n | \"vi\" // 越南语\n | \"cs\" // 捷克语\n | \"da\" // 丹麦语\n | \"fil\" // 菲律宾语\n | \"fi\" // 芬兰语\n | \"is\" // 冰岛语\n | \"ms\" // 马来语\n | \"no\" // 挪威语\n | \"pl\" // 波兰语\n | \"sv\"; // 瑞典语\n\n// Client event types (sent to server)\ntype ClientEventType =\n | \"session.update\"\n | \"input_audio_buffer.append\"\n | \"input_audio_buffer.commit\"\n | \"session.finish\";\n\n// Server event types (received from server)\ntype ServerEventType =\n | \"session.created\"\n | \"session.updated\"\n | \"input_audio_buffer.speech_started\"\n | \"input_audio_buffer.speech_stopped\"\n | \"input_audio_buffer.committed\"\n | \"conversation.item.input_audio_transcription.text\"\n | \"conversation.item.input_audio_transcription.completed\"\n | \"session.finished\"\n | \"error\";\n\n// Base event interface\ninterface BaseEvent {\n event_id: string;\n type: ClientEventType | ServerEventType;\n}\n\n// Client events\ninterface SessionUpdateEvent extends BaseEvent {\n type: \"session.update\";\n session: SessionConfig;\n}\n\ninterface InputAudioBufferAppendEvent extends BaseEvent {\n type: \"input_audio_buffer.append\";\n audio: string;\n}\n\ninterface InputAudioBufferCommitEvent extends BaseEvent {\n type: \"input_audio_buffer.commit\";\n}\n\ninterface SessionFinishEvent extends BaseEvent {\n type: \"session.finish\";\n}\n\ntype ClientEvent =\n | SessionUpdateEvent\n | InputAudioBufferAppendEvent\n | InputAudioBufferCommitEvent\n | SessionFinishEvent;\n\n// Server events\ninterface SessionCreatedEvent extends BaseEvent {\n type: \"session.created\";\n session: { id: string };\n}\n\ninterface SessionUpdatedEvent extends BaseEvent {\n type: \"session.updated\";\n session: SessionConfig;\n}\n\ninterface SpeechStartedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_started\";\n}\n\ninterface SpeechStoppedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_stopped\";\n}\n\ninterface InputAudioBufferCommittedEvent extends BaseEvent {\n type: \"input_audio_buffer.committed\";\n}\n\ninterface TranscriptionTextEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.text\";\n text?: string;\n stash?: string;\n transcript?: string;\n}\n\ninterface TranscriptionCompletedEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.completed\";\n text?: string;\n transcript?: string;\n}\n\ninterface SessionFinishedEvent extends BaseEvent {\n type: \"session.finished\";\n}\n\ninterface ErrorEvent extends BaseEvent {\n type: \"error\";\n error: {\n message: string;\n code?: string;\n };\n}\n\ntype ServerEvent =\n | SessionCreatedEvent\n | SessionUpdatedEvent\n | SpeechStartedEvent\n | SpeechStoppedEvent\n | InputAudioBufferCommittedEvent\n | TranscriptionTextEvent\n | TranscriptionCompletedEvent\n | SessionFinishedEvent\n | ErrorEvent;\n\n// Session configuration\ninterface TurnDetectionConfig {\n type: \"server_vad\";\n /** VAD检测阈值,推荐设为 0.0,默认值 0.2,范围 [-1, 1] */\n threshold?: number;\n /** VAD断句检测阈值(ms),推荐设为 400,默认值 800,范围 [200, 6000] */\n silence_duration_ms?: number;\n}\n\ninterface InputAudioTranscriptionConfig {\n language?: ASRLanguage;\n}\n\ninterface SessionConfig {\n input_audio_format?: \"pcm\" | \"opus\";\n sample_rate?: 16000 | 8000;\n input_audio_transcription?: InputAudioTranscriptionConfig;\n turn_detection?: TurnDetectionConfig | null;\n}\n\ninterface RealtimeRecorder {\n start(onAudio: (base64: string) => void): Promise<void>;\n stop(): Promise<void>;\n}\n\nasync function createRealtimeRecorder(): Promise<RealtimeRecorder> {\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n let source: MediaStreamAudioSourceNode | null = null;\n let processor: ScriptProcessorNode | null = null;\n\n return {\n async start(onAudio) {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },\n });\n\n log(\"✅ 麦克风已启动\", \"success\");\n log(\"💬 请对着麦克风说话,实时识别中...\", \"success\");\n\n ctx = new AudioContext({ sampleRate: 16000 });\n source = ctx.createMediaStreamSource(stream);\n processor = ctx.createScriptProcessor(4096, 1, 1);\n\n processor.onaudioprocess = (e) => {\n const inputData = e.inputBuffer.getChannelData(0);\n const pcm = new Int16Array(inputData.length);\n for (let i = 0; i < inputData.length; i++) {\n const s = Math.max(-1, Math.min(1, inputData[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n\n const bytes = new Uint8Array(pcm.buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] || 0);\n }\n\n onAudio(btoa(binary));\n };\n\n source.connect(processor);\n processor.connect(ctx.destination);\n },\n\n async stop() {\n stream?.getTracks().forEach((t) => t.stop());\n source?.disconnect();\n processor?.disconnect();\n if (ctx) {\n await ctx.close();\n }\n stream = null;\n ctx = null;\n source = null;\n processor = null;\n },\n };\n}\n\nconst log = (message: string, type = \"\") => {\n console.log(`[${type}]`, message);\n};\n\n// Generate unique event ID\nlet eventIdCounter = 0;\nfunction generateEventId(): string {\n return `event_${Date.now()}_${++eventIdCounter}`;\n}\n\nexport interface ASRClientConfig {\n /**\n * Audio format\n * @default \"pcm\"\n */\n audioFormat?: \"pcm\" | \"opus\";\n /**\n * Sample rate in Hz\n * @default 16000\n * @description 支持 16000 和 8000。设置为 8000 时,服务端会先升采样到16000Hz再进行识别,可能引入微小延迟。\n */\n sampleRate?: 16000 | 8000;\n /**\n * Audio source language\n * @default \"zh\"\n * @description 支持多种语言,包括 zh(中文)、yue(粤语)、en(英文)、ja(日语)等\n */\n language?: ASRLanguage;\n /**\n * Enable VAD (Voice Activity Detection) mode\n * @default true\n * @description true = VAD模式(服务端自动检测语音开始/结束),false = Manual模式(客户端手动控制)\n */\n enableVAD?: boolean;\n /**\n * VAD detection threshold\n * @default 0.2\n * @description 推荐设为 0.0。取值范围 [-1, 1]。较低的阈值会提高 VAD 的灵敏度。\n */\n vadThreshold?: number;\n /**\n * VAD silence duration threshold in milliseconds\n * @default 800\n * @description 推荐设为 400。取值范围 [200, 6000]。静音持续时长超过该阈值将被认为是语句结束。\n */\n vadSilenceDurationMs?: number;\n /**\n * Get access token for WebSocket authentication\n */\n getAccessToken?: () => string | null;\n /**\n * Called when connection is ready (session.created received and session.update sent)\n */\n onReady?: () => void;\n /**\n * Called when speech is detected (VAD mode only)\n */\n onSpeechStart?: () => void;\n /**\n * Called when speech stops (VAD mode only)\n */\n onSpeechEnd?: () => void;\n /**\n * Called on transcript result\n * @param text - Transcribed text\n * @param isFinal - Whether this is the final result\n */\n onTranscript?: (text: string, isFinal: boolean) => void;\n /**\n * Called when audio buffer is committed (non-VAD mode only)\n */\n onAudioBufferCommitted?: () => void;\n /**\n * Called when session is finished\n */\n onSessionFinished?: () => void;\n /**\n * Called on error\n */\n onError?: (error: Error) => void;\n /**\n * Called on close\n */\n onClose?: () => void;\n}\n\nexport interface ASRClient {\n /** Connect to ASR service and establish session */\n connect(): Promise<void>;\n /** Start recording from microphone */\n startRecording(): Promise<void>;\n /**\n * Stop recording\n * @description In non-VAD mode, this triggers recognition by sending input_audio_buffer.commit\n */\n stopRecording(): Promise<void>;\n /**\n * Close connection gracefully\n * @description Sends session.finish and waits for session.finished before closing\n */\n close(): Promise<void>;\n /**\n * Check if currently recording\n */\n isRecording(): boolean;\n /**\n * Check if connected to server\n */\n isConnected(): boolean;\n}\n\nfunction createASRClient(config: ASRClientConfig): ASRClient {\n const {\n onReady,\n onSpeechStart,\n onSpeechEnd,\n onTranscript,\n onAudioBufferCommitted,\n onSessionFinished,\n onError,\n onClose,\n getAccessToken,\n audioFormat = \"pcm\",\n sampleRate = 16000,\n language = \"zh\",\n enableVAD = true,\n vadThreshold = 0.2,\n vadSilenceDurationMs = 400,\n } = config;\n\n let ws: WebSocket | null = null;\n let recorder: RealtimeRecorder | null = null;\n let isRecordingFlag = false;\n let isClosing = false;\n\n const path = ASR_PATH;\n\n /**\n * Send event to server\n */\n function sendEvent(event: ClientEvent): void {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n ws.send(JSON.stringify(event));\n }\n\n /**\n * Build session configuration\n */\n function buildSessionConfig(): SessionConfig {\n const sessionConfig: SessionConfig = {\n input_audio_format: audioFormat,\n sample_rate: sampleRate,\n input_audio_transcription: {\n language,\n },\n };\n\n if (enableVAD) {\n sessionConfig.turn_detection = {\n type: \"server_vad\",\n threshold: vadThreshold,\n silence_duration_ms: vadSilenceDurationMs,\n };\n } else {\n // Non-VAD mode: set turn_detection to null\n sessionConfig.turn_detection = null;\n }\n\n return sessionConfig;\n }\n\n /**\n * Send session.update event\n */\n function sendSessionUpdate(): void {\n const event: SessionUpdateEvent = {\n event_id: generateEventId(),\n type: \"session.update\",\n session: buildSessionConfig(),\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.append event\n */\n function sendAudioBufferAppend(audio: string): void {\n const event: InputAudioBufferAppendEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.append\",\n audio,\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.commit event (for non-VAD mode)\n */\n function sendAudioBufferCommit(): void {\n const event: InputAudioBufferCommitEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.commit\",\n };\n sendEvent(event);\n }\n\n /**\n * Send session.finish event\n */\n function sendSessionFinish(): void {\n const event: SessionFinishEvent = {\n event_id: generateEventId(),\n type: \"session.finish\",\n };\n sendEvent(event);\n }\n\n /**\n * Handle server events\n */\n function handleServerEvent(data: ServerEvent): void {\n switch (data.type) {\n case \"session.created\":\n // Send session.update immediately after session.created\n try {\n sendSessionUpdate();\n } catch (err) {\n onError?.(\n new Error(\n \"Failed to send session.update: \" + (err instanceof Error ? err.message : String(err))\n )\n );\n }\n break;\n\n case \"session.updated\":\n // Server confirmed session configuration\n onReady?.();\n break;\n\n case \"input_audio_buffer.speech_started\":\n onSpeechStart?.();\n break;\n\n case \"input_audio_buffer.speech_stopped\":\n onSpeechEnd?.();\n break;\n\n case \"input_audio_buffer.committed\":\n onAudioBufferCommitted?.();\n break;\n\n case \"conversation.item.input_audio_transcription.text\":\n onTranscript?.(data.text || data.stash || data.transcript || \"\", false);\n break;\n\n case \"conversation.item.input_audio_transcription.completed\":\n onTranscript?.(data.text || data.transcript || \"\", true);\n break;\n\n case \"session.finished\":\n onSessionFinished?.();\n // Close WebSocket after receiving session.finished\n close();\n break;\n\n case \"error\":\n const err = new Error(data.error?.message || \"ASR error\");\n onError?.(err);\n break;\n\n default:\n // Unknown event type\n console.warn(\"[ASR] Unknown server event:\", (data as ServerEvent).type);\n }\n }\n\n async function connect(): Promise<void> {\n // Build WebSocket URL with optional token parameter\n let wsUrl = path;\n if (getAccessToken) {\n const token = getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n wsUrl = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n // Support both ws:// and wss:// protocols\n if (typeof window !== \"undefined\" && window.location) {\n const protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n if (!wsUrl.startsWith(\"ws://\") && !wsUrl.startsWith(\"wss://\")) {\n wsUrl = `${protocol}//${window.location.host}${wsUrl}`;\n }\n }\n\n ws = new WebSocket(wsUrl);\n\n return new Promise((resolve, reject) => {\n if (!ws) {\n reject(new Error(\"Failed to create WebSocket\"));\n return;\n }\n\n ws.onopen = () => {\n log(\"WebSocket connected\", \"success\");\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as ServerEvent;\n handleServerEvent(data);\n\n // Resolve on session.updated (ready state)\n if (data.type === \"session.updated\") {\n resolve();\n }\n } catch (err) {\n const error = new Error(\n \"Failed to parse server message: \" + (err instanceof Error ? err.message : String(err))\n );\n onError?.(error);\n reject(error);\n }\n };\n\n ws.onerror = (error) => {\n console.error(\"WebSocket error:\", error);\n const err = new Error(\"WebSocket error\");\n onError?.(err);\n reject(err);\n };\n\n ws.onclose = () => {\n isRecordingFlag = false;\n ws = null;\n onClose?.();\n };\n });\n }\n\n async function startRecording(): Promise<void> {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n\n if (isRecordingFlag) {\n throw new Error(\"Already recording\");\n }\n\n recorder = await createRealtimeRecorder();\n isRecordingFlag = true;\n\n await recorder.start((audio) => {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n try {\n sendAudioBufferAppend(audio);\n } catch (err) {\n console.error(\"[ASR] Failed to send audio:\", err);\n }\n });\n }\n\n async function stopRecording(): Promise<void> {\n if (!isRecordingFlag) {\n return;\n }\n\n try {\n await recorder?.stop();\n } catch (err) {\n console.error(\"[ASR] Error stopping recorder:\", err);\n }\n recorder = null;\n isRecordingFlag = false;\n\n // In non-VAD mode, send commit to trigger recognition\n if (!enableVAD && ws?.readyState === WebSocket.OPEN) {\n try {\n sendAudioBufferCommit();\n } catch (err) {\n console.error(\"[ASR] Failed to send commit:\", err);\n }\n }\n }\n\n async function close(): Promise<void> {\n if (isClosing) {\n return;\n }\n\n isClosing = true;\n\n // Stop recording first\n await stopRecording();\n\n // Send session.finish if connected\n if (ws?.readyState === WebSocket.OPEN) {\n try {\n sendSessionFinish();\n // Wait a bit for session.finished response\n await new Promise((resolve) => setTimeout(resolve, 1000));\n } catch (err) {\n console.error(\"[ASR] Failed to send session.finish:\", err);\n }\n }\n\n // Close WebSocket\n if (ws && ws?.readyState !== WebSocket.CLOSING && ws?.readyState !== WebSocket.CLOSED) {\n ws?.close();\n }\n ws = null;\n isClosing = false;\n }\n\n function isRecording(): boolean {\n return isRecordingFlag;\n }\n\n function isConnected(): boolean {\n return ws !== null && ws.readyState === WebSocket.OPEN;\n }\n\n return {\n connect,\n startRecording,\n stopRecording,\n close,\n isRecording,\n isConnected,\n };\n}\n\nexport default (authConfig: Pick<ASRClientConfig, \"getAccessToken\">) => (config: ASRClientConfig) =>\n createASRClient({ ...authConfig, ...config });\n\nexport type {\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n *\n * HTTP-based speech recognition suitable for press-to-talk scenarios where you hold to speak\n * and release to recognize. Good for voice messages, voice search, etc.\n *\n * @example\n * ```typescript\n * const client = createASRHttpClient({\n * onRecordingStart() {\n * console.log(\"Recording started\");\n * },\n * onRecordingStop() {\n * console.log(\"Recording stopped\");\n * },\n * onResult(text) {\n * console.log(\"Recognized:\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * // Hold to speak, release to recognize\n * await client.startRecording();\n * // ... stop ...\n * const result = await client.stopRecording();\n * ```\n */\n\nimport { createHttpClient, type HttpClient } from \"@amaster.ai/http-client\";\n\nconst ASR_HTTP_PATH = \"/api/proxy/builtin/platform/qwen-asr/compatible-mode/v1/chat/completions\";\n\n// 录音内核\nconst RECORDER_WORKLET = `\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0].slice(0));\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n`;\n\ninterface Recorder {\n /** Start recording */\n start(): Promise<void>;\n /**\n * Stop recording and get base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @returns Base64-encoded WAV audio data\n */\n stop(): Promise<void>;\n}\n\ninterface RecorderOptions {\n /** Called when recording starts */\n onStart?: () => void;\n /**\n * Called when recording stops, with base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @param base64 - Base64-encoded WAV audio data\n * @returns void\n */\n onStop?: (base64: string) => void;\n onError?: (error: Error) => void;\n}\n\n// 创建录音器\nasync function createWebRecorder(props?: RecorderOptions): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n const cleanup = () => {\n try {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n } catch (e) {}\n };\n\n return {\n async start() {\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n ctx = new AudioContext();\n\n const blob = new Blob([RECORDER_WORKLET], { type: \"application/javascript\" });\n const url = URL.createObjectURL(blob);\n await ctx.audioWorklet.addModule(url);\n URL.revokeObjectURL(url);\n\n source = ctx.createMediaStreamSource(stream);\n node = new AudioWorkletNode(ctx, \"recorder-processor\");\n\n node.port.onmessage = (e) => {\n const input = e.data as Float32Array;\n const pcm = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n\n props?.onStart?.();\n } catch (error) {\n props?.onError?.(error instanceof Error ? error : new Error(String(error)));\n cleanup();\n }\n },\n\n async stop() {\n cleanup();\n\n const total = chunks.reduce((s, c) => s + c.length, 0);\n const pcm = new Int16Array(total);\n let offset = 0;\n for (const c of chunks) {\n pcm.set(c, offset);\n offset += c.length;\n }\n\n const result = { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\n const base64 = await blobToBase64(pcmToWav(result.pcm, result.sampleRate));\n props?.onStop?.(base64);\n },\n };\n}\n\n/**\n * Convert PCM to WAV Blob\n * @param pcm - PCM data\n * @param sampleRate - Sample rate\n * @returns WAV Blob\n */\nfunction pcmToWav(pcm: Int16Array, sampleRate: number): Blob {\n const buffer = new ArrayBuffer(44 + pcm.length * 2);\n const view = new DataView(buffer);\n\n const write = (o: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(o + i, s.charCodeAt(i));\n };\n\n write(0, \"RIFF\");\n view.setUint32(4, 36 + pcm.length * 2, true);\n write(8, \"WAVE\");\n write(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n write(36, \"data\");\n view.setUint32(40, pcm.length * 2, true);\n\n for (let i = 0; i < pcm.length; i++) {\n view.setInt16(44 + i * 2, pcm[i] || 0, true);\n }\n\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const result = reader.result as string;\n resolve(result.split(\",\")[1] || \"\");\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\nexport interface ASRHttpClientConfig {\n http?: HttpClient;\n /** Get access token */\n getAccessToken?(): string | null;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\n /** Create custom recorder */\n createRecorder?(options?: RecorderOptions): Promise<Recorder>;\n /** Called when recording starts */\n onRecordingStart?: () => void;\n /** Called when recording stops */\n onRecordingStop?: () => void;\n /** Called with recognition result */\n onResult?: (text: string) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\nexport interface ASRHttpClient {\n /** Start recording (press-to-talk) */\n startRecording(): Promise<void>;\n /** Stop recording and get result */\n stopRecording(): Promise<void>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<void>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(base64: string): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nclass AsrHttpClient {\n http: HttpClient;\n recorder: Recorder | null = null;\n path: string = \"\";\n config: ASRHttpClientConfig;\n constructor(config: ASRHttpClientConfig, path: string) {\n this.http = config.http ?? createHttpClient();\n this.config = config;\n this.path = path;\n }\n async startRecording() {\n const options = {\n onStart: this.config.onRecordingStart,\n onStop: async (base64: string) => {\n const text = await this.recognizeFile(base64);\n this.config.onResult?.(text);\n this.config.onRecordingStop?.();\n },\n onError: this.config.onError,\n };\n debugger\n this.recorder = await (this.config.createRecorder?.(options) ?? createWebRecorder(options));\n await this.recorder.start();\n }\n\n async stopRecording() {\n if (!this.recorder) throw new Error(\"Not recording\");\n await this.recorder.stop();\n this.recorder = null;\n }\n\n async recognizeFile(base64: string): Promise<string> {\n try {\n const response = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [\n {\n type: \"input_audio\",\n input_audio: { data: `data:audio/wav;base64,${base64}` },\n },\n ],\n },\n ],\n }),\n });\n\n return (response as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n return \"\";\n }\n }\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n await this.stopRecording();\n }\n\n async recognizeUrl(url: string) {\n try {\n const res = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [{ type: \"input_audio\", input_audio: { url } }],\n },\n ],\n }),\n });\n return (res as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {}\n }\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let path = ASR_HTTP_PATH;\n\n if (config.getAccessToken) {\n const token = config.getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n path = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n return new AsrHttpClient(config, path);\n}\n\nexport default (authConfig: Pick<ASRHttpClientConfig, \"getAccessToken\">) =>\n (config: ASRHttpClientConfig) =>\n createASRHttpClient({ ...authConfig, ...config });\n"],"mappings":"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","names":["err"]}
|
|
1
|
+
{"version":3,"sources":["../src/asr-client.ts","../src/http-asr-client.ts"],"sourcesContent":["/**\n * ASR Realtime WebSocket Client for Qwen-ASR Realtime API\n *\n * WebSocket-based real-time speech recognition for streaming transcription.\n * Follows the Qwen-ASR Realtime API protocol with proper event handling.\n *\n * @example\n * ```typescript\n * const client = createASRClient({\n * language: \"zh\",\n * enableVAD: true,\n * onReady() {\n * console.log(\"ASR connected\");\n * },\n * onTranscript(text, isFinal) {\n * console.log(isFinal ? \"[Final]\" : \"[Interim]\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * await client.connect();\n * await client.startRecording();\n * // ... stop ...\n * await client.stopRecording();\n * await client.close();\n * ```\n */\n\nconst ASR_PATH = \"/api/proxy/builtin/platform/qwen-asr-realtime/api-ws/v1/realtime\";\n\nexport type ASRLanguage =\n | \"zh\" // 中文(普通话、四川话、闽南语、吴语)\n | \"yue\" // 粤语\n | \"en\" // 英文\n | \"ja\" // 日语\n | \"de\" // 德语\n | \"ko\" // 韩语\n | \"ru\" // 俄语\n | \"fr\" // 法语\n | \"pt\" // 葡萄牙语\n | \"ar\" // 阿拉伯语\n | \"it\" // 意大利语\n | \"es\" // 西班牙语\n | \"hi\" // 印地语\n | \"id\" // 印尼语\n | \"th\" // 泰语\n | \"tr\" // 土耳其语\n | \"uk\" // 乌克兰语\n | \"vi\" // 越南语\n | \"cs\" // 捷克语\n | \"da\" // 丹麦语\n | \"fil\" // 菲律宾语\n | \"fi\" // 芬兰语\n | \"is\" // 冰岛语\n | \"ms\" // 马来语\n | \"no\" // 挪威语\n | \"pl\" // 波兰语\n | \"sv\"; // 瑞典语\n\n// Client event types (sent to server)\ntype ClientEventType =\n | \"session.update\"\n | \"input_audio_buffer.append\"\n | \"input_audio_buffer.commit\"\n | \"session.finish\";\n\n// Server event types (received from server)\ntype ServerEventType =\n | \"session.created\"\n | \"session.updated\"\n | \"input_audio_buffer.speech_started\"\n | \"input_audio_buffer.speech_stopped\"\n | \"input_audio_buffer.committed\"\n | \"conversation.item.input_audio_transcription.text\"\n | \"conversation.item.input_audio_transcription.completed\"\n | \"session.finished\"\n | \"error\";\n\n// Base event interface\ninterface BaseEvent {\n event_id: string;\n type: ClientEventType | ServerEventType;\n}\n\n// Client events\ninterface SessionUpdateEvent extends BaseEvent {\n type: \"session.update\";\n session: SessionConfig;\n}\n\ninterface InputAudioBufferAppendEvent extends BaseEvent {\n type: \"input_audio_buffer.append\";\n audio: string;\n}\n\ninterface InputAudioBufferCommitEvent extends BaseEvent {\n type: \"input_audio_buffer.commit\";\n}\n\ninterface SessionFinishEvent extends BaseEvent {\n type: \"session.finish\";\n}\n\ntype ClientEvent =\n | SessionUpdateEvent\n | InputAudioBufferAppendEvent\n | InputAudioBufferCommitEvent\n | SessionFinishEvent;\n\n// Server events\ninterface SessionCreatedEvent extends BaseEvent {\n type: \"session.created\";\n session: { id: string };\n}\n\ninterface SessionUpdatedEvent extends BaseEvent {\n type: \"session.updated\";\n session: SessionConfig;\n}\n\ninterface SpeechStartedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_started\";\n}\n\ninterface SpeechStoppedEvent extends BaseEvent {\n type: \"input_audio_buffer.speech_stopped\";\n}\n\ninterface InputAudioBufferCommittedEvent extends BaseEvent {\n type: \"input_audio_buffer.committed\";\n}\n\ninterface TranscriptionTextEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.text\";\n text?: string;\n stash?: string;\n transcript?: string;\n}\n\ninterface TranscriptionCompletedEvent extends BaseEvent {\n type: \"conversation.item.input_audio_transcription.completed\";\n text?: string;\n transcript?: string;\n}\n\ninterface SessionFinishedEvent extends BaseEvent {\n type: \"session.finished\";\n}\n\ninterface ErrorEvent extends BaseEvent {\n type: \"error\";\n error: {\n message: string;\n code?: string;\n };\n}\n\ntype ServerEvent =\n | SessionCreatedEvent\n | SessionUpdatedEvent\n | SpeechStartedEvent\n | SpeechStoppedEvent\n | InputAudioBufferCommittedEvent\n | TranscriptionTextEvent\n | TranscriptionCompletedEvent\n | SessionFinishedEvent\n | ErrorEvent;\n\n// Session configuration\ninterface TurnDetectionConfig {\n type: \"server_vad\";\n /** VAD检测阈值,推荐设为 0.0,默认值 0.2,范围 [-1, 1] */\n threshold?: number;\n /** VAD断句检测阈值(ms),推荐设为 400,默认值 800,范围 [200, 6000] */\n silence_duration_ms?: number;\n}\n\ninterface InputAudioTranscriptionConfig {\n language?: ASRLanguage;\n}\n\ninterface SessionConfig {\n input_audio_format?: \"pcm\" | \"opus\";\n sample_rate?: 16000 | 8000;\n input_audio_transcription?: InputAudioTranscriptionConfig;\n turn_detection?: TurnDetectionConfig | null;\n}\n\ninterface RealtimeRecorder {\n start(onAudio: (base64: string) => void): Promise<void>;\n stop(): Promise<void>;\n}\n\nasync function createRealtimeRecorder(): Promise<RealtimeRecorder> {\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n let source: MediaStreamAudioSourceNode | null = null;\n let processor: ScriptProcessorNode | null = null;\n\n return {\n async start(onAudio) {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true },\n });\n\n log(\"✅ 麦克风已启动\", \"success\");\n log(\"💬 请对着麦克风说话,实时识别中...\", \"success\");\n\n ctx = new AudioContext({ sampleRate: 16000 });\n source = ctx.createMediaStreamSource(stream);\n processor = ctx.createScriptProcessor(4096, 1, 1);\n\n processor.onaudioprocess = (e) => {\n const inputData = e.inputBuffer.getChannelData(0);\n const pcm = new Int16Array(inputData.length);\n for (let i = 0; i < inputData.length; i++) {\n const s = Math.max(-1, Math.min(1, inputData[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n\n const bytes = new Uint8Array(pcm.buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] || 0);\n }\n\n onAudio(btoa(binary));\n };\n\n source.connect(processor);\n processor.connect(ctx.destination);\n },\n\n async stop() {\n stream?.getTracks().forEach((t) => t.stop());\n source?.disconnect();\n processor?.disconnect();\n if (ctx) {\n await ctx.close();\n }\n stream = null;\n ctx = null;\n source = null;\n processor = null;\n },\n };\n}\n\nconst log = (message: string, type = \"\") => {\n console.log(`[${type}]`, message);\n};\n\n// Generate unique event ID\nlet eventIdCounter = 0;\nfunction generateEventId(): string {\n return `event_${Date.now()}_${++eventIdCounter}`;\n}\n\nexport interface ASRClientConfig {\n /**\n * Audio format\n * @default \"pcm\"\n */\n audioFormat?: \"pcm\" | \"opus\";\n /**\n * Sample rate in Hz\n * @default 16000\n * @description 支持 16000 和 8000。设置为 8000 时,服务端会先升采样到16000Hz再进行识别,可能引入微小延迟。\n */\n sampleRate?: 16000 | 8000;\n /**\n * Audio source language\n * @default \"zh\"\n * @description 支持多种语言,包括 zh(中文)、yue(粤语)、en(英文)、ja(日语)等\n */\n language?: ASRLanguage;\n /**\n * Enable VAD (Voice Activity Detection) mode\n * @default true\n * @description true = VAD模式(服务端自动检测语音开始/结束),false = Manual模式(客户端手动控制)\n */\n enableVAD?: boolean;\n /**\n * VAD detection threshold\n * @default 0.2\n * @description 推荐设为 0.0。取值范围 [-1, 1]。较低的阈值会提高 VAD 的灵敏度。\n */\n vadThreshold?: number;\n /**\n * VAD silence duration threshold in milliseconds\n * @default 800\n * @description 推荐设为 400。取值范围 [200, 6000]。静音持续时长超过该阈值将被认为是语句结束。\n */\n vadSilenceDurationMs?: number;\n /**\n * Get access token for WebSocket authentication\n */\n getAccessToken?: () => string | null;\n /**\n * Called when connection is ready (session.created received and session.update sent)\n */\n onReady?: () => void;\n /**\n * Called when speech is detected (VAD mode only)\n */\n onSpeechStart?: () => void;\n /**\n * Called when speech stops (VAD mode only)\n */\n onSpeechEnd?: () => void;\n /**\n * Called on transcript result\n * @param text - Transcribed text\n * @param isFinal - Whether this is the final result\n */\n onTranscript?: (text: string, isFinal: boolean) => void;\n /**\n * Called when audio buffer is committed (non-VAD mode only)\n */\n onAudioBufferCommitted?: () => void;\n /**\n * Called when session is finished\n */\n onSessionFinished?: () => void;\n /**\n * Called on error\n */\n onError?: (error: Error) => void;\n /**\n * Called on close\n */\n onClose?: () => void;\n}\n\nexport interface ASRClient {\n /** Connect to ASR service and establish session */\n connect(): Promise<void>;\n /** Start recording from microphone */\n startRecording(): Promise<void>;\n /**\n * Stop recording\n * @description In non-VAD mode, this triggers recognition by sending input_audio_buffer.commit\n */\n stopRecording(): Promise<void>;\n /**\n * Close connection gracefully\n * @description Sends session.finish and waits for session.finished before closing\n */\n close(): Promise<void>;\n /**\n * Check if currently recording\n */\n isRecording(): boolean;\n /**\n * Check if connected to server\n */\n isConnected(): boolean;\n}\n\nfunction createASRClient(config: ASRClientConfig): ASRClient {\n const {\n onReady,\n onSpeechStart,\n onSpeechEnd,\n onTranscript,\n onAudioBufferCommitted,\n onSessionFinished,\n onError,\n onClose,\n getAccessToken,\n audioFormat = \"pcm\",\n sampleRate = 16000,\n language = \"zh\",\n enableVAD = true,\n vadThreshold = 0.2,\n vadSilenceDurationMs = 400,\n } = config;\n\n let ws: WebSocket | null = null;\n let recorder: RealtimeRecorder | null = null;\n let isRecordingFlag = false;\n let isClosing = false;\n\n const path = ASR_PATH;\n\n /**\n * Send event to server\n */\n function sendEvent(event: ClientEvent): void {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n ws.send(JSON.stringify(event));\n }\n\n /**\n * Build session configuration\n */\n function buildSessionConfig(): SessionConfig {\n const sessionConfig: SessionConfig = {\n input_audio_format: audioFormat,\n sample_rate: sampleRate,\n input_audio_transcription: {\n language,\n },\n };\n\n if (enableVAD) {\n sessionConfig.turn_detection = {\n type: \"server_vad\",\n threshold: vadThreshold,\n silence_duration_ms: vadSilenceDurationMs,\n };\n } else {\n // Non-VAD mode: set turn_detection to null\n sessionConfig.turn_detection = null;\n }\n\n return sessionConfig;\n }\n\n /**\n * Send session.update event\n */\n function sendSessionUpdate(): void {\n const event: SessionUpdateEvent = {\n event_id: generateEventId(),\n type: \"session.update\",\n session: buildSessionConfig(),\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.append event\n */\n function sendAudioBufferAppend(audio: string): void {\n const event: InputAudioBufferAppendEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.append\",\n audio,\n };\n sendEvent(event);\n }\n\n /**\n * Send input_audio_buffer.commit event (for non-VAD mode)\n */\n function sendAudioBufferCommit(): void {\n const event: InputAudioBufferCommitEvent = {\n event_id: generateEventId(),\n type: \"input_audio_buffer.commit\",\n };\n sendEvent(event);\n }\n\n /**\n * Send session.finish event\n */\n function sendSessionFinish(): void {\n const event: SessionFinishEvent = {\n event_id: generateEventId(),\n type: \"session.finish\",\n };\n sendEvent(event);\n }\n\n /**\n * Handle server events\n */\n function handleServerEvent(data: ServerEvent): void {\n switch (data.type) {\n case \"session.created\":\n // Send session.update immediately after session.created\n try {\n sendSessionUpdate();\n } catch (err) {\n onError?.(\n new Error(\n \"Failed to send session.update: \" + (err instanceof Error ? err.message : String(err))\n )\n );\n }\n break;\n\n case \"session.updated\":\n // Server confirmed session configuration\n onReady?.();\n break;\n\n case \"input_audio_buffer.speech_started\":\n onSpeechStart?.();\n break;\n\n case \"input_audio_buffer.speech_stopped\":\n onSpeechEnd?.();\n break;\n\n case \"input_audio_buffer.committed\":\n onAudioBufferCommitted?.();\n break;\n\n case \"conversation.item.input_audio_transcription.text\":\n onTranscript?.(data.text || data.stash || data.transcript || \"\", false);\n break;\n\n case \"conversation.item.input_audio_transcription.completed\":\n onTranscript?.(data.text || data.transcript || \"\", true);\n break;\n\n case \"session.finished\":\n onSessionFinished?.();\n // Close WebSocket after receiving session.finished\n close();\n break;\n\n case \"error\":\n const err = new Error(data.error?.message || \"ASR error\");\n onError?.(err);\n break;\n\n default:\n // Unknown event type\n console.warn(\"[ASR] Unknown server event:\", (data as ServerEvent).type);\n }\n }\n\n async function connect(): Promise<void> {\n // Build WebSocket URL with optional token parameter\n let wsUrl = path;\n if (getAccessToken) {\n const token = getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n wsUrl = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n // Support both ws:// and wss:// protocols\n if (typeof window !== \"undefined\" && window.location) {\n const protocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n if (!wsUrl.startsWith(\"ws://\") && !wsUrl.startsWith(\"wss://\")) {\n wsUrl = `${protocol}//${window.location.host}${wsUrl}`;\n }\n }\n\n ws = new WebSocket(wsUrl);\n\n return new Promise((resolve, reject) => {\n if (!ws) {\n reject(new Error(\"Failed to create WebSocket\"));\n return;\n }\n\n ws.onopen = () => {\n log(\"WebSocket connected\", \"success\");\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as ServerEvent;\n handleServerEvent(data);\n\n // Resolve on session.updated (ready state)\n if (data.type === \"session.updated\") {\n resolve();\n }\n } catch (err) {\n const error = new Error(\n \"Failed to parse server message: \" + (err instanceof Error ? err.message : String(err))\n );\n onError?.(error);\n reject(error);\n }\n };\n\n ws.onerror = (error) => {\n console.error(\"WebSocket error:\", error);\n const err = new Error(\"WebSocket error\");\n onError?.(err);\n reject(err);\n };\n\n ws.onclose = () => {\n isRecordingFlag = false;\n ws = null;\n onClose?.();\n };\n });\n }\n\n async function startRecording(): Promise<void> {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"WebSocket not connected\");\n }\n\n if (isRecordingFlag) {\n throw new Error(\"Already recording\");\n }\n\n recorder = await createRealtimeRecorder();\n isRecordingFlag = true;\n\n await recorder.start((audio) => {\n if (!ws || ws.readyState !== WebSocket.OPEN) return;\n try {\n sendAudioBufferAppend(audio);\n } catch (err) {\n console.error(\"[ASR] Failed to send audio:\", err);\n }\n });\n }\n\n async function stopRecording(): Promise<void> {\n if (!isRecordingFlag) {\n return;\n }\n\n try {\n await recorder?.stop();\n } catch (err) {\n console.error(\"[ASR] Error stopping recorder:\", err);\n }\n recorder = null;\n isRecordingFlag = false;\n\n // In non-VAD mode, send commit to trigger recognition\n if (!enableVAD && ws?.readyState === WebSocket.OPEN) {\n try {\n sendAudioBufferCommit();\n } catch (err) {\n console.error(\"[ASR] Failed to send commit:\", err);\n }\n }\n }\n\n async function close(): Promise<void> {\n if (isClosing) {\n return;\n }\n\n isClosing = true;\n\n // Stop recording first\n await stopRecording();\n\n // Send session.finish if connected\n if (ws?.readyState === WebSocket.OPEN) {\n try {\n sendSessionFinish();\n // Wait a bit for session.finished response\n await new Promise((resolve) => setTimeout(resolve, 1000));\n } catch (err) {\n console.error(\"[ASR] Failed to send session.finish:\", err);\n }\n }\n\n // Close WebSocket\n if (ws && ws?.readyState !== WebSocket.CLOSING && ws?.readyState !== WebSocket.CLOSED) {\n ws?.close();\n }\n ws = null;\n isClosing = false;\n }\n\n function isRecording(): boolean {\n return isRecordingFlag;\n }\n\n function isConnected(): boolean {\n return ws !== null && ws.readyState === WebSocket.OPEN;\n }\n\n return {\n connect,\n startRecording,\n stopRecording,\n close,\n isRecording,\n isConnected,\n };\n}\n\nexport default (authConfig: Pick<ASRClientConfig, \"getAccessToken\">) => (config: ASRClientConfig) =>\n createASRClient({ ...authConfig, ...config });\n\nexport type {\n SessionConfig,\n TurnDetectionConfig,\n InputAudioTranscriptionConfig,\n ClientEvent,\n ServerEvent,\n SessionUpdateEvent,\n InputAudioBufferAppendEvent,\n InputAudioBufferCommitEvent,\n SessionFinishEvent,\n SessionCreatedEvent,\n SessionUpdatedEvent,\n TranscriptionTextEvent,\n TranscriptionCompletedEvent,\n SessionFinishedEvent,\n ErrorEvent,\n};\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n *\n * HTTP-based speech recognition suitable for press-to-talk scenarios where you hold to speak\n * and release to recognize. Good for voice messages, voice search, etc.\n *\n * @example\n * ```typescript\n * const client = createASRHttpClient({\n * onRecordingStart() {\n * console.log(\"Recording started\");\n * },\n * onRecordingStop() {\n * console.log(\"Recording stopped\");\n * },\n * onResult(text) {\n * console.log(\"Recognized:\", text);\n * },\n * onError(err) {\n * console.error(\"ASR error:\", err);\n * },\n * });\n *\n * // Hold to speak, release to recognize\n * await client.startRecording();\n * // ... stop ...\n * const result = await client.stopRecording();\n * ```\n */\n\nimport { createHttpClient, type HttpClient } from \"@amaster.ai/http-client\";\n\nconst ASR_HTTP_PATH =\n \"/api/proxy/builtin/platform/qwen-asr/compatible-mode/v1/chat/completions\";\n\n// 录音内核\nconst RECORDER_WORKLET = `\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0].slice(0));\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n`;\n\ntype Status = \"idle\" | \"recording\" | \"recognizing\";\n\ninterface Recorder {\n /** Start recording */\n start(): Promise<void>;\n /**\n * Stop recording and get base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @returns Base64-encoded WAV audio data\n */\n stop(): Promise<void>;\n}\n\ninterface RecorderOptions {\n /** Called when recording starts */\n onStart?: () => void;\n /**\n * Called when recording stops, with base64-encoded WAV audio data. You can use this data to call the ASR API.\n *\n * @param base64 - Base64-encoded WAV audio data\n * @returns void\n */\n onStop?: (base64: string) => void;\n onError?: (error: Error) => void;\n}\n\n// 创建录音器\nasync function createWebRecorder(props?: RecorderOptions): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n const cleanup = () => {\n try {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n } catch (e) {}\n };\n\n return {\n async start() {\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n ctx = new AudioContext();\n\n const blob = new Blob([RECORDER_WORKLET], {\n type: \"application/javascript\",\n });\n const url = URL.createObjectURL(blob);\n await ctx.audioWorklet.addModule(url);\n URL.revokeObjectURL(url);\n\n source = ctx.createMediaStreamSource(stream);\n node = new AudioWorkletNode(ctx, \"recorder-processor\");\n\n node.port.onmessage = (e) => {\n const input = e.data as Float32Array;\n const pcm = new Int16Array(input.length);\n for (let i = 0; i < input.length; i++) {\n const s = Math.max(-1, Math.min(1, input[i] || 0));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n\n props?.onStart?.();\n } catch (error) {\n props?.onError?.(\n error instanceof Error ? error : new Error(String(error)),\n );\n cleanup();\n }\n },\n\n async stop() {\n cleanup();\n\n const total = chunks.reduce((s, c) => s + c.length, 0);\n const pcm = new Int16Array(total);\n let offset = 0;\n for (const c of chunks) {\n pcm.set(c, offset);\n offset += c.length;\n }\n\n const result = { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\n const base64 = await blobToBase64(\n pcmToWav(result.pcm, result.sampleRate),\n );\n props?.onStop?.(base64);\n },\n };\n}\n\n/**\n * Convert PCM to WAV Blob\n * @param pcm - PCM data\n * @param sampleRate - Sample rate\n * @returns WAV Blob\n */\nfunction pcmToWav(pcm: Int16Array, sampleRate: number): Blob {\n const buffer = new ArrayBuffer(44 + pcm.length * 2);\n const view = new DataView(buffer);\n\n const write = (o: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(o + i, s.charCodeAt(i));\n };\n\n write(0, \"RIFF\");\n view.setUint32(4, 36 + pcm.length * 2, true);\n write(8, \"WAVE\");\n write(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n write(36, \"data\");\n view.setUint32(40, pcm.length * 2, true);\n\n for (let i = 0; i < pcm.length; i++) {\n view.setInt16(44 + i * 2, pcm[i] || 0, true);\n }\n\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const result = reader.result as string;\n resolve(result.split(\",\")[1] || \"\");\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\nexport interface ASRHttpClientConfig {\n http?: HttpClient;\n /** Get access token */\n getAccessToken?(): string | null;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\n /** Create custom recorder */\n createRecorder?(options?: RecorderOptions): Promise<Recorder>;\n /** Called when recording starts */\n onRecordingStart?: () => void;\n /** Called when recording stops */\n onRecordingStop?: () => void;\n /** Called with recognition result */\n onResult?: (text: string) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n /** Called when status changes */\n onStatusChange?: (status: Status) => void;\n}\n\nexport interface ASRHttpClient {\n /** Start recording (press-to-talk) */\n startRecording(): Promise<void>;\n /** Stop recording and get result */\n stopRecording(): Promise<void>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<void>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(base64: string): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nclass AsrHttpClient {\n http: HttpClient;\n recorder: Recorder | null = null;\n path: string = \"\";\n config: ASRHttpClientConfig;\n recognizing = false;\n constructor(config: ASRHttpClientConfig, path: string) {\n this.http = config.http ?? createHttpClient();\n this.config = config;\n this.path = path;\n }\n async startRecording() {\n if (this.recorder) {\n return;\n }\n const options = {\n onStart: () => {\n this.config.onRecordingStart?.();\n this.config.onStatusChange?.(\"recording\");\n },\n onStop: async (base64: string) => {\n this.config.onStatusChange?.(\"recognizing\");\n const text = await this.recognizeFile(base64);\n this.config.onResult?.(text);\n this.config.onRecordingStop?.();\n this.config.onStatusChange?.(\"idle\");\n this.recorder = null;\n },\n onError: (err: any) => {\n this.config.onError?.(err);\n this.config.onStatusChange?.(\"idle\");\n this.recorder = null;\n },\n };\n this.recorder = await (this.config.createRecorder?.(options) ??\n createWebRecorder(options));\n await this.recorder.start();\n }\n\n async stopRecording() {\n if (this.recorder) {\n await this.recorder.stop();\n this.recorder = null;\n } else {\n this.config.onResult?.(\"\");\n this.config.onRecordingStop?.();\n this.config.onStatusChange?.(\"idle\");\n }\n }\n\n async recognizeFile(base64: string): Promise<string> {\n if (this.recognizing) {\n return \"\";\n }\n this.recognizing = true;\n try {\n const response = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [\n {\n type: \"input_audio\",\n input_audio: { data: `data:audio/wav;base64,${base64}` },\n },\n ],\n },\n ],\n }),\n });\n\n return (response as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n console.error(\"ASR recognition error:\", e);\n return \"\";\n } finally {\n this.recognizing = false;\n }\n }\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n await this.stopRecording();\n }\n\n async recognizeUrl(url: string) {\n try {\n const res = await this.http.request({\n url: this.path,\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n model: \"qwen3-asr-flash\",\n messages: [\n {\n role: \"user\",\n content: [{ type: \"input_audio\", input_audio: { url } }],\n },\n ],\n }),\n });\n return (res as any)?.data?.choices?.[0]?.message?.content || \"\";\n } catch (e) {\n console.error(\"ASR recognition error:\", e);\n return \"\";\n }\n }\n}\n\nexport function createASRHttpClient(\n config: ASRHttpClientConfig,\n): ASRHttpClient {\n let path = ASR_HTTP_PATH;\n\n if (config.getAccessToken) {\n const token = config.getAccessToken();\n if (token) {\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n path = `${path}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n return new AsrHttpClient(config, path);\n}\n\nexport default (authConfig: Pick<ASRHttpClientConfig, \"getAccessToken\">) =>\n (config: ASRHttpClientConfig) =>\n createASRHttpClient({ ...authConfig, ...config });\n"],"mappings":"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","names":["err"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amaster.ai/asr-client",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
4
4
|
"description": "Qwen ASR Realtime WebSocket client with microphone recording",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"registry": "https://registry.npmjs.org/"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@amaster.ai/http-client": "1.0.0-alpha.
|
|
36
|
+
"@amaster.ai/http-client": "1.0.0-alpha.3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.3.5",
|