univoice 0.10.0 → 0.11.0
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/README.md +7 -5
- package/dist/chunk-DVEMLSBS.js +128 -0
- package/dist/chunk-DVEMLSBS.js.map +1 -0
- package/dist/chunk-OZBT3ASY.js +404 -0
- package/dist/chunk-OZBT3ASY.js.map +1 -0
- package/dist/{chunk-F3GCENLN.js → chunk-TRLZRYS3.js} +3 -124
- package/dist/chunk-TRLZRYS3.js.map +1 -0
- package/dist/src/asr/index.js +2 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/tts/index.d.ts +2 -1
- package/dist/src/tts/index.js +2 -1
- package/dist/{tee-F-nBCt45.d.ts → tee-D6nGEM5I.d.ts} +13 -1
- package/examples/asr/providers/doubao/streaming-end-of-speech-detection.ts +20 -105
- package/examples/tts/providers/doubao/pcm-to-opus.ts +104 -0
- package/package.json +1 -1
- package/dist/chunk-6WSSFM2A.js +0 -183
- package/dist/chunk-6WSSFM2A.js.map +0 -1
- package/dist/chunk-F3GCENLN.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
<br />
|
|
2
1
|
<div align="center">
|
|
3
|
-
<
|
|
2
|
+
<br />
|
|
3
|
+
<br />
|
|
4
|
+
<img src="logo.svg" alt="Univoice" width="400" height="100" />
|
|
5
|
+
<br />
|
|
6
|
+
<br />
|
|
7
|
+
<br />
|
|
4
8
|
</div>
|
|
5
|
-
<br />
|
|
6
9
|
|
|
7
10
|
<div align="center">
|
|
8
11
|
|
|
@@ -711,8 +714,7 @@ const asr = createASR({
|
|
|
711
714
|
|
|
712
715
|
---
|
|
713
716
|
|
|
714
|
-
*数据更新于: 2026-04-
|
|
715
|
-
|
|
717
|
+
*数据更新于: 2026-04-14*
|
|
716
718
|
<!-- PERFORMANCE_TABLE_END -->
|
|
717
719
|
|
|
718
720
|
---
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { __name } from './chunk-7QVYU63E.js';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
import { writeFile } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
// src/asr/utils/collect.ts
|
|
6
|
+
async function collectText(response, options = {}) {
|
|
7
|
+
const { text } = response;
|
|
8
|
+
if (options.onSegment && response.segments) {
|
|
9
|
+
for (const segment of response.segments) {
|
|
10
|
+
options.onSegment(segment.text);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (options.onComplete) {
|
|
14
|
+
options.onComplete(text);
|
|
15
|
+
}
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
__name(collectText, "collectText");
|
|
19
|
+
async function* decodeOpusStream(opusPackets, options) {
|
|
20
|
+
const { sampleRate = 16e3, channels = 1, frameSizeMs = 20 } = options || {};
|
|
21
|
+
let prismMedia;
|
|
22
|
+
try {
|
|
23
|
+
prismMedia = await import('prism-media');
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"prism-media is required for Opus decoding but is not installed. Install it with: npm install prism-media or pnpm add prism-media"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const frameSize = sampleRate / 1e3 * frameSizeMs;
|
|
30
|
+
const decoder = new prismMedia.opus.Decoder({
|
|
31
|
+
frameSize,
|
|
32
|
+
channels,
|
|
33
|
+
rate: sampleRate
|
|
34
|
+
});
|
|
35
|
+
yield* bridgeTransformStream(decoder, opusPackets);
|
|
36
|
+
}
|
|
37
|
+
__name(decodeOpusStream, "decodeOpusStream");
|
|
38
|
+
async function* bridgeTransformStream(decoder, opusPackets) {
|
|
39
|
+
const pcmChunks = [];
|
|
40
|
+
let decodeError = null;
|
|
41
|
+
let outputDone = false;
|
|
42
|
+
decoder.on("data", (chunk) => {
|
|
43
|
+
pcmChunks.push(chunk);
|
|
44
|
+
});
|
|
45
|
+
decoder.on("error", (err) => {
|
|
46
|
+
decodeError = err;
|
|
47
|
+
});
|
|
48
|
+
decoder.on("end", () => {
|
|
49
|
+
outputDone = true;
|
|
50
|
+
});
|
|
51
|
+
const writePromise = (async () => {
|
|
52
|
+
try {
|
|
53
|
+
for await (const packet of opusPackets) {
|
|
54
|
+
if (decodeError) break;
|
|
55
|
+
const data = Buffer.isBuffer(packet) ? packet : Buffer.from(packet);
|
|
56
|
+
if (!decoder.write(data)) {
|
|
57
|
+
await new Promise((resolve) => decoder.once("drain", resolve));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
decodeError = err instanceof Error ? err : new Error(String(err));
|
|
62
|
+
} finally {
|
|
63
|
+
decoder.end();
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
67
|
+
while (!outputDone || pcmChunks.length > 0) {
|
|
68
|
+
if (decodeError) {
|
|
69
|
+
throw decodeError;
|
|
70
|
+
}
|
|
71
|
+
if (pcmChunks.length > 0) {
|
|
72
|
+
const chunk = pcmChunks.shift();
|
|
73
|
+
if (chunk) yield chunk;
|
|
74
|
+
} else if (!outputDone) {
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
const cleanup = /* @__PURE__ */ __name(() => {
|
|
77
|
+
decoder.removeListener("data", onData);
|
|
78
|
+
decoder.removeListener("end", onEnd);
|
|
79
|
+
decoder.removeListener("error", onError);
|
|
80
|
+
}, "cleanup");
|
|
81
|
+
const onData = /* @__PURE__ */ __name(() => {
|
|
82
|
+
cleanup();
|
|
83
|
+
resolve();
|
|
84
|
+
}, "onData");
|
|
85
|
+
const onEnd = /* @__PURE__ */ __name(() => {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve();
|
|
88
|
+
}, "onEnd");
|
|
89
|
+
const onError = /* @__PURE__ */ __name((err) => {
|
|
90
|
+
cleanup();
|
|
91
|
+
decodeError = err;
|
|
92
|
+
resolve();
|
|
93
|
+
}, "onError");
|
|
94
|
+
decoder.once("data", onData);
|
|
95
|
+
decoder.once("end", onEnd);
|
|
96
|
+
decoder.once("error", onError);
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await writePromise;
|
|
103
|
+
if (decodeError) {
|
|
104
|
+
throw decodeError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
__name(bridgeTransformStream, "bridgeTransformStream");
|
|
108
|
+
async function saveText(response, options = {}) {
|
|
109
|
+
const format = options.format || "txt";
|
|
110
|
+
const timestamp = Date.now();
|
|
111
|
+
const filename = options.filename || `asr_${timestamp}.${format}`;
|
|
112
|
+
const filepath = options.directory ? `${options.directory}/${filename}` : filename;
|
|
113
|
+
let content;
|
|
114
|
+
if (format === "json") {
|
|
115
|
+
content = JSON.stringify(response, null, 2);
|
|
116
|
+
} else if (format === "srt" || format === "vtt") {
|
|
117
|
+
content = response.text;
|
|
118
|
+
} else {
|
|
119
|
+
content = response.text;
|
|
120
|
+
}
|
|
121
|
+
await writeFile(filepath, content, "utf-8");
|
|
122
|
+
return filepath;
|
|
123
|
+
}
|
|
124
|
+
__name(saveText, "saveText");
|
|
125
|
+
|
|
126
|
+
export { collectText, decodeOpusStream, saveText };
|
|
127
|
+
//# sourceMappingURL=chunk-DVEMLSBS.js.map
|
|
128
|
+
//# sourceMappingURL=chunk-DVEMLSBS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/asr/utils/collect.ts","../src/asr/utils/opus-decode.ts","../src/asr/utils/save.ts"],"names":[],"mappings":"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","file":"chunk-DVEMLSBS.js","sourcesContent":["import type { ASRResponse } from '@/types/asr';\n\nexport interface CollectOptions {\n onSegment?: (segment: string) => void;\n onComplete?: (text: string) => void;\n}\n\nexport async function collectText(\n response: ASRResponse,\n options: CollectOptions = {}\n): Promise<string> {\n const { text } = response;\n\n if (options.onSegment && response.segments) {\n for (const segment of response.segments) {\n options.onSegment(segment.text);\n }\n }\n\n if (options.onComplete) {\n options.onComplete(text);\n }\n\n return text;\n}\n","/**\n * 流式 Opus 解码器\n *\n * 将 Opus 数据包流式解码为 PCM 流\n * 依赖 prism-media(作为 optionalDependency)\n *\n * @module opus-decode\n */\n\nimport { Buffer } from 'node:buffer';\n\n/**\n * 解码选项\n */\nexport interface DecodeOpusStreamOptions {\n /** 目标 PCM 采样率,默认 16000(ASR 常用采样率) */\n sampleRate?: number;\n /** 声道数,默认 1(单声道) */\n channels?: number;\n /** Opus 帧大小(毫秒),默认 20 */\n frameSizeMs?: number;\n}\n\n/**\n * 将 Opus 数据包流式解码为 PCM 流\n *\n * 真正的流式处理:收到一个 Opus packet 就解码输出 PCM,不等待全部数据。\n * 利用 prism-media 的 opus.Decoder(Transform Stream)实现逐 packet 解码。\n * Opus 解码器原生支持输出任意采样率的 PCM,无需 ffmpeg 重采样。\n *\n * @param opusPackets Opus 数据包流(AsyncIterable<Buffer>)\n * @param options 解码选项\n * @returns PCM 音频流(AsyncIterable<Buffer>)\n *\n * @example\n * ```typescript\n * import { decodeOpusStream } from 'univoice/asr';\n *\n * // 硬件端发送的 Opus 裸流\n * const pcmStream = decodeOpusStream(hardwareStream, {\n * sampleRate: 16000,\n * });\n *\n * // 直接传给 ASR 服务\n * for await (const chunk of asr.listen(pcmStream, { stream: true })) {\n * console.log(chunk.text);\n * }\n * ```\n */\nexport async function* decodeOpusStream(\n opusPackets: AsyncIterable<Buffer>,\n options?: DecodeOpusStreamOptions\n): AsyncIterable<Buffer> {\n const { sampleRate = 16000, channels = 1, frameSizeMs = 20 } = options || {};\n\n // 动态导入 prism-media\n let prismMedia: typeof import('prism-media');\n try {\n prismMedia = await import('prism-media');\n } catch {\n throw new Error(\n 'prism-media is required for Opus decoding but is not installed. ' +\n 'Install it with: npm install prism-media or pnpm add prism-media'\n );\n }\n\n // 计算帧大小(采样数)\n const frameSize = (sampleRate / 1000) * frameSizeMs;\n\n // 创建 Opus 解码器(Transform Stream)\n // Opus 解码器原生支持输出任意采样率的 PCM\n const decoder = new prismMedia.opus.Decoder({\n frameSize,\n channels,\n rate: sampleRate,\n });\n\n // 将 Transform Stream 桥接为 AsyncIterable,实现背压处理\n yield* bridgeTransformStream(decoder, opusPackets);\n}\n\n/**\n * 将 Node.js Transform Stream 桥接为 AsyncIterable\n *\n * 核心机制(拉取式):\n * 1. 后台异步任务从 opusPackets 逐个拉取 packet 并写入解码器\n * 2. 解码器解码后输出 PCM chunk 到队列\n * 3. 消费者从队列拉取 PCM chunk\n * 4. 通过背压控制(highWaterMark)确保不会无限缓冲\n *\n * @param decoder Opus 解码器(Transform Stream)\n * @param opusPackets Opus 数据包源\n */\nasync function* bridgeTransformStream(\n decoder: InstanceType<typeof import('prism-media').opus.Decoder>,\n opusPackets: AsyncIterable<Buffer>\n): AsyncIterable<Buffer> {\n const pcmChunks: Buffer[] = [];\n let decodeError: Error | null = null;\n let outputDone = false;\n\n // 收集解码输出\n decoder.on('data', (chunk: Buffer) => {\n pcmChunks.push(chunk);\n });\n\n decoder.on('error', (err: Error) => {\n decodeError = err;\n });\n\n decoder.on('end', () => {\n outputDone = true;\n });\n\n // 后台:流式写入 Opus 数据包到解码器\n const writePromise = (async () => {\n try {\n for await (const packet of opusPackets) {\n if (decodeError) break;\n const data = Buffer.isBuffer(packet) ? packet : Buffer.from(packet);\n // 写入解码器,如果缓冲区满则等待 drain(背压控制)\n if (!decoder.write(data)) {\n await new Promise<void>((resolve) => decoder.once('drain', resolve));\n }\n }\n } catch (err) {\n decodeError = err instanceof Error ? err : new Error(String(err));\n } finally {\n decoder.end();\n }\n })();\n\n // 等待写入启动\n await new Promise((resolve) => setImmediate(resolve));\n\n // 从解码器拉取 PCM 数据\n while (!outputDone || pcmChunks.length > 0) {\n if (decodeError) {\n throw decodeError;\n }\n\n if (pcmChunks.length > 0) {\n const chunk = pcmChunks.shift();\n if (chunk) yield chunk;\n } else if (!outputDone) {\n // 等待解码器产生数据或结束\n await new Promise<void>((resolve) => {\n const cleanup = () => {\n decoder.removeListener('data', onData);\n decoder.removeListener('end', onEnd);\n decoder.removeListener('error', onError);\n };\n const onData = () => {\n cleanup();\n resolve();\n };\n const onEnd = () => {\n cleanup();\n resolve();\n };\n const onError = (err: Error) => {\n cleanup();\n decodeError = err;\n resolve();\n };\n decoder.once('data', onData);\n decoder.once('end', onEnd);\n decoder.once('error', onError);\n });\n } else {\n break;\n }\n }\n\n // 等待写入完成\n await writePromise;\n\n if (decodeError) {\n throw decodeError;\n }\n}\n","import { writeFile } from 'node:fs/promises';\nimport type { ASRResponse } from '@/types/asr';\n\nexport interface SaveOptions {\n filename?: string;\n directory?: string;\n format?: 'txt' | 'json' | 'srt' | 'vtt';\n}\n\nexport async function saveText(response: ASRResponse, options: SaveOptions = {}): Promise<string> {\n const format = options.format || 'txt';\n const timestamp = Date.now();\n const filename = options.filename || `asr_${timestamp}.${format}`;\n const filepath = options.directory ? `${options.directory}/${filename}` : filename;\n\n let content: string;\n if (format === 'json') {\n content = JSON.stringify(response, null, 2);\n } else if (format === 'srt' || format === 'vtt') {\n // TODO: Implement SRT/VTT formatting\n content = response.text;\n } else {\n content = response.text;\n }\n\n await writeFile(filepath, content, 'utf-8');\n return filepath;\n}\n"]}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { createOggMuxerWithEos } from './chunk-TRLZRYS3.js';
|
|
2
|
+
import { __name } from './chunk-7QVYU63E.js';
|
|
3
|
+
import { Buffer as Buffer$1 } from 'buffer';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { writeFile } from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
async function collectAudio(response, options = {}) {
|
|
9
|
+
const { audio } = response;
|
|
10
|
+
const chunks = [];
|
|
11
|
+
if (isUint8Array(audio)) {
|
|
12
|
+
chunks.push(audio);
|
|
13
|
+
} else if (isBuffer(audio)) {
|
|
14
|
+
chunks.push(new Uint8Array(audio));
|
|
15
|
+
}
|
|
16
|
+
const result = concatUint8Arrays(chunks);
|
|
17
|
+
if (options.onComplete) {
|
|
18
|
+
options.onComplete(result);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
__name(collectAudio, "collectAudio");
|
|
23
|
+
function isUint8Array(value) {
|
|
24
|
+
return value instanceof Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
__name(isUint8Array, "isUint8Array");
|
|
27
|
+
function isBuffer(value) {
|
|
28
|
+
return Buffer$1.isBuffer(value);
|
|
29
|
+
}
|
|
30
|
+
__name(isBuffer, "isBuffer");
|
|
31
|
+
function concatUint8Arrays(arrays) {
|
|
32
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
33
|
+
const result = new Uint8Array(totalLength);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const arr of arrays) {
|
|
36
|
+
result.set(arr, offset);
|
|
37
|
+
offset += arr.length;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
__name(concatUint8Arrays, "concatUint8Arrays");
|
|
42
|
+
var VALID_FRAME_DURATIONS_MS = [2.5, 5, 10, 20, 40, 60];
|
|
43
|
+
async function* pcmToOpus(pcmStream, options) {
|
|
44
|
+
const {
|
|
45
|
+
sampleRate = 24e3,
|
|
46
|
+
channels = 1,
|
|
47
|
+
frameDurationMs = 60,
|
|
48
|
+
bytesPerSample = 2,
|
|
49
|
+
ogg: oggEnabled
|
|
50
|
+
} = options || {};
|
|
51
|
+
if (sampleRate <= 0) {
|
|
52
|
+
throw new RangeError("sampleRate must be a positive number");
|
|
53
|
+
}
|
|
54
|
+
if (channels <= 0) {
|
|
55
|
+
throw new RangeError("channels must be a positive number");
|
|
56
|
+
}
|
|
57
|
+
if (!VALID_FRAME_DURATIONS_MS.includes(frameDurationMs)) {
|
|
58
|
+
throw new RangeError(`frameDurationMs must be one of: ${VALID_FRAME_DURATIONS_MS.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
if (bytesPerSample !== 1 && bytesPerSample !== 2) {
|
|
61
|
+
throw new RangeError("bytesPerSample must be 1 or 2");
|
|
62
|
+
}
|
|
63
|
+
const frameSizeSamples = Math.round(sampleRate / 1e3 * frameDurationMs);
|
|
64
|
+
const frameSizeBytes = frameSizeSamples * bytesPerSample;
|
|
65
|
+
let prismMedia;
|
|
66
|
+
try {
|
|
67
|
+
prismMedia = await import('prism-media');
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"prism-media is required for Opus encoding but is not installed. Install it with: pnpm add prism-media"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const encoder = new prismMedia.opus.Encoder({
|
|
74
|
+
frameSize: frameSizeSamples,
|
|
75
|
+
channels,
|
|
76
|
+
rate: sampleRate
|
|
77
|
+
});
|
|
78
|
+
const outputQueue = [];
|
|
79
|
+
let encodeError = null;
|
|
80
|
+
let outputDone = false;
|
|
81
|
+
let wakeUpResolver = null;
|
|
82
|
+
function wakeUp() {
|
|
83
|
+
if (wakeUpResolver) {
|
|
84
|
+
const r = wakeUpResolver;
|
|
85
|
+
wakeUpResolver = null;
|
|
86
|
+
r();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
__name(wakeUp, "wakeUp");
|
|
90
|
+
const useOgg = !!oggEnabled;
|
|
91
|
+
const oggEncoderName = typeof oggEnabled === "object" ? oggEnabled.encoder : void 0;
|
|
92
|
+
let oggMuxer = null;
|
|
93
|
+
let pushIterable = null;
|
|
94
|
+
if (useOgg) {
|
|
95
|
+
pushIterable = new PushIterable();
|
|
96
|
+
oggMuxer = createOggMuxerWithEos(pushIterable.asyncIterable, {
|
|
97
|
+
sampleRate,
|
|
98
|
+
channels,
|
|
99
|
+
frameSizeMs: frameDurationMs,
|
|
100
|
+
encoder: oggEncoderName ?? "univoice"
|
|
101
|
+
});
|
|
102
|
+
(async () => {
|
|
103
|
+
try {
|
|
104
|
+
for await (const page of oggMuxer.stream) {
|
|
105
|
+
outputQueue.push(page);
|
|
106
|
+
wakeUp();
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (!encodeError) {
|
|
110
|
+
encodeError = err instanceof Error ? err : new Error(String(err));
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
outputDone = true;
|
|
114
|
+
wakeUp();
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
}
|
|
118
|
+
encoder.on("data", (chunk) => {
|
|
119
|
+
if (useOgg && pushIterable) {
|
|
120
|
+
pushIterable.push(chunk);
|
|
121
|
+
} else {
|
|
122
|
+
outputQueue.push(chunk);
|
|
123
|
+
wakeUp();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
encoder.on("error", (err) => {
|
|
127
|
+
encodeError = err;
|
|
128
|
+
if (!useOgg) {
|
|
129
|
+
outputDone = true;
|
|
130
|
+
}
|
|
131
|
+
wakeUp();
|
|
132
|
+
});
|
|
133
|
+
if (!useOgg) {
|
|
134
|
+
encoder.on("end", () => {
|
|
135
|
+
outputDone = true;
|
|
136
|
+
wakeUp();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const writePromise = (async () => {
|
|
140
|
+
let pcmBuffer = Buffer$1.alloc(0);
|
|
141
|
+
try {
|
|
142
|
+
for await (const chunk of pcmStream) {
|
|
143
|
+
if (encodeError) break;
|
|
144
|
+
const rawData = extractPcmData(chunk);
|
|
145
|
+
const data = Buffer$1.isBuffer(rawData) ? rawData : Buffer$1.from(rawData);
|
|
146
|
+
pcmBuffer = Buffer$1.concat([pcmBuffer, data]);
|
|
147
|
+
while (pcmBuffer.length >= frameSizeBytes) {
|
|
148
|
+
const frame = pcmBuffer.subarray(0, frameSizeBytes);
|
|
149
|
+
pcmBuffer = pcmBuffer.subarray(frameSizeBytes);
|
|
150
|
+
encoder.write(frame);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (pcmBuffer.length > 0) {
|
|
154
|
+
const paddedFrame = Buffer$1.alloc(frameSizeBytes);
|
|
155
|
+
pcmBuffer.copy(paddedFrame);
|
|
156
|
+
encoder.write(paddedFrame);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
encodeError = err instanceof Error ? err : new Error(String(err));
|
|
160
|
+
} finally {
|
|
161
|
+
encoder.end();
|
|
162
|
+
if (useOgg && oggMuxer) {
|
|
163
|
+
oggMuxer.finish();
|
|
164
|
+
if (pushIterable) {
|
|
165
|
+
pushIterable.finish();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
171
|
+
while (!outputDone || outputQueue.length > 0) {
|
|
172
|
+
if (encodeError) {
|
|
173
|
+
await writePromise.catch(() => {
|
|
174
|
+
});
|
|
175
|
+
throw encodeError;
|
|
176
|
+
}
|
|
177
|
+
if (outputQueue.length > 0) {
|
|
178
|
+
const item = outputQueue.shift();
|
|
179
|
+
if (item) yield item;
|
|
180
|
+
} else if (!outputDone) {
|
|
181
|
+
await new Promise((resolve) => {
|
|
182
|
+
wakeUpResolver = resolve;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
await writePromise;
|
|
187
|
+
if (encodeError) {
|
|
188
|
+
throw encodeError;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
__name(pcmToOpus, "pcmToOpus");
|
|
192
|
+
function extractPcmData(chunk) {
|
|
193
|
+
if ("audioChunk" in chunk && chunk.audioChunk instanceof Uint8Array) {
|
|
194
|
+
return chunk.audioChunk;
|
|
195
|
+
}
|
|
196
|
+
return chunk;
|
|
197
|
+
}
|
|
198
|
+
__name(extractPcmData, "extractPcmData");
|
|
199
|
+
var PushIterable = class {
|
|
200
|
+
static {
|
|
201
|
+
__name(this, "PushIterable");
|
|
202
|
+
}
|
|
203
|
+
queue = [];
|
|
204
|
+
done = false;
|
|
205
|
+
error = null;
|
|
206
|
+
pendingResolver = null;
|
|
207
|
+
push(value) {
|
|
208
|
+
this.queue.push(value);
|
|
209
|
+
if (this.pendingResolver) {
|
|
210
|
+
const resolver = this.pendingResolver;
|
|
211
|
+
this.pendingResolver = null;
|
|
212
|
+
const shifted = this.queue.shift();
|
|
213
|
+
resolver({ value: shifted, done: false });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
finish(err) {
|
|
217
|
+
if (err) {
|
|
218
|
+
this.error = err;
|
|
219
|
+
}
|
|
220
|
+
this.done = true;
|
|
221
|
+
if (this.pendingResolver) {
|
|
222
|
+
const resolver = this.pendingResolver;
|
|
223
|
+
this.pendingResolver = null;
|
|
224
|
+
if (this.error) {
|
|
225
|
+
resolver(Promise.reject(this.error));
|
|
226
|
+
} else {
|
|
227
|
+
resolver({ value: void 0, done: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
get asyncIterable() {
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
[Symbol.asyncIterator]() {
|
|
235
|
+
return {
|
|
236
|
+
next: /* @__PURE__ */ __name(() => {
|
|
237
|
+
if (this.queue.length > 0) {
|
|
238
|
+
const shifted = this.queue.shift();
|
|
239
|
+
return Promise.resolve({
|
|
240
|
+
value: shifted,
|
|
241
|
+
done: false
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (this.error) {
|
|
245
|
+
const err = this.error;
|
|
246
|
+
this.error = null;
|
|
247
|
+
return Promise.reject(err);
|
|
248
|
+
}
|
|
249
|
+
if (this.done) {
|
|
250
|
+
return Promise.resolve({
|
|
251
|
+
value: void 0,
|
|
252
|
+
done: true
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
this.pendingResolver = resolve;
|
|
257
|
+
});
|
|
258
|
+
}, "next")
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
async function playAudio(response, options = {}) {
|
|
263
|
+
const player = options.player || "afplay";
|
|
264
|
+
let buffer;
|
|
265
|
+
if (response.audio instanceof Buffer) {
|
|
266
|
+
buffer = response.audio;
|
|
267
|
+
} else if (response.audio instanceof Uint8Array) {
|
|
268
|
+
buffer = Buffer.from(response.audio);
|
|
269
|
+
} else {
|
|
270
|
+
throw new Error("Invalid audio data");
|
|
271
|
+
}
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const proc = spawn(player, [], { stdio: ["pipe", "inherit", "inherit"] });
|
|
274
|
+
proc.stdin.write(buffer);
|
|
275
|
+
proc.stdin.end();
|
|
276
|
+
proc.on("close", (code) => {
|
|
277
|
+
if (code === 0) {
|
|
278
|
+
resolve();
|
|
279
|
+
} else {
|
|
280
|
+
reject(new Error(`Player exited with code ${code}`));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
proc.on("error", reject);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
__name(playAudio, "playAudio");
|
|
287
|
+
async function saveTTSResponse(response, options = {}) {
|
|
288
|
+
const { format } = response;
|
|
289
|
+
const timestamp = Date.now();
|
|
290
|
+
const filename = options.filename || `tts_${timestamp}.${format}`;
|
|
291
|
+
const filepath = options.directory ? `${options.directory}/${filename}` : filename;
|
|
292
|
+
let buffer;
|
|
293
|
+
if (response.audio instanceof Buffer) {
|
|
294
|
+
buffer = response.audio;
|
|
295
|
+
} else if (response.audio instanceof Uint8Array) {
|
|
296
|
+
buffer = Buffer.from(response.audio);
|
|
297
|
+
} else {
|
|
298
|
+
throw new Error("Invalid audio data");
|
|
299
|
+
}
|
|
300
|
+
await writeFile(filepath, buffer);
|
|
301
|
+
return filepath;
|
|
302
|
+
}
|
|
303
|
+
__name(saveTTSResponse, "saveTTSResponse");
|
|
304
|
+
function isAsyncIterable(value) {
|
|
305
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
|
|
306
|
+
}
|
|
307
|
+
__name(isAsyncIterable, "isAsyncIterable");
|
|
308
|
+
function isTTSStreamChunk(value) {
|
|
309
|
+
return typeof value === "object" && value !== null && "audioChunk" in value && value.audioChunk instanceof Uint8Array;
|
|
310
|
+
}
|
|
311
|
+
__name(isTTSStreamChunk, "isTTSStreamChunk");
|
|
312
|
+
function concatUint8Arrays2(arrays) {
|
|
313
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
314
|
+
const result = new Uint8Array(totalLength);
|
|
315
|
+
let offset = 0;
|
|
316
|
+
for (const arr of arrays) {
|
|
317
|
+
result.set(arr, offset);
|
|
318
|
+
offset += arr.length;
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
__name(concatUint8Arrays2, "concatUint8Arrays");
|
|
323
|
+
function createWavHeader(dataLength, sampleRate = 24e3, channels = 1, bitsPerSample = 16) {
|
|
324
|
+
const headerLength = 44;
|
|
325
|
+
const header = new Uint8Array(headerLength);
|
|
326
|
+
const view = new DataView(header.buffer);
|
|
327
|
+
view.setUint8(0, 82);
|
|
328
|
+
view.setUint8(1, 73);
|
|
329
|
+
view.setUint8(2, 70);
|
|
330
|
+
view.setUint8(3, 70);
|
|
331
|
+
view.setUint32(4, 36 + dataLength, true);
|
|
332
|
+
view.setUint8(8, 87);
|
|
333
|
+
view.setUint8(9, 65);
|
|
334
|
+
view.setUint8(10, 86);
|
|
335
|
+
view.setUint8(11, 69);
|
|
336
|
+
view.setUint8(12, 102);
|
|
337
|
+
view.setUint8(13, 109);
|
|
338
|
+
view.setUint8(14, 116);
|
|
339
|
+
view.setUint8(15, 32);
|
|
340
|
+
view.setUint32(16, 16, true);
|
|
341
|
+
view.setUint16(20, 1, true);
|
|
342
|
+
view.setUint16(22, channels, true);
|
|
343
|
+
view.setUint32(24, sampleRate, true);
|
|
344
|
+
view.setUint32(28, sampleRate * channels * (bitsPerSample / 8), true);
|
|
345
|
+
view.setUint16(32, channels * (bitsPerSample / 8), true);
|
|
346
|
+
view.setUint16(34, bitsPerSample, true);
|
|
347
|
+
view.setUint8(36, 100);
|
|
348
|
+
view.setUint8(37, 97);
|
|
349
|
+
view.setUint8(38, 116);
|
|
350
|
+
view.setUint8(39, 97);
|
|
351
|
+
view.setUint32(40, dataLength, true);
|
|
352
|
+
return header;
|
|
353
|
+
}
|
|
354
|
+
__name(createWavHeader, "createWavHeader");
|
|
355
|
+
async function saveAudio(filePath, source, options) {
|
|
356
|
+
const chunks = [];
|
|
357
|
+
if (isAsyncIterable(source)) {
|
|
358
|
+
for await (const chunk of source) {
|
|
359
|
+
if (isTTSStreamChunk(chunk)) {
|
|
360
|
+
chunks.push(chunk.audioChunk);
|
|
361
|
+
} else if (chunk instanceof Uint8Array) {
|
|
362
|
+
chunks.push(chunk);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
chunks.push(...source);
|
|
367
|
+
}
|
|
368
|
+
const audio = concatUint8Arrays2(chunks);
|
|
369
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
370
|
+
if (ext === ".wav") {
|
|
371
|
+
const hasWavHeader = audio.length >= 4 && audio[0] === 82 && // 'R'
|
|
372
|
+
audio[1] === 73 && // 'I'
|
|
373
|
+
audio[2] === 70 && // 'F'
|
|
374
|
+
audio[3] === 70;
|
|
375
|
+
if (!hasWavHeader) {
|
|
376
|
+
const sampleRate = options?.sampleRate ?? 24e3;
|
|
377
|
+
const wavHeader = createWavHeader(audio.length, sampleRate);
|
|
378
|
+
const wavData = new Uint8Array(wavHeader.length + audio.length);
|
|
379
|
+
wavData.set(wavHeader);
|
|
380
|
+
wavData.set(audio, wavHeader.length);
|
|
381
|
+
await writeFile(filePath, wavData);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await writeFile(filePath, audio);
|
|
386
|
+
}
|
|
387
|
+
__name(saveAudio, "saveAudio");
|
|
388
|
+
|
|
389
|
+
// src/tts/utils/tee.ts
|
|
390
|
+
async function teeAudio(response, options = {}) {
|
|
391
|
+
const audio = await collectAudio(response);
|
|
392
|
+
if (options.save) {
|
|
393
|
+
await saveTTSResponse({ ...response, audio }, options.save);
|
|
394
|
+
}
|
|
395
|
+
if (options.play) {
|
|
396
|
+
await playAudio({ ...response, audio }, options.play);
|
|
397
|
+
}
|
|
398
|
+
return { ...response, audio };
|
|
399
|
+
}
|
|
400
|
+
__name(teeAudio, "teeAudio");
|
|
401
|
+
|
|
402
|
+
export { collectAudio, pcmToOpus, playAudio, saveAudio, saveTTSResponse, teeAudio };
|
|
403
|
+
//# sourceMappingURL=chunk-OZBT3ASY.js.map
|
|
404
|
+
//# sourceMappingURL=chunk-OZBT3ASY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tts/utils/collect.ts","../src/tts/utils/pcm-to-opus.ts","../src/tts/utils/play.ts","../src/tts/utils/save.ts","../src/tts/utils/save-audio.ts","../src/tts/utils/tee.ts"],"names":["Buffer","concatUint8Arrays","writeFile"],"mappings":";;;;;;;AASA,eAAsB,YAAA,CACpB,QAAA,EACA,OAAA,GAA0B,EAAC,EACN;AACrB,EAAA,MAAM,EAAE,OAAM,GAAI,QAAA;AAClB,EAAA,MAAM,SAAuB,EAAC;AAE9B,EAAA,IAAI,YAAA,CAAa,KAAK,CAAA,EAAG;AACvB,IAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,EACnB,CAAA,MAAA,IAAW,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1B,IAAA,MAAA,CAAO,IAAA,CAAK,IAAI,UAAA,CAAW,KAAK,CAAC,CAAA;AAAA,EACnC;AAEA,EAAA,MAAM,MAAA,GAAS,kBAAkB,MAAM,CAAA;AAEvC,EAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,IAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA,EAC3B;AAEA,EAAA,OAAO,MAAA;AACT;AApBsB,MAAA,CAAA,YAAA,EAAA,cAAA,CAAA;AAsBtB,SAAS,aAAa,KAAA,EAAqC;AACzD,EAAA,OAAO,KAAA,YAAiB,UAAA;AAC1B;AAFS,MAAA,CAAA,YAAA,EAAA,cAAA,CAAA;AAIT,SAAS,SAAS,KAAA,EAAiC;AACjD,EAAA,OAAOA,QAAAA,CAAO,SAAS,KAAK,CAAA;AAC9B;AAFS,MAAA,CAAA,QAAA,EAAA,UAAA,CAAA;AAIT,SAAS,kBAAkB,MAAA,EAAkC;AAC3D,EAAA,MAAM,WAAA,GAAc,OAAO,MAAA,CAAO,CAAC,KAAK,GAAA,KAAQ,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ,CAAC,CAAA;AACnE,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,WAAW,CAAA;AACzC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,KAAK,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,GAAA,CAAI,MAAA;AAAA,EAChB;AACA,EAAA,OAAO,MAAA;AACT;AATS,MAAA,CAAA,iBAAA,EAAA,mBAAA,CAAA;ACnBT,IAAM,2BAA2B,CAAC,GAAA,EAAK,GAAG,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AA0DxD,gBAAuB,SAAA,CACrB,WACA,OAAA,EACuB;AAEvB,EAAA,MAAM;AAAA,IACJ,UAAA,GAAa,IAAA;AAAA,IACb,QAAA,GAAW,CAAA;AAAA,IACX,eAAA,GAAkB,EAAA;AAAA,IAClB,cAAA,GAAiB,CAAA;AAAA,IACjB,GAAA,EAAK;AAAA,GACP,GAAI,WAAW,EAAC;AAEhB,EAAA,IAAI,cAAc,CAAA,EAAG;AACnB,IAAA,MAAM,IAAI,WAAW,sCAAsC,CAAA;AAAA,EAC7D;AACA,EAAA,IAAI,YAAY,CAAA,EAAG;AACjB,IAAA,MAAM,IAAI,WAAW,oCAAoC,CAAA;AAAA,EAC3D;AACA,EAAA,IAAI,CAAC,wBAAA,CAAyB,QAAA,CAAS,eAAe,CAAA,EAAG;AACvD,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,gCAAA,EAAmC,yBAAyB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EAC/F;AACA,EAAA,IAAI,cAAA,KAAmB,CAAA,IAAK,cAAA,KAAmB,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,WAAW,+BAA+B,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,KAAA,CAAO,UAAA,GAAa,MAAQ,eAAe,CAAA;AACzE,EAAA,MAAM,iBAAiB,gBAAA,GAAmB,cAAA;AAG1C,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI;AACF,IAAA,UAAA,GAAa,MAAM,OAAO,aAAa,CAAA;AAAA,EACzC,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAGA,EAAA,MAAM,OAAA,GAAU,IAAI,UAAA,CAAW,IAAA,CAAK,OAAA,CAAQ;AAAA,IAC1C,SAAA,EAAW,gBAAA;AAAA,IACX,QAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACP,CAAA;AAGD,EAAA,MAAM,cAAwB,EAAC;AAC/B,EAAA,IAAI,WAAA,GAA4B,IAAA;AAChC,EAAA,IAAI,UAAA,GAAa,KAAA;AAKjB,EAAA,IAAI,cAAA,GAAsC,IAAA;AAE1C,EAAA,SAAS,MAAA,GAAe;AACtB,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,MAAM,CAAA,GAAI,cAAA;AACV,MAAA,cAAA,GAAiB,IAAA;AACjB,MAAA,CAAA,EAAE;AAAA,IACJ;AAAA,EACF;AANS,EAAA,MAAA,CAAA,MAAA,EAAA,QAAA,CAAA;AAST,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,UAAA;AACjB,EAAA,MAAM,cAAA,GAAiB,OAAO,UAAA,KAAe,QAAA,GAAW,WAAW,OAAA,GAAU,MAAA;AAE7E,EAAA,IAAI,QAAA,GAA4D,IAAA;AAChE,EAAA,IAAI,YAAA,GAA4C,IAAA;AAEhD,EAAA,IAAI,MAAA,EAAQ;AAEV,IAAA,YAAA,GAAe,IAAI,YAAA,EAAqB;AACxC,IAAA,QAAA,GAAW,qBAAA,CAAsB,aAAa,aAAA,EAAe;AAAA,MAC3D,UAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA,EAAa,eAAA;AAAA,MACb,SAAS,cAAA,IAAkB;AAAA,KAC5B,CAAA;AAED,IAAA,CAAC,YAAY;AACX,MAAA,IAAI;AACF,QAAA,WAAA,MAAiB,IAAA,IAAQ,SAAS,MAAA,EAAQ;AACxC,UAAA,WAAA,CAAY,KAAK,IAAI,CAAA;AACrB,UAAA,MAAA,EAAO;AAAA,QACT;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,CAAC,WAAA,EAAa;AAChB,UAAA,WAAA,GAAc,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,QAClE;AAAA,MACF,CAAA,SAAE;AACA,QAAA,UAAA,GAAa,IAAA;AACb,QAAA,MAAA,EAAO;AAAA,MACT;AAAA,IACF,CAAA,GAAG;AAAA,EACL;AAGA,EAAA,OAAA,CAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACpC,IAAA,IAAI,UAAU,YAAA,EAAc;AAC1B,MAAA,YAAA,CAAa,KAAK,KAAK,CAAA;AAAA,IACzB,CAAA,MAAO;AACL,MAAA,WAAA,CAAY,KAAK,KAAK,CAAA;AACtB,MAAA,MAAA,EAAO;AAAA,IACT;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAe;AAClC,IAAA,WAAA,GAAc,GAAA;AACd,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,UAAA,GAAa,IAAA;AAAA,IACf;AACA,IAAA,MAAA,EAAO;AAAA,EACT,CAAC,CAAA;AAGD,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,OAAA,CAAQ,EAAA,CAAG,OAAO,MAAM;AACtB,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,MAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,EACH;AAGA,EAAA,MAAM,gBAAgB,YAAY;AAChC,IAAA,IAAI,SAAA,GAAYA,QAAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AAE9B,IAAA,IAAI;AACF,MAAA,WAAA,MAAiB,SAAS,SAAA,EAAW;AACnC,QAAA,IAAI,WAAA,EAAa;AAGjB,QAAA,MAAM,OAAA,GAAU,eAAe,KAAK,CAAA;AACpC,QAAA,MAAM,IAAA,GAAOA,SAAO,QAAA,CAAS,OAAO,IAAI,OAAA,GAAUA,QAAAA,CAAO,KAAK,OAAO,CAAA;AAGrE,QAAA,SAAA,GAAYA,QAAAA,CAAO,MAAA,CAAO,CAAC,SAAA,EAAW,IAAI,CAAC,CAAA;AAG3C,QAAA,OAAO,SAAA,CAAU,UAAU,cAAA,EAAgB;AACzC,UAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,QAAA,CAAS,CAAA,EAAG,cAAc,CAAA;AAClD,UAAA,SAAA,GAAY,SAAA,CAAU,SAAS,cAAc,CAAA;AAC7C,UAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA,QACrB;AAAA,MACF;AAGA,MAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACxB,QAAA,MAAM,WAAA,GAAcA,QAAAA,CAAO,KAAA,CAAM,cAAc,CAAA;AAC/C,QAAA,SAAA,CAAU,KAAK,WAAW,CAAA;AAC1B,QAAA,OAAA,CAAQ,MAAM,WAAW,CAAA;AAAA,MAC3B;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,WAAA,GAAc,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,IAClE,CAAA,SAAE;AAEA,MAAA,OAAA,CAAQ,GAAA,EAAI;AAGZ,MAAA,IAAI,UAAU,QAAA,EAAU;AACtB,QAAA,QAAA,CAAS,MAAA,EAAO;AAChB,QAAA,IAAI,YAAA,EAAc;AAChB,UAAA,YAAA,CAAa,MAAA,EAAO;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA,GAAG;AAGH,EAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,YAAA,CAAa,OAAO,CAAC,CAAA;AAGpD,EAAA,OAAO,CAAC,UAAA,IAAc,WAAA,CAAY,MAAA,GAAS,CAAA,EAAG;AAC5C,IAAA,IAAI,WAAA,EAAa;AAEf,MAAA,MAAM,YAAA,CAAa,MAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AACjC,MAAA,MAAM,WAAA;AAAA,IACR;AAEA,IAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG;AAC1B,MAAA,MAAM,IAAA,GAAO,YAAY,KAAA,EAAM;AAC/B,MAAA,IAAI,MAAM,MAAM,IAAA;AAAA,IAClB,CAAA,MAAA,IAAW,CAAC,UAAA,EAAY;AAEtB,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,QAAA,cAAA,GAAiB,OAAA;AAAA,MACnB,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAGA,EAAA,MAAM,YAAA;AAEN,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAM,WAAA;AAAA,EACR;AACF;AAtMuB,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;AA+MvB,SAAS,eAAe,KAAA,EAAkE;AACxF,EAAA,IAAI,YAAA,IAAgB,KAAA,IAAS,KAAA,CAAM,UAAA,YAAsB,UAAA,EAAY;AACnE,IAAA,OAAO,KAAA,CAAM,UAAA;AAAA,EACf;AACA,EAAA,OAAO,KAAA;AACT;AALS,MAAA,CAAA,cAAA,EAAA,gBAAA,CAAA;AAeT,IAAM,eAAN,MAAkD;AAAA,EA5SlD;AA4SkD,IAAA,MAAA,CAAA,IAAA,EAAA,cAAA,CAAA;AAAA;AAAA,EACxC,QAAa,EAAC;AAAA,EACd,IAAA,GAAO,KAAA;AAAA,EACP,KAAA,GAAsB,IAAA;AAAA,EACtB,eAAA,GAAgE,IAAA;AAAA,EAExE,KAAK,KAAA,EAAgB;AACnB,IAAA,IAAA,CAAK,KAAA,CAAM,KAAK,KAAK,CAAA;AACrB,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,MAAM,WAAW,IAAA,CAAK,eAAA;AACtB,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,KAAA,EAAM;AACjC,MAAA,QAAA,CAAS,EAAE,KAAA,EAAO,OAAA,EAAc,IAAA,EAAM,OAAO,CAAA;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,OAAO,GAAA,EAAmB;AACxB,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,IAAA,CAAK,KAAA,GAAQ,GAAA;AAAA,IACf;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,MAAM,WAAW,IAAA,CAAK,eAAA;AACtB,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AACvB,MAAA,IAAI,KAAK,KAAA,EAAO;AACd,QAAA,QAAA,CAAS,OAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,KAAK,CAAiC,CAAA;AAAA,MACrE,CAAA,MAAO;AACL,QAAA,QAAA,CAAS,EAAE,KAAA,EAAO,MAAA,EAAgB,IAAA,EAAM,MAAM,CAAA;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,aAAA,GAAkC;AACpC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,CAAC,MAAA,CAAO,aAAa,CAAA,GAAsB;AACzC,IAAA,OAAO;AAAA,MACL,sBAAM,MAAA,CAAA,MAAkC;AACtC,QAAA,IAAI,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG;AACzB,UAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,KAAA,EAAM;AACjC,UAAA,OAAO,QAAQ,OAAA,CAAQ;AAAA,YACrB,KAAA,EAAO,OAAA;AAAA,YACP,IAAA,EAAM;AAAA,WACP,CAAA;AAAA,QACH;AACA,QAAA,IAAI,KAAK,KAAA,EAAO;AACd,UAAA,MAAM,MAAM,IAAA,CAAK,KAAA;AACjB,UAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,UAAA,OAAO,OAAA,CAAQ,OAAO,GAAG,CAAA;AAAA,QAC3B;AACA,QAAA,IAAI,KAAK,IAAA,EAAM;AACb,UAAA,OAAO,QAAQ,OAAA,CAAQ;AAAA,YACrB,KAAA,EAAO,MAAA;AAAA,YACP,IAAA,EAAM;AAAA,WACP,CAAA;AAAA,QACH;AACA,QAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC9B,UAAA,IAAA,CAAK,eAAA,GAAkB,OAAA;AAAA,QACzB,CAAC,CAAA;AAAA,MACH,CAAA,EAtBM,MAAA;AAAA,KAuBR;AAAA,EACF;AACF,CAAA;ACpWA,eAAsB,SAAA,CAAU,QAAA,EAAuB,OAAA,GAAuB,EAAC,EAAkB;AAC/F,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,QAAA;AAEjC,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,QAAA,CAAS,iBAAiB,MAAA,EAAQ;AACpC,IAAA,MAAA,GAAS,QAAA,CAAS,KAAA;AAAA,EACpB,CAAA,MAAA,IAAW,QAAA,CAAS,KAAA,YAAiB,UAAA,EAAY;AAC/C,IAAA,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,QAAA,CAAS,KAAK,CAAA;AAAA,EACrC,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,EACtC;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,MAAA,EAAQ,EAAC,EAAG,EAAE,KAAA,EAAO,CAAC,MAAA,EAAQ,SAAA,EAAW,SAAS,CAAA,EAAG,CAAA;AACxE,IAAA,IAAA,CAAK,KAAA,CAAM,MAAM,MAAM,CAAA;AACvB,IAAA,IAAA,CAAK,MAAM,GAAA,EAAI;AACf,IAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS;AACzB,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,OAAA,EAAQ;AAAA,MACV,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,IAAI,EAAE,CAAC,CAAA;AAAA,MACrD;AAAA,IACF,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACzB,CAAC,CAAA;AACH;AAzBsB,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;ACStB,eAAsB,eAAA,CACpB,QAAA,EACA,OAAA,GAAuB,EAAC,EACP;AACjB,EAAA,MAAM,EAAE,QAAO,GAAI,QAAA;AACnB,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,EAAA,MAAM,WAAW,OAAA,CAAQ,QAAA,IAAY,CAAA,IAAA,EAAO,SAAS,IAAI,MAAM,CAAA,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,QAAQ,SAAA,GAAY,CAAA,EAAG,QAAQ,SAAS,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA,GAAK,QAAA;AAE1E,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,QAAA,CAAS,iBAAiB,MAAA,EAAQ;AACpC,IAAA,MAAA,GAAS,QAAA,CAAS,KAAA;AAAA,EACpB,CAAA,MAAA,IAAW,QAAA,CAAS,KAAA,YAAiB,UAAA,EAAY;AAC/C,IAAA,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,QAAA,CAAS,KAAK,CAAA;AAAA,EACrC,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,EACtC;AAEA,EAAA,MAAM,SAAA,CAAU,UAAU,MAAM,CAAA;AAChC,EAAA,OAAO,QAAA;AACT;AApBsB,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;ACTtB,SAAS,gBAAgB,KAAA,EAAiD;AACxE,EAAA,OACE,OAAO,KAAA,KAAU,QAAA,IACjB,KAAA,KAAU,IAAA,IACV,MAAA,CAAO,aAAA,IAAiB,KAAA,IACxB,OAAQ,KAAA,CAAiC,MAAA,CAAO,aAAa,CAAA,KAAM,UAAA;AAEvE;AAPS,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;AAYT,SAAS,iBAAiB,KAAA,EAAyC;AACjE,EAAA,OACE,OAAO,UAAU,QAAA,IACjB,KAAA,KAAU,QACV,YAAA,IAAgB,KAAA,IACf,MAAyB,UAAA,YAAsB,UAAA;AAEpD;AAPS,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA;AAYT,SAASC,mBAAkB,MAAA,EAAkC;AAC3D,EAAA,MAAM,WAAA,GAAc,OAAO,MAAA,CAAO,CAAC,KAAK,GAAA,KAAQ,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ,CAAC,CAAA;AACnE,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,WAAW,CAAA;AACzC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,KAAK,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,GAAA,CAAI,MAAA;AAAA,EAChB;AACA,EAAA,OAAO,MAAA;AACT;AATS,MAAA,CAAAA,kBAAAA,EAAA,mBAAA,CAAA;AAkBT,SAAS,gBACP,UAAA,EACA,UAAA,GAAa,MACb,QAAA,GAAW,CAAA,EACX,gBAAgB,EAAA,EACJ;AACZ,EAAA,MAAM,YAAA,GAAe,EAAA;AACrB,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,YAAY,CAAA;AAC1C,EAAA,MAAM,IAAA,GAAO,IAAI,QAAA,CAAS,MAAA,CAAO,MAAM,CAAA;AAGvC,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,EAAA,GAAK,UAAA,EAAY,IAAI,CAAA;AACvC,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,QAAA,CAAS,GAAG,EAAI,CAAA;AACrB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAI,CAAA;AAGtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,GAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,GAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,GAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAI,CAAA;AACtB,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,EAAA,EAAI,IAAI,CAAA;AAC3B,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,CAAA,EAAG,IAAI,CAAA;AAC1B,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,QAAA,EAAU,IAAI,CAAA;AACjC,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,UAAA,EAAY,IAAI,CAAA;AACnC,EAAA,IAAA,CAAK,UAAU,EAAA,EAAI,UAAA,GAAa,QAAA,IAAY,aAAA,GAAgB,IAAI,IAAI,CAAA;AACpE,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,QAAA,IAAY,aAAA,GAAgB,IAAI,IAAI,CAAA;AACvD,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,aAAA,EAAe,IAAI,CAAA;AAGtC,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,GAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,GAAI,CAAA;AACtB,EAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAI,CAAA;AACtB,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,UAAA,EAAY,IAAI,CAAA;AAEnC,EAAA,OAAO,MAAA;AACT;AA1CS,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;AAwDT,eAAsB,SAAA,CACpB,QAAA,EACA,MAAA,EACA,OAAA,EACe;AACf,EAAA,MAAM,SAAuB,EAAC;AAG9B,EAAA,IAAI,eAAA,CAAgB,MAAM,CAAA,EAAG;AAC3B,IAAA,WAAA,MAAiB,SAAS,MAAA,EAAQ;AAEhC,MAAA,IAAI,gBAAA,CAAiB,KAAK,CAAA,EAAG;AAC3B,QAAA,MAAA,CAAO,IAAA,CAAK,MAAM,UAAU,CAAA;AAAA,MAC9B,CAAA,MAAA,IAAW,iBAAiB,UAAA,EAAY;AACtC,QAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,MACnB;AAAA,IACF;AAAA,EACF,CAAA,MAAO;AACL,IAAA,MAAA,CAAO,IAAA,CAAK,GAAG,MAAM,CAAA;AAAA,EACvB;AAGA,EAAA,MAAM,KAAA,GAAQA,mBAAkB,MAAM,CAAA;AAGtC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,QAAQ,EAAE,WAAA,EAAY;AAC/C,EAAA,IAAI,QAAQ,MAAA,EAAQ;AAElB,IAAA,MAAM,eACJ,KAAA,CAAM,MAAA,IAAU,CAAA,IAChB,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA;AAAA,IACb,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA;AAAA,IACb,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA;AAAA,IACb,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA;AAEf,IAAA,IAAI,CAAC,YAAA,EAAc;AAEjB,MAAA,MAAM,UAAA,GAAa,SAAS,UAAA,IAAc,IAAA;AAC1C,MAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,KAAA,CAAM,MAAA,EAAQ,UAAU,CAAA;AAC1D,MAAA,MAAM,UAAU,IAAI,UAAA,CAAW,SAAA,CAAU,MAAA,GAAS,MAAM,MAAM,CAAA;AAC9D,MAAA,OAAA,CAAQ,IAAI,SAAS,CAAA;AACrB,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAA,EAAO,SAAA,CAAU,MAAM,CAAA;AACnC,MAAA,MAAMC,SAAAA,CAAU,UAAU,OAAO,CAAA;AACjC,MAAA;AAAA,IACF;AAAA,EACF;AAEA,EAAA,MAAMA,SAAAA,CAAU,UAAU,KAAK,CAAA;AACjC;AAhDsB,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;;;AC1FtB,eAAsB,QAAA,CACpB,QAAA,EACA,OAAA,GAAsB,EAAC,EACD;AACtB,EAAA,MAAM,KAAA,GAAQ,MAAM,YAAA,CAAa,QAAQ,CAAA;AAEzC,EAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,IAAA,MAAM,gBAAgB,EAAE,GAAG,UAAU,KAAA,EAAM,EAAG,QAAQ,IAAI,CAAA;AAAA,EAC5D;AAEA,EAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,IAAA,MAAM,UAAU,EAAE,GAAG,UAAU,KAAA,EAAM,EAAG,QAAQ,IAAI,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,EAAE,GAAG,QAAA,EAAU,KAAA,EAAM;AAC9B;AAfsB,MAAA,CAAA,QAAA,EAAA,UAAA,CAAA","file":"chunk-OZBT3ASY.js","sourcesContent":["import { Buffer } from 'node:buffer';\nimport type { TTSResponse } from '@/types/tts';\n\nexport interface CollectOptions {\n onChunk?: (chunk: Uint8Array) => void;\n onComplete?: (audio: Uint8Array) => void;\n onError?: (error: Error) => void;\n}\n\nexport async function collectAudio(\n response: TTSResponse,\n options: CollectOptions = {}\n): Promise<Uint8Array> {\n const { audio } = response;\n const chunks: Uint8Array[] = [];\n\n if (isUint8Array(audio)) {\n chunks.push(audio);\n } else if (isBuffer(audio)) {\n chunks.push(new Uint8Array(audio));\n }\n\n const result = concatUint8Arrays(chunks);\n\n if (options.onComplete) {\n options.onComplete(result);\n }\n\n return result;\n}\n\nfunction isUint8Array(value: unknown): value is Uint8Array {\n return value instanceof Uint8Array;\n}\n\nfunction isBuffer(value: unknown): value is Buffer {\n return Buffer.isBuffer(value);\n}\n\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n","/**\n * PCM → Opus 流式编码器\n *\n * 将 PCM 音频流流式编码为 Opus 数据包流(可选 OGG 容器封装)。\n * 依赖 prism-media(作为 optionalDependency),使用 libopus 原生编码能力。\n *\n * 核心机制:\n * 1. 从 TTS 流式拉取 PCM chunk(AsyncIterable<TTSStreamChunk> 或裸 Uint8Array 流)\n * 2. 帧缓冲区累积 PCM 数据,凑够一整帧后送入 Opus 编码器\n * 3. 编码器输出 Opus packet 后立即 yield 给消费者(真正的流式,不等待全部数据)\n * 4. 流结束时尾部不足一帧的数据用零填充\n *\n * @module pcm-to-opus\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createOggMuxerWithEos } from '@/asr/utils/ogg-muxer';\nimport type { TTSStreamChunk } from '@/types/tts';\n\n/** Opus 支持的帧时长(毫秒) */\nconst VALID_FRAME_DURATIONS_MS = [2.5, 5, 10, 20, 40, 60];\n\n/**\n * PCM → Opus 流式编码选项\n */\nexport interface PcmToOpusOptions {\n /** PCM 采样率(Hz),默认 24000(与 Doubao TTS PCM 输出一致) */\n sampleRate?: number;\n /** 声道数,默认 1(单声道) */\n channels?: number;\n /**\n * Opus 帧时长(毫秒),默认 60\n *\n * 可选值: 2.5, 5, 10, 20, 40, 60\n * - 20ms: 标准值,延迟与压缩率平衡\n * - 60ms: 高压缩率,适合 TTS 离线场景\n */\n frameDurationMs?: number;\n /** PCM 位深(bytes per sample),默认 2(16-bit) */\n bytesPerSample?: number;\n /**\n * 是否封装为 OGG 容器格式,默认 false\n *\n * 设为 true 时输出可直接播放的 OGG 流,\n * 内部复用 createOggMuxerWithEos 实现。\n */\n ogg?: boolean | { encoder?: string };\n}\n\n/**\n * 将 PCM 音频流流式编码为 Opus 数据包流\n *\n * @param pcmStream PCM 音频数据流\n * - AsyncIterable<TTSStreamChunk>: TTS speak() 的标准返回类型,自动提取 audioChunk\n * - AsyncIterable<Uint8Array> | AsyncIterable<Buffer>: 原始 PCM 字节流\n * @param options 编码选项\n * @returns Opus 数据包流(AsyncIterable<Buffer>)\n * - 裸 Opus 模式(默认): 每个 Buffer 是一个独立的 Opus 编码帧\n * - OGG 模式({ ogg: true }): 每个 Buffer 是一个 OGG 页面,可直接写入文件播放\n *\n * @example\n * ```typescript\n * import { createTTS, pcmToOpus } from 'univoice';\n *\n * const tts = createTTS({ provider: 'doubao', format: 'pcm', sampleRate: 24000 });\n * const pcmStream = tts.speak('你好世界', { stream: true });\n *\n * // 裸 Opus 包流\n * for await (const packet of pcmToOpus(pcmStream)) {\n * ws.send(packet);\n * }\n *\n * // OGG 封装流(可直接播放)\n * for await (const page of pcmToOpus(pcmStream, { ogg: true })) {\n * // 写入文件或发送到 WebSocket\n * }\n * ```\n */\nexport async function* pcmToOpus(\n pcmStream: AsyncIterable<TTSStreamChunk> | AsyncIterable<Uint8Array> | AsyncIterable<Buffer>,\n options?: PcmToOpusOptions\n): AsyncIterable<Buffer> {\n // ====== 参数解析与校验 ======\n const {\n sampleRate = 24000,\n channels = 1,\n frameDurationMs = 60,\n bytesPerSample = 2,\n ogg: oggEnabled,\n } = options || {};\n\n if (sampleRate <= 0) {\n throw new RangeError('sampleRate must be a positive number');\n }\n if (channels <= 0) {\n throw new RangeError('channels must be a positive number');\n }\n if (!VALID_FRAME_DURATIONS_MS.includes(frameDurationMs)) {\n throw new RangeError(`frameDurationMs must be one of: ${VALID_FRAME_DURATIONS_MS.join(', ')}`);\n }\n if (bytesPerSample !== 1 && bytesPerSample !== 2) {\n throw new RangeError('bytesPerSample must be 1 or 2');\n }\n\n const frameSizeSamples = Math.round((sampleRate / 1000) * frameDurationMs);\n const frameSizeBytes = frameSizeSamples * bytesPerSample;\n\n // ====== 动态导入 prism-media ======\n let prismMedia: typeof import('prism-media');\n try {\n prismMedia = await import('prism-media');\n } catch {\n throw new Error(\n 'prism-media is required for Opus encoding but is not installed. ' +\n 'Install it with: pnpm add prism-media'\n );\n }\n\n // ====== 创建 Opus 编码器(Transform Stream)======\n const encoder = new prismMedia.opus.Encoder({\n frameSize: frameSizeSamples,\n channels,\n rate: sampleRate,\n });\n\n // ====== 输出队列与状态 ======\n const outputQueue: Buffer[] = [];\n let encodeError: Error | null = null;\n let outputDone = false;\n\n // 条件变量:用于在输出队列为空且未完成时阻塞主循环,\n // 当有新数据入队或流结束时唤醒。\n // 无论裸 Opus 模式还是 OGG 模式,都通过此机制统一唤醒,避免事件丢失。\n let wakeUpResolver: (() => void) | null = null;\n\n function wakeUp(): void {\n if (wakeUpResolver) {\n const r = wakeUpResolver;\n wakeUpResolver = null;\n r();\n }\n }\n\n // ====== OGG 模式初始化 ======\n const useOgg = !!oggEnabled;\n const oggEncoderName = typeof oggEnabled === 'object' ? oggEnabled.encoder : undefined;\n\n let oggMuxer: ReturnType<typeof createOggMuxerWithEos> | null = null;\n let pushIterable: PushIterable<Buffer> | null = null;\n\n if (useOgg) {\n // 创建可手动推送的 AsyncIterable 适配器,将 encoder 的 data 事件桥接为 AsyncIterable\n pushIterable = new PushIterable<Buffer>();\n oggMuxer = createOggMuxerWithEos(pushIterable.asyncIterable, {\n sampleRate,\n channels,\n frameSizeMs: frameDurationMs,\n encoder: oggEncoderName ?? 'univoice',\n });\n // 从 OGG muxer 的流中拉取页面并送入输出队列\n (async () => {\n try {\n for await (const page of oggMuxer.stream) {\n outputQueue.push(page);\n wakeUp();\n }\n } catch (err) {\n if (!encodeError) {\n encodeError = err instanceof Error ? err : new Error(String(err));\n }\n } finally {\n outputDone = true;\n wakeUp();\n }\n })();\n }\n\n // 收集编码输出(裸 Opus 模式直接入队;OGG 模式通过 PushIterable 送入 muxer)\n encoder.on('data', (chunk: Buffer) => {\n if (useOgg && pushIterable) {\n pushIterable.push(chunk);\n } else {\n outputQueue.push(chunk);\n wakeUp();\n }\n });\n\n encoder.on('error', (err: Error) => {\n encodeError = err;\n if (!useOgg) {\n outputDone = true;\n }\n wakeUp();\n });\n\n // 裸 Opus 模式的结束信号\n if (!useOgg) {\n encoder.on('end', () => {\n outputDone = true;\n wakeUp();\n });\n }\n\n // ====== 后台任务:从 PCM 流拉取数据并送入编码器 ======\n const writePromise = (async () => {\n let pcmBuffer = Buffer.alloc(0);\n\n try {\n for await (const chunk of pcmStream) {\n if (encodeError) break;\n\n // 类型收窄:TTSStreamChunk 提取 audioChunk,其他直接使用\n const rawData = extractPcmData(chunk);\n const data = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);\n\n // 追加到帧缓冲区\n pcmBuffer = Buffer.concat([pcmBuffer, data]);\n\n // 凑够一帧就编码\n while (pcmBuffer.length >= frameSizeBytes) {\n const frame = pcmBuffer.subarray(0, frameSizeBytes);\n pcmBuffer = pcmBuffer.subarray(frameSizeBytes);\n encoder.write(frame);\n }\n }\n\n // 尾部不足一帧的数据用零填充(静音填充)\n if (pcmBuffer.length > 0) {\n const paddedFrame = Buffer.alloc(frameSizeBytes);\n pcmBuffer.copy(paddedFrame);\n encoder.write(paddedFrame);\n }\n } catch (err) {\n encodeError = err instanceof Error ? err : new Error(String(err));\n } finally {\n // 结束编码器(刷新内部缓冲区)\n encoder.end();\n\n // OGG 模式:通知 muxer 发送 EOS 页面\n if (useOgg && oggMuxer) {\n oggMuxer.finish();\n if (pushIterable) {\n pushIterable.finish();\n }\n }\n }\n })();\n\n // 等待后台任务启动\n await new Promise((resolve) => setImmediate(resolve));\n\n // ====== 主循环:从队列 yield 数据给消费者 ======\n while (!outputDone || outputQueue.length > 0) {\n if (encodeError) {\n // 等待后台任务完成以确保资源清理\n await writePromise.catch(() => {});\n throw encodeError;\n }\n\n if (outputQueue.length > 0) {\n const item = outputQueue.shift();\n if (item) yield item;\n } else if (!outputDone) {\n // 等待有新数据入队或流结束(通过 wakeUp 条件变量唤醒)\n await new Promise<void>((resolve) => {\n wakeUpResolver = resolve;\n });\n }\n }\n\n // 等待后台写入任务完成\n await writePromise;\n\n if (encodeError) {\n throw encodeError;\n }\n}\n\n// ==================== 内部工具 ====================\n\n/**\n * 从流式 chunk 中提取原始 PCM 数据\n *\n * @internal\n */\nfunction extractPcmData(chunk: TTSStreamChunk | Uint8Array | Buffer): Uint8Array | Buffer {\n if ('audioChunk' in chunk && chunk.audioChunk instanceof Uint8Array) {\n return chunk.audioChunk;\n }\n return chunk as Uint8Array | Buffer;\n}\n\n/**\n * 可手动推送数据的 AsyncIterable 适配器\n *\n * 用于将事件驱动(如 encoder.on('data'))的数据源转换为 AsyncIterable<Buffer>,\n * 以便接入 createOggMuxerWithEos 等 AsyncIterable 消费者。\n *\n * @internal\n */\nclass PushIterable<T> implements AsyncIterable<T> {\n private queue: T[] = [];\n private done = false;\n private error: Error | null = null;\n private pendingResolver: ((result: IteratorResult<T>) => void) | null = null;\n\n push(value: T): void {\n this.queue.push(value);\n if (this.pendingResolver) {\n const resolver = this.pendingResolver;\n this.pendingResolver = null;\n const shifted = this.queue.shift();\n resolver({ value: shifted as T, done: false });\n }\n }\n\n finish(err?: Error): void {\n if (err) {\n this.error = err;\n }\n this.done = true;\n if (this.pendingResolver) {\n const resolver = this.pendingResolver;\n this.pendingResolver = null;\n if (this.error) {\n resolver(Promise.reject(this.error) as unknown as IteratorResult<T>);\n } else {\n resolver({ value: undefined as T, done: true });\n }\n }\n }\n\n get asyncIterable(): AsyncIterable<T> {\n return this;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n return {\n next: (): Promise<IteratorResult<T>> => {\n if (this.queue.length > 0) {\n const shifted = this.queue.shift();\n return Promise.resolve({\n value: shifted as T,\n done: false,\n });\n }\n if (this.error) {\n const err = this.error;\n this.error = null;\n return Promise.reject(err);\n }\n if (this.done) {\n return Promise.resolve({\n value: undefined as T,\n done: true,\n });\n }\n return new Promise((resolve) => {\n this.pendingResolver = resolve;\n });\n },\n };\n }\n}\n","import { spawn } from 'node:child_process';\nimport type { TTSResponse } from '@/types/tts';\n\nexport interface PlayOptions {\n player?: string;\n}\n\nexport async function playAudio(response: TTSResponse, options: PlayOptions = {}): Promise<void> {\n const player = options.player || 'afplay';\n\n let buffer: Buffer;\n if (response.audio instanceof Buffer) {\n buffer = response.audio;\n } else if (response.audio instanceof Uint8Array) {\n buffer = Buffer.from(response.audio);\n } else {\n throw new Error('Invalid audio data');\n }\n\n return new Promise((resolve, reject) => {\n const proc = spawn(player, [], { stdio: ['pipe', 'inherit', 'inherit'] });\n proc.stdin.write(buffer);\n proc.stdin.end();\n proc.on('close', (code) => {\n if (code === 0) {\n resolve();\n } else {\n reject(new Error(`Player exited with code ${code}`));\n }\n });\n proc.on('error', reject);\n });\n}\n","import { writeFile } from 'node:fs/promises';\nimport type { TTSResponse } from '@/types/tts';\n\nexport interface SaveOptions {\n filename?: string;\n directory?: string;\n}\n\n/**\n * 保存 TTSResponse 到文件\n * 自动生成文件名,适合快速保存 TTS 响应\n *\n * @param response TTS 响应对象\n * @param options 保存选项\n * @returns 保存的文件路径\n */\nexport async function saveTTSResponse(\n response: TTSResponse,\n options: SaveOptions = {}\n): Promise<string> {\n const { format } = response;\n const timestamp = Date.now();\n const filename = options.filename || `tts_${timestamp}.${format}`;\n const filepath = options.directory ? `${options.directory}/${filename}` : filename;\n\n let buffer: Buffer;\n if (response.audio instanceof Buffer) {\n buffer = response.audio;\n } else if (response.audio instanceof Uint8Array) {\n buffer = Buffer.from(response.audio);\n } else {\n throw new Error('Invalid audio data');\n }\n\n await writeFile(filepath, buffer);\n return filepath;\n}\n","import { writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { TTSStreamChunk } from '@/types/tts';\n\n/**\n * 判断是否为异步迭代器\n */\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Symbol.asyncIterator in value &&\n typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'\n );\n}\n\n/**\n * 判断是否为 TTSStreamChunk 类型\n */\nfunction isTTSStreamChunk(value: unknown): value is TTSStreamChunk {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'audioChunk' in value &&\n (value as TTSStreamChunk).audioChunk instanceof Uint8Array\n );\n}\n\n/**\n * 合并多个 Uint8Array\n */\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * 创建 WAV 文件头\n * @param dataLength 音频数据长度\n * @param sampleRate 采样率(默认 24000)\n * @param channels 声道数(默认 1)\n * @param bitsPerSample 位深(默认 16)\n */\nfunction createWavHeader(\n dataLength: number,\n sampleRate = 24000,\n channels = 1,\n bitsPerSample = 16\n): Uint8Array {\n const headerLength = 44;\n const header = new Uint8Array(headerLength);\n const view = new DataView(header.buffer);\n\n // RIFF chunk descriptor\n view.setUint8(0, 0x52); // 'R'\n view.setUint8(1, 0x49); // 'I'\n view.setUint8(2, 0x46); // 'F'\n view.setUint8(3, 0x46); // 'F'\n view.setUint32(4, 36 + dataLength, true); // file size - 8\n view.setUint8(8, 0x57); // 'W'\n view.setUint8(9, 0x41); // 'A'\n view.setUint8(10, 0x56); // 'V'\n view.setUint8(11, 0x45); // 'E'\n\n // fmt sub-chunk\n view.setUint8(12, 0x66); // 'f'\n view.setUint8(13, 0x6d); // 'm'\n view.setUint8(14, 0x74); // 't'\n view.setUint8(15, 0x20); // ' '\n view.setUint32(16, 16, true); // sub-chunk size\n view.setUint16(20, 1, true); // audio format (PCM)\n view.setUint16(22, channels, true); // number of channels\n view.setUint32(24, sampleRate, true); // sample rate\n view.setUint32(28, sampleRate * channels * (bitsPerSample / 8), true); // byte rate\n view.setUint16(32, channels * (bitsPerSample / 8), true); // block align\n view.setUint16(34, bitsPerSample, true); // bits per sample\n\n // data sub-chunk\n view.setUint8(36, 0x64); // 'd'\n view.setUint8(37, 0x61); // 'a'\n view.setUint8(38, 0x74); // 't'\n view.setUint8(39, 0x61); // 'a'\n view.setUint32(40, dataLength, true); // data size\n\n return header;\n}\n\n/**\n * 保存音频数据到文件\n * 支持三种调用方式:\n * 1. saveAudio(filePath, chunks) - chunks 是 Uint8Array[]\n * 2. saveAudio(filePath, asyncIterable) - AsyncIterable<Uint8Array>\n * 3. saveAudio(filePath, asyncIterable) - AsyncIterable<TTSStreamChunk>\n *\n * @param filePath 目标文件路径\n * @param source 音频数据源,可以是 Uint8Array 数组或异步迭代器\n * @param options 可选参数\n * @param options.sampleRate 采样率,用于生成 WAV 头(默认 24000)\n */\nexport async function saveAudio(\n filePath: string,\n source: Uint8Array[] | AsyncIterable<Uint8Array> | AsyncIterable<TTSStreamChunk>,\n options?: { sampleRate?: number }\n): Promise<void> {\n const chunks: Uint8Array[] = [];\n\n // 判断是否为异步迭代器\n if (isAsyncIterable(source)) {\n for await (const chunk of source) {\n // 自动检测并提取 audioChunk\n if (isTTSStreamChunk(chunk)) {\n chunks.push(chunk.audioChunk);\n } else if (chunk instanceof Uint8Array) {\n chunks.push(chunk);\n }\n }\n } else {\n chunks.push(...source);\n }\n\n // 合并音频数据\n const audio = concatUint8Arrays(chunks);\n\n // 根据文件扩展名判断是否需要添加 WAV 头\n const ext = path.extname(filePath).toLowerCase();\n if (ext === '.wav') {\n // 检查是否已有 WAV 头(RIFF 标识)\n const hasWavHeader =\n audio.length >= 4 &&\n audio[0] === 0x52 && // 'R'\n audio[1] === 0x49 && // 'I'\n audio[2] === 0x46 && // 'F'\n audio[3] === 0x46; // 'F'\n\n if (!hasWavHeader) {\n // 添加 WAV 头\n const sampleRate = options?.sampleRate ?? 24000;\n const wavHeader = createWavHeader(audio.length, sampleRate);\n const wavData = new Uint8Array(wavHeader.length + audio.length);\n wavData.set(wavHeader);\n wavData.set(audio, wavHeader.length);\n await writeFile(filePath, wavData);\n return;\n }\n }\n\n await writeFile(filePath, audio);\n}\n","import { collectAudio } from '@/tts/utils/collect';\nimport { playAudio } from '@/tts/utils/play';\nimport { saveTTSResponse } from '@/tts/utils/save';\nimport type { TTSResponse } from '@/types/tts';\n\nexport interface TeeOptions {\n save?: {\n filename?: string;\n directory?: string;\n };\n play?: {\n player?: string;\n };\n}\n\nexport async function teeAudio(\n response: TTSResponse,\n options: TeeOptions = {}\n): Promise<TTSResponse> {\n const audio = await collectAudio(response);\n\n if (options.save) {\n await saveTTSResponse({ ...response, audio }, options.save);\n }\n\n if (options.play) {\n await playAudio({ ...response, audio }, options.play);\n }\n\n return { ...response, audio };\n}\n"]}
|