@amaster.ai/asr-http-client 1.1.0-beta.46 → 1.1.0-beta.48

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 CHANGED
@@ -75,10 +75,12 @@ async function createRecorder() {
75
75
  source.connect(node);
76
76
  },
77
77
  async stop() {
78
- source.disconnect();
79
- node.disconnect();
80
- stream.getTracks().forEach((t) => t.stop());
81
- await ctx.close();
78
+ source?.disconnect();
79
+ node?.disconnect();
80
+ stream?.getTracks().forEach((t) => t.stop());
81
+ if (ctx) {
82
+ await ctx.close();
83
+ }
82
84
  const total = chunks.reduce((s, c) => s + c.length, 0);
83
85
  const pcm = new Int16Array(total);
84
86
  let offset = 0;
@@ -86,7 +88,7 @@ async function createRecorder() {
86
88
  pcm.set(c, offset);
87
89
  offset += c.length;
88
90
  }
89
- return { pcm, sampleRate: ctx.sampleRate };
91
+ return { pcm, sampleRate: ctx?.sampleRate ?? 16e3 };
90
92
  }
91
93
  };
92
94
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/asr-http-client.ts"],"sourcesContent":["export * from './asr-http-client';\nexport { createASRHttpClient as createAsrHttpClient } from './asr-http-client';\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n */\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(): Promise<void>;\n stop(): Promise<{ pcm: Int16Array; sampleRate: number }>;\n}\n\n// 创建录音器\nasync function createRecorder(): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n return {\n async start() {\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]));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n },\n\n async stop() {\n source.disconnect();\n node.disconnect();\n stream.getTracks().forEach((t) => t.stop());\n await ctx.close();\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 return { pcm, sampleRate: ctx.sampleRate };\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], 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\n/**\n * Recognize from Blob\n * @param blob - Audio Blob\n * @returns Recognized text\n */\nasync function recognizeBlob(blob: Blob, path?: string): Promise<string> {\n const base64 = await blobToBase64(blob);\n const response = await fetch(path ?? ASR_HTTP_PATH, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await response.json();\n return data.choices?.[0]?.message?.content || \"\";\n}\n\n/** Recognize audio file */\nexport async function recognizeFile(file: File | Blob, path?: string): Promise<string> {\n return recognizeBlob(file, path);\n}\n\nexport interface ASRHttpClientConfig {\n path?: string;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\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<string>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<string>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(file: File | Blob): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let recorder: Recorder | null = null;\n\n const path = config.path ?? ASR_HTTP_PATH;\n\n return {\n async startRecording() {\n recorder = await createRecorder();\n await recorder.start();\n config.onRecordingStart?.();\n },\n\n async stopRecording() {\n if (!recorder) throw new Error(\"Not recording\");\n const { pcm, sampleRate } = await recorder.stop();\n recorder = null;\n config.onRecordingStop?.();\n const text = await recognizeBlob(pcmToWav(pcm, sampleRate), path);\n config.onResult?.(text);\n return text;\n },\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n return this.stopRecording();\n },\n\n recognizeFile: (file: File | Blob) => {\n return recognizeFile(file, path);\n },\n\n async recognizeUrl(url: string) {\n const res = await fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await res.json();\n return data.choices?.[0]?.message?.content || \"\";\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/asr-http-client.ts"],"sourcesContent":["export * from './asr-http-client';\nexport { createASRHttpClient as createAsrHttpClient } from './asr-http-client';\n","/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n */\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(): Promise<void>;\n stop(): Promise<{ pcm: Int16Array; sampleRate: number }>;\n}\n\n// 创建录音器\nasync function createRecorder(): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n return {\n async start() {\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]));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n },\n\n async stop() {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n if (ctx) {\n await ctx.close();\n }\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 return { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\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], 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\n/**\n * Recognize from Blob\n * @param blob - Audio Blob\n * @returns Recognized text\n */\nasync function recognizeBlob(blob: Blob, path?: string): Promise<string> {\n const base64 = await blobToBase64(blob);\n const response = await fetch(path ?? ASR_HTTP_PATH, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await response.json();\n return data.choices?.[0]?.message?.content || \"\";\n}\n\n/** Recognize audio file */\nexport async function recognizeFile(file: File | Blob, path?: string): Promise<string> {\n return recognizeBlob(file, path);\n}\n\nexport interface ASRHttpClientConfig {\n path?: string;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\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<string>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<string>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(file: File | Blob): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let recorder: Recorder | null = null;\n\n const path = config.path ?? ASR_HTTP_PATH;\n\n return {\n async startRecording() {\n recorder = await createRecorder();\n await recorder.start();\n config.onRecordingStart?.();\n },\n\n async stopRecording() {\n if (!recorder) throw new Error(\"Not recording\");\n const { pcm, sampleRate } = await recorder.stop();\n recorder = null;\n config.onRecordingStop?.();\n const text = await recognizeBlob(pcmToWav(pcm, sampleRate), path);\n config.onResult?.(text);\n return text;\n },\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n return this.stopRecording();\n },\n\n recognizeFile: (file: File | Blob) => {\n return recognizeFile(file, path);\n },\n\n async recognizeUrl(url: string) {\n const res = await fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await res.json();\n return data.choices?.[0]?.message?.content || \"\";\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -47,10 +47,12 @@ async function createRecorder() {
47
47
  source.connect(node);
48
48
  },
49
49
  async stop() {
50
- source.disconnect();
51
- node.disconnect();
52
- stream.getTracks().forEach((t) => t.stop());
53
- await ctx.close();
50
+ source?.disconnect();
51
+ node?.disconnect();
52
+ stream?.getTracks().forEach((t) => t.stop());
53
+ if (ctx) {
54
+ await ctx.close();
55
+ }
54
56
  const total = chunks.reduce((s, c) => s + c.length, 0);
55
57
  const pcm = new Int16Array(total);
56
58
  let offset = 0;
@@ -58,7 +60,7 @@ async function createRecorder() {
58
60
  pcm.set(c, offset);
59
61
  offset += c.length;
60
62
  }
61
- return { pcm, sampleRate: ctx.sampleRate };
63
+ return { pcm, sampleRate: ctx?.sampleRate ?? 16e3 };
62
64
  }
63
65
  };
64
66
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/asr-http-client.ts"],"sourcesContent":["/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n */\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(): Promise<void>;\n stop(): Promise<{ pcm: Int16Array; sampleRate: number }>;\n}\n\n// 创建录音器\nasync function createRecorder(): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n return {\n async start() {\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]));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n },\n\n async stop() {\n source.disconnect();\n node.disconnect();\n stream.getTracks().forEach((t) => t.stop());\n await ctx.close();\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 return { pcm, sampleRate: ctx.sampleRate };\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], 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\n/**\n * Recognize from Blob\n * @param blob - Audio Blob\n * @returns Recognized text\n */\nasync function recognizeBlob(blob: Blob, path?: string): Promise<string> {\n const base64 = await blobToBase64(blob);\n const response = await fetch(path ?? ASR_HTTP_PATH, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await response.json();\n return data.choices?.[0]?.message?.content || \"\";\n}\n\n/** Recognize audio file */\nexport async function recognizeFile(file: File | Blob, path?: string): Promise<string> {\n return recognizeBlob(file, path);\n}\n\nexport interface ASRHttpClientConfig {\n path?: string;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\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<string>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<string>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(file: File | Blob): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let recorder: Recorder | null = null;\n\n const path = config.path ?? ASR_HTTP_PATH;\n\n return {\n async startRecording() {\n recorder = await createRecorder();\n await recorder.start();\n config.onRecordingStart?.();\n },\n\n async stopRecording() {\n if (!recorder) throw new Error(\"Not recording\");\n const { pcm, sampleRate } = await recorder.stop();\n recorder = null;\n config.onRecordingStop?.();\n const text = await recognizeBlob(pcmToWav(pcm, sampleRate), path);\n config.onResult?.(text);\n return text;\n },\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n return this.stopRecording();\n },\n\n recognizeFile: (file: File | Blob) => {\n return recognizeFile(file, path);\n },\n\n async recognizeUrl(url: string) {\n const res = await fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await res.json();\n return data.choices?.[0]?.message?.content || \"\";\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/asr-http-client.ts"],"sourcesContent":["/**\n * HTTP ASR Client - Press-to-talk style speech recognition\n */\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(): Promise<void>;\n stop(): Promise<{ pcm: Int16Array; sampleRate: number }>;\n}\n\n// 创建录音器\nasync function createRecorder(): Promise<Recorder> {\n let stream: MediaStream;\n let ctx: AudioContext;\n let node: AudioWorkletNode;\n let source: MediaStreamAudioSourceNode;\n const chunks: Int16Array[] = [];\n\n return {\n async start() {\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]));\n pcm[i] = s < 0 ? s * 32768 : s * 32767;\n }\n chunks.push(pcm);\n };\n\n source.connect(node);\n },\n\n async stop() {\n source?.disconnect();\n node?.disconnect();\n stream?.getTracks().forEach((t) => t.stop());\n if (ctx) {\n await ctx.close();\n }\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 return { pcm, sampleRate: ctx?.sampleRate ?? 16000 };\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], 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\n/**\n * Recognize from Blob\n * @param blob - Audio Blob\n * @returns Recognized text\n */\nasync function recognizeBlob(blob: Blob, path?: string): Promise<string> {\n const base64 = await blobToBase64(blob);\n const response = await fetch(path ?? ASR_HTTP_PATH, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await response.json();\n return data.choices?.[0]?.message?.content || \"\";\n}\n\n/** Recognize audio file */\nexport async function recognizeFile(file: File | Blob, path?: string): Promise<string> {\n return recognizeBlob(file, path);\n}\n\nexport interface ASRHttpClientConfig {\n path?: string;\n /** Language, default 'zh' */\n language?: string;\n /** Sample rate, default 16000 */\n sampleRate?: number;\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<string>;\n /** Record for specific duration then recognize */\n recordAndRecognize(durationMs: number): Promise<string>;\n /** Recognize audio file (File or Blob) */\n recognizeFile(file: File | Blob): Promise<string>;\n /** Recognize audio from URL */\n recognizeUrl(audioUrl: string): Promise<string>;\n}\n\nexport function createASRHttpClient(config: ASRHttpClientConfig): ASRHttpClient {\n let recorder: Recorder | null = null;\n\n const path = config.path ?? ASR_HTTP_PATH;\n\n return {\n async startRecording() {\n recorder = await createRecorder();\n await recorder.start();\n config.onRecordingStart?.();\n },\n\n async stopRecording() {\n if (!recorder) throw new Error(\"Not recording\");\n const { pcm, sampleRate } = await recorder.stop();\n recorder = null;\n config.onRecordingStop?.();\n const text = await recognizeBlob(pcmToWav(pcm, sampleRate), path);\n config.onResult?.(text);\n return text;\n },\n\n async recordAndRecognize(ms: number) {\n await this.startRecording();\n await new Promise((r) => setTimeout(r, ms));\n return this.stopRecording();\n },\n\n recognizeFile: (file: File | Blob) => {\n return recognizeFile(file, path);\n },\n\n async recognizeUrl(url: string) {\n const res = await fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: 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 const data = await res.json();\n return data.choices?.[0]?.message?.content || \"\";\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/asr-http-client",
3
- "version": "1.1.0-beta.46",
3
+ "version": "1.1.0-beta.48",
4
4
  "description": "HTTP ASR client with press-to-talk recording",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",