@zhin.js/speech 0.0.1
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/LICENSE +21 -0
- package/README.md +38 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -0
- package/lib/pipeline.d.ts +3 -0
- package/lib/pipeline.d.ts.map +1 -0
- package/lib/pipeline.js +53 -0
- package/lib/pipeline.js.map +1 -0
- package/lib/stt/ollama.d.ts +3 -0
- package/lib/stt/ollama.d.ts.map +1 -0
- package/lib/stt/ollama.js +24 -0
- package/lib/stt/ollama.js.map +1 -0
- package/lib/stt/openai.d.ts +3 -0
- package/lib/stt/openai.d.ts.map +1 -0
- package/lib/stt/openai.js +33 -0
- package/lib/stt/openai.js.map +1 -0
- package/lib/tts/azure.d.ts +4 -0
- package/lib/tts/azure.d.ts.map +1 -0
- package/lib/tts/azure.js +36 -0
- package/lib/tts/azure.js.map +1 -0
- package/lib/tts/custom.d.ts +3 -0
- package/lib/tts/custom.d.ts.map +1 -0
- package/lib/tts/custom.js +46 -0
- package/lib/tts/custom.js.map +1 -0
- package/lib/tts/edge.d.ts +3 -0
- package/lib/tts/edge.d.ts.map +1 -0
- package/lib/tts/edge.js +29 -0
- package/lib/tts/edge.js.map +1 -0
- package/lib/tts/index.d.ts +3 -0
- package/lib/tts/index.d.ts.map +1 -0
- package/lib/tts/index.js +20 -0
- package/lib/tts/index.js.map +1 -0
- package/lib/tts/openai.d.ts +3 -0
- package/lib/tts/openai.d.ts.map +1 -0
- package/lib/tts/openai.js +40 -0
- package/lib/tts/openai.js.map +1 -0
- package/lib/types.d.ts +60 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +18 -0
- package/src/pipeline.ts +67 -0
- package/src/stt/ollama.ts +32 -0
- package/src/stt/openai.ts +42 -0
- package/src/tts/azure.ts +42 -0
- package/src/tts/custom.ts +50 -0
- package/src/tts/edge.ts +34 -0
- package/src/tts/index.ts +22 -0
- package/src/tts/openai.ts +44 -0
- package/src/types.ts +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 凉菜
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @zhin.js/speech
|
|
2
|
+
|
|
3
|
+
Zhin.js 可选语音包:STT(Whisper)与 TTS(edge / OpenAI / Azure / 自定义 OpenAI 兼容 API)。
|
|
4
|
+
|
|
5
|
+
安装:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @zhin.js/speech
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
配置(`zhin.config.yml`):
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
speech:
|
|
15
|
+
stt:
|
|
16
|
+
provider: ollama
|
|
17
|
+
model: whisper
|
|
18
|
+
host: http://localhost:11434
|
|
19
|
+
tts:
|
|
20
|
+
provider: edge
|
|
21
|
+
voice: zh-CN-XiaoxiaoNeural
|
|
22
|
+
edgeTtsCommand: edge-tts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
由 `zhin.js` 启动时自动注册 `voice_stt` / `voice_tts` 工具与入站 STT(`ai.multimodal.audio.strategy: transcribe`)。
|
|
26
|
+
|
|
27
|
+
## TTS Providers
|
|
28
|
+
|
|
29
|
+
| provider | 说明 |
|
|
30
|
+
|----------|------|
|
|
31
|
+
| `edge`(默认) | 需 `pip install edge-tts` |
|
|
32
|
+
| `openai` | `POST /v1/audio/speech` |
|
|
33
|
+
| `azure` | Azure Cognitive Speech REST |
|
|
34
|
+
| `custom` | OpenAI 兼容 `baseUrl` |
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createSpeechPipeline } from './pipeline.js';
|
|
2
|
+
export { resolveTtsProvider } from './tts/index.js';
|
|
3
|
+
export type { SpeechConfig, SpeechLogger, SpeechPipeline, STTConfig, TTSConfig, TranscribeInput, TtsProvider, TtsProviderId, TtsSynthesizeInput, TtsSynthesizeResult, SttProviderId, } from './types.js';
|
|
4
|
+
/** 动态 import 时使用的包名(与 package.json name 一致) */
|
|
5
|
+
export declare const SPEECH_PACKAGE = "@zhin.js/speech";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,SAAS,EACT,SAAS,EACT,eAAe,EACf,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,+CAA+C;AAC/C,eAAO,MAAM,cAAc,oBAAoB,CAAC"}
|
package/lib/index.js
ADDED
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAepD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EAGf,MAAM,YAAY,CAAC;AA0BpB,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,YAAY,GACrB,cAAc,CA4BhB"}
|
package/lib/pipeline.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { transcribeWithOllama } from './stt/ollama.js';
|
|
2
|
+
import { transcribeWithOpenAI } from './stt/openai.js';
|
|
3
|
+
import { resolveTtsProvider } from './tts/index.js';
|
|
4
|
+
const DEFAULT_SPEECH_CONFIG = {
|
|
5
|
+
stt: {
|
|
6
|
+
enabled: true,
|
|
7
|
+
provider: 'ollama',
|
|
8
|
+
model: 'whisper',
|
|
9
|
+
host: 'http://localhost:11434',
|
|
10
|
+
},
|
|
11
|
+
tts: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
provider: 'edge',
|
|
14
|
+
voice: 'zh-CN-XiaoxiaoNeural',
|
|
15
|
+
rate: '+0%',
|
|
16
|
+
pitch: '+0Hz',
|
|
17
|
+
edgeTtsCommand: 'edge-tts',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
function mergeSpeechConfig(config) {
|
|
21
|
+
return {
|
|
22
|
+
stt: { ...DEFAULT_SPEECH_CONFIG.stt, ...config?.stt },
|
|
23
|
+
tts: { ...DEFAULT_SPEECH_CONFIG.tts, ...config?.tts },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function createSpeechPipeline(config, _logger) {
|
|
27
|
+
const merged = mergeSpeechConfig(config);
|
|
28
|
+
const sttConfig = merged.stt || {};
|
|
29
|
+
const defaultTts = resolveTtsProvider(merged);
|
|
30
|
+
return {
|
|
31
|
+
async transcribe(input) {
|
|
32
|
+
if (sttConfig.enabled === false) {
|
|
33
|
+
throw new Error('STT is disabled in speech config');
|
|
34
|
+
}
|
|
35
|
+
const provider = sttConfig.provider || 'ollama';
|
|
36
|
+
const mimeType = input.mimeType || 'audio/wav';
|
|
37
|
+
if (provider === 'openai') {
|
|
38
|
+
return transcribeWithOpenAI(input.data, sttConfig, mimeType);
|
|
39
|
+
}
|
|
40
|
+
return transcribeWithOllama(input.data, sttConfig, mimeType);
|
|
41
|
+
},
|
|
42
|
+
async synthesize(input) {
|
|
43
|
+
if (merged.tts?.enabled === false) {
|
|
44
|
+
throw new Error('TTS is disabled in speech config');
|
|
45
|
+
}
|
|
46
|
+
const provider = input.provider
|
|
47
|
+
? resolveTtsProvider(merged, input.provider)
|
|
48
|
+
: defaultTts;
|
|
49
|
+
return provider.synthesize(input);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AASpD,MAAM,qBAAqB,GAAiB;IAC1C,GAAG,EAAE;QACH,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,wBAAwB;KAC/B;IACD,GAAG,EAAE;QACH,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,sBAAsB;QAC7B,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,MAAM;QACb,cAAc,EAAE,UAAU;KAC3B;CACF,CAAC;AAEF,SAAS,iBAAiB,CAAC,MAAqB;IAC9C,OAAO;QACL,GAAG,EAAE,EAAE,GAAG,qBAAqB,CAAC,GAAG,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;QACrD,GAAG,EAAE,EAAE,GAAG,qBAAqB,CAAC,GAAG,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,MAAqB,EACrB,OAAsB;IAEtB,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACnC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE9C,OAAO;QACL,KAAK,CAAC,UAAU,CAAC,KAAsB;YACrC,IAAI,SAAS,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YACtD,CAAC;YACD,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,IAAI,QAAQ,CAAC;YAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,WAAW,CAAC;YAC/C,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBAC1B,OAAO,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC/D,CAAC;YACD,OAAO,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC/D,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,KAAyB;YACxC,IAAI,MAAM,CAAC,GAAG,EAAE,OAAO,KAAK,KAAK,EAAE,CAAC;gBAClC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YACtD,CAAC;YACD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ;gBAC7B,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;gBAC5C,CAAC,CAAC,UAAU,CAAC;YACf,OAAO,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.d.ts","sourceRoot":"","sources":["../../src/stt/ollama.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,EACpB,SAAS,GAAE,MAAoB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAyBjB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function transcribeWithOllama(audioData, sttConfig, _mimeType = 'audio/wav') {
|
|
2
|
+
const host = sttConfig.host || 'http://localhost:11434';
|
|
3
|
+
const model = sttConfig.model || 'whisper';
|
|
4
|
+
const base64Audio = audioData.toString('base64');
|
|
5
|
+
const response = await fetch(`${host}/api/chat`, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
model,
|
|
10
|
+
messages: [{
|
|
11
|
+
role: 'user',
|
|
12
|
+
content: '请将这段音频转写为文字,只输出转写的文字内容,不要加任何说明。',
|
|
13
|
+
images: [base64Audio],
|
|
14
|
+
}],
|
|
15
|
+
stream: false,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Ollama STT failed: ${response.status} ${response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
return data.message?.content?.trim() || '';
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/stt/ollama.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAiB,EACjB,SAAoB,EACpB,YAAoB,WAAW;IAE/B,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,IAAI,wBAAwB,CAAC;IACxD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC;IAC3C,MAAM,WAAW,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,WAAW,EAAE;QAC/C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK;YACL,QAAQ,EAAE,CAAC;oBACT,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE,iCAAiC;oBAC1C,MAAM,EAAE,CAAC,WAAW,CAAC;iBACtB,CAAC;YACF,MAAM,EAAE,KAAK;SACd,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAwC,CAAC;IACzE,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/stt/openai.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,EACpB,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA+BjB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
export async function transcribeWithOpenAI(audioData, sttConfig, mimeType = 'audio/wav') {
|
|
6
|
+
const host = sttConfig.host || 'https://api.openai.com';
|
|
7
|
+
const model = sttConfig.model || 'whisper-1';
|
|
8
|
+
const apiKey = sttConfig.apiKey || '';
|
|
9
|
+
const tmpFile = join(tmpdir(), `zhin-stt-${randomBytes(8).toString('hex')}.wav`);
|
|
10
|
+
await writeFile(tmpFile, audioData);
|
|
11
|
+
try {
|
|
12
|
+
const ext = mimeType.includes('mp3') ? 'mp3' : 'wav';
|
|
13
|
+
const formData = new FormData();
|
|
14
|
+
const blob = new Blob([new Uint8Array(audioData)], { type: mimeType });
|
|
15
|
+
formData.append('file', blob, `audio.${ext}`);
|
|
16
|
+
formData.append('model', model);
|
|
17
|
+
formData.append('language', 'zh');
|
|
18
|
+
const response = await fetch(`${host}/v1/audio/transcriptions`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
21
|
+
body: formData,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`OpenAI STT failed: ${response.status} ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return data.text?.trim() || '';
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await unlink(tmpFile).catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/stt/openai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGrD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAiB,EACjB,SAAoB,EACpB,WAAmB,WAAW;IAE9B,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,IAAI,wBAAwB,CAAC;IACxD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,WAAW,CAAC;IAC7C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC;IAEtC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACjF,MAAM,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAEpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACvE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,CAAC,CAAC;QAC9C,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAChC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAElC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,0BAA0B,EAAE;YAC9D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE;YAC9C,IAAI,EAAE,QAAQ;SACf,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QAClF,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAuB,CAAC;QACxD,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TTSConfig, TtsProvider } from '../types.js';
|
|
2
|
+
/** Azure Cognitive Services text-to-speech REST (output format: audio-16khz-128kbitrate-mono-mp3) */
|
|
3
|
+
export declare function createAzureTtsProvider(config: TTSConfig): TtsProvider;
|
|
4
|
+
//# sourceMappingURL=azure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.d.ts","sourceRoot":"","sources":["../../src/tts/azure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAA2C,MAAM,aAAa,CAAC;AAEnG,qGAAqG;AACrG,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,CA6BrE"}
|
package/lib/tts/azure.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Azure Cognitive Services text-to-speech REST (output format: audio-16khz-128kbitrate-mono-mp3) */
|
|
2
|
+
export function createAzureTtsProvider(config) {
|
|
3
|
+
return {
|
|
4
|
+
id: 'azure',
|
|
5
|
+
async synthesize(input) {
|
|
6
|
+
const region = config.region || 'eastasia';
|
|
7
|
+
const subscriptionKey = config.subscriptionKey || '';
|
|
8
|
+
const voice = input.voice || config.voice || 'zh-CN-XiaoxiaoNeural';
|
|
9
|
+
const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`;
|
|
10
|
+
const ssml = `<speak version='1.0' xml:lang='zh-CN'><voice name='${voice}'>${escapeXml(input.text)}</voice></speak>`;
|
|
11
|
+
const response = await fetch(url, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Ocp-Apim-Subscription-Key': subscriptionKey,
|
|
15
|
+
'Content-Type': 'application/ssml+xml',
|
|
16
|
+
'X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3',
|
|
17
|
+
},
|
|
18
|
+
body: ssml,
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Azure TTS failed: ${response.status} ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
24
|
+
return { data, format: 'mp3' };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function escapeXml(text) {
|
|
29
|
+
return text
|
|
30
|
+
.replace(/&/g, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>')
|
|
33
|
+
.replace(/"/g, '"')
|
|
34
|
+
.replace(/'/g, ''');
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=azure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.js","sourceRoot":"","sources":["../../src/tts/azure.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,MAAM,UAAU,sBAAsB,CAAC,MAAiB;IACtD,OAAO;QACL,EAAE,EAAE,OAAO;QACX,KAAK,CAAC,UAAU,CAAC,KAAyB;YACxC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,UAAU,CAAC;YAC3C,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,IAAI,sBAAsB,CAAC;YACpE,MAAM,GAAG,GAAG,WAAW,MAAM,gDAAgD,CAAC;YAE9E,MAAM,IAAI,GAAG,sDAAsD,KAAK,KAAK,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC;YAErH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,2BAA2B,EAAE,eAAe;oBAC5C,cAAc,EAAE,sBAAsB;oBACtC,0BAA0B,EAAE,kCAAkC;iBAC/D;gBACD,IAAI,EAAE,IAAI;aACX,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YACjF,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACjC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/tts/custom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAa,WAAW,EAA2C,MAAM,aAAa,CAAC;AAMjH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,CA2CzE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function resolveOpenAiApiKey(config) {
|
|
2
|
+
return config.tts?.apiKey || config.stt?.apiKey || '';
|
|
3
|
+
}
|
|
4
|
+
export function createCustomTtsProvider(config) {
|
|
5
|
+
const ttsConfig = config.tts || {};
|
|
6
|
+
return {
|
|
7
|
+
id: 'custom',
|
|
8
|
+
async synthesize(input) {
|
|
9
|
+
const baseUrl = (ttsConfig.baseUrl || '').replace(/\/$/, '');
|
|
10
|
+
if (!baseUrl) {
|
|
11
|
+
throw new Error('custom TTS requires speech.tts.baseUrl');
|
|
12
|
+
}
|
|
13
|
+
const model = ttsConfig.model || 'tts-1';
|
|
14
|
+
const voice = input.voice || ttsConfig.voice || 'default';
|
|
15
|
+
const apiKey = resolveOpenAiApiKey(config);
|
|
16
|
+
const format = input.format === 'wav' ? 'wav' : 'mp3';
|
|
17
|
+
const headers = {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
...(ttsConfig.headers || {}),
|
|
20
|
+
};
|
|
21
|
+
if (apiKey)
|
|
22
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
23
|
+
const body = {
|
|
24
|
+
model,
|
|
25
|
+
input: input.text,
|
|
26
|
+
voice,
|
|
27
|
+
response_format: format,
|
|
28
|
+
};
|
|
29
|
+
if (input.speed != null)
|
|
30
|
+
body.speed = input.speed;
|
|
31
|
+
else if (ttsConfig.speed != null)
|
|
32
|
+
body.speed = ttsConfig.speed;
|
|
33
|
+
const response = await fetch(baseUrl, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers,
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Custom TTS failed: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
42
|
+
return { data, format };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=custom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.js","sourceRoot":"","sources":["../../src/tts/custom.ts"],"names":[],"mappings":"AAEA,SAAS,mBAAmB,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAC,GAAG,EAAE,MAAM,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAoB;IAC1D,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACnC,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,KAAK,CAAC,UAAU,CAAC,KAAyB;YACxC,MAAM,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC7D,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;YAC5D,CAAC;YACD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC;YACzC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC;YAC1D,MAAM,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;YAEtD,MAAM,OAAO,GAA2B;gBACtC,cAAc,EAAE,kBAAkB;gBAClC,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC;aAC7B,CAAC;YACF,IAAI,MAAM;gBAAE,OAAO,CAAC,aAAa,GAAG,UAAU,MAAM,EAAE,CAAC;YAEvD,MAAM,IAAI,GAA4B;gBACpC,KAAK;gBACL,KAAK,EAAE,KAAK,CAAC,IAAI;gBACjB,KAAK;gBACL,eAAe,EAAE,MAAM;aACxB,CAAC;YACF,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI;gBAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;iBAC7C,IAAI,SAAS,CAAC,KAAK,IAAI,IAAI;gBAAE,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;YAE/D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE;gBACpC,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YAClF,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../src/tts/edge.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAA2C,MAAM,aAAa,CAAC;AAInG,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,CAuBpE"}
|
package/lib/tts/edge.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export function createEdgeTtsProvider(config) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'edge',
|
|
11
|
+
async synthesize(input) {
|
|
12
|
+
const voice = input.voice || config.voice || 'zh-CN-XiaoxiaoNeural';
|
|
13
|
+
const rate = config.rate || '+0%';
|
|
14
|
+
const pitch = config.pitch || '+0Hz';
|
|
15
|
+
const cmd = config.edgeTtsCommand || 'edge-tts';
|
|
16
|
+
const tmpFile = join(tmpdir(), `zhin-tts-${randomBytes(8).toString('hex')}.mp3`);
|
|
17
|
+
const safeText = input.text.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
18
|
+
try {
|
|
19
|
+
await execAsync(`${cmd} --voice "${voice}" --rate="${rate}" --pitch="${pitch}" --text "${safeText}" --write-media "${tmpFile}"`, { timeout: 30_000 });
|
|
20
|
+
const data = await readFile(tmpFile);
|
|
21
|
+
return { data, format: 'mp3' };
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await unlink(tmpFile).catch(() => { });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=edge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge.js","sourceRoot":"","sources":["../../src/tts/edge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,OAAO;QACL,EAAE,EAAE,MAAM;QACV,KAAK,CAAC,UAAU,CAAC,KAAyB;YACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,IAAI,sBAAsB,CAAC;YACpE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC;YAClC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC;YACrC,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,IAAI,UAAU,CAAC;YAChD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACjF,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAEtE,IAAI,CAAC;gBACH,MAAM,SAAS,CACb,GAAG,GAAG,aAAa,KAAK,aAAa,IAAI,cAAc,KAAK,aAAa,QAAQ,oBAAoB,OAAO,GAAG,EAC/G,EAAE,OAAO,EAAE,MAAM,EAAE,CACpB,CAAC;gBACF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACrC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;YACjC,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,aAAa,GAAG,WAAW,CAe9F"}
|
package/lib/tts/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createAzureTtsProvider } from './azure.js';
|
|
2
|
+
import { createCustomTtsProvider } from './custom.js';
|
|
3
|
+
import { createEdgeTtsProvider } from './edge.js';
|
|
4
|
+
import { createOpenAiTtsProvider } from './openai.js';
|
|
5
|
+
export function resolveTtsProvider(config, override) {
|
|
6
|
+
const ttsConfig = config.tts || {};
|
|
7
|
+
const id = override || ttsConfig.provider || 'edge';
|
|
8
|
+
switch (id) {
|
|
9
|
+
case 'openai':
|
|
10
|
+
return createOpenAiTtsProvider(config);
|
|
11
|
+
case 'azure':
|
|
12
|
+
return createAzureTtsProvider(ttsConfig);
|
|
13
|
+
case 'custom':
|
|
14
|
+
return createCustomTtsProvider(config);
|
|
15
|
+
case 'edge':
|
|
16
|
+
default:
|
|
17
|
+
return createEdgeTtsProvider(ttsConfig);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tts/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,UAAU,kBAAkB,CAAC,MAAoB,EAAE,QAAwB;IAC/E,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACnC,MAAM,EAAE,GAAG,QAAQ,IAAI,SAAS,CAAC,QAAQ,IAAI,MAAM,CAAC;IAEpD,QAAQ,EAAE,EAAE,CAAC;QACX,KAAK,QAAQ;YACX,OAAO,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACzC,KAAK,OAAO;YACV,OAAO,sBAAsB,CAAC,SAAS,CAAC,CAAC;QAC3C,KAAK,QAAQ;YACX,OAAO,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACzC,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/tts/openai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAa,WAAW,EAA2C,MAAM,aAAa,CAAC;AAMjH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,CAqCzE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function resolveOpenAiApiKey(config) {
|
|
2
|
+
return config.tts?.apiKey || config.stt?.apiKey || '';
|
|
3
|
+
}
|
|
4
|
+
export function createOpenAiTtsProvider(config) {
|
|
5
|
+
const ttsConfig = config.tts || {};
|
|
6
|
+
return {
|
|
7
|
+
id: 'openai',
|
|
8
|
+
async synthesize(input) {
|
|
9
|
+
const host = (ttsConfig.host || 'https://api.openai.com').replace(/\/$/, '');
|
|
10
|
+
const model = ttsConfig.model || 'tts-1';
|
|
11
|
+
const voice = input.voice || ttsConfig.voice || 'alloy';
|
|
12
|
+
const apiKey = resolveOpenAiApiKey(config);
|
|
13
|
+
const format = input.format === 'wav' ? 'wav' : 'mp3';
|
|
14
|
+
const body = {
|
|
15
|
+
model,
|
|
16
|
+
input: input.text,
|
|
17
|
+
voice,
|
|
18
|
+
response_format: format,
|
|
19
|
+
};
|
|
20
|
+
if (input.speed != null)
|
|
21
|
+
body.speed = input.speed;
|
|
22
|
+
else if (ttsConfig.speed != null)
|
|
23
|
+
body.speed = ttsConfig.speed;
|
|
24
|
+
const response = await fetch(`${host}/v1/audio/speech`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${apiKey}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`OpenAI TTS failed: ${response.status} ${response.statusText}`);
|
|
34
|
+
}
|
|
35
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
36
|
+
return { data, format };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/tts/openai.ts"],"names":[],"mappings":"AAEA,SAAS,mBAAmB,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAC,GAAG,EAAE,MAAM,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAoB;IAC1D,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACnC,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,KAAK,CAAC,UAAU,CAAC,KAAyB;YACxC,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,wBAAwB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC;YACzC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC;YACxD,MAAM,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;YAEtD,MAAM,IAAI,GAA4B;gBACpC,KAAK;gBACL,KAAK,EAAE,KAAK,CAAC,IAAI;gBACjB,KAAK;gBACL,eAAe,EAAE,MAAM;aACxB,CAAC;YACF,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI;gBAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;iBAC7C,IAAI,SAAS,CAAC,KAAK,IAAI,IAAI;gBAAE,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;YAE/D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,kBAAkB,EAAE;gBACtD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,MAAM,EAAE;oBACjC,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YAClF,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type SttProviderId = 'ollama' | 'openai';
|
|
2
|
+
export type TtsProviderId = 'edge' | 'openai' | 'azure' | 'custom';
|
|
3
|
+
export interface STTConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
provider?: SttProviderId;
|
|
6
|
+
model?: string;
|
|
7
|
+
host?: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TTSConfig {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
provider?: TtsProviderId;
|
|
13
|
+
voice?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
host?: string;
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
/** edge-tts */
|
|
18
|
+
rate?: string;
|
|
19
|
+
pitch?: string;
|
|
20
|
+
edgeTtsCommand?: string;
|
|
21
|
+
/** azure */
|
|
22
|
+
region?: string;
|
|
23
|
+
subscriptionKey?: string;
|
|
24
|
+
/** custom OpenAI-compatible */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
speed?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface SpeechConfig {
|
|
30
|
+
stt?: STTConfig;
|
|
31
|
+
tts?: TTSConfig;
|
|
32
|
+
}
|
|
33
|
+
export interface TtsSynthesizeInput {
|
|
34
|
+
text: string;
|
|
35
|
+
voice?: string;
|
|
36
|
+
format?: 'mp3' | 'wav';
|
|
37
|
+
speed?: number;
|
|
38
|
+
provider?: TtsProviderId;
|
|
39
|
+
}
|
|
40
|
+
export interface TtsSynthesizeResult {
|
|
41
|
+
data: Buffer;
|
|
42
|
+
format: 'mp3' | 'wav';
|
|
43
|
+
}
|
|
44
|
+
export interface TtsProvider {
|
|
45
|
+
readonly id: TtsProviderId;
|
|
46
|
+
synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult>;
|
|
47
|
+
}
|
|
48
|
+
export interface TranscribeInput {
|
|
49
|
+
data: Buffer;
|
|
50
|
+
mimeType?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface SpeechPipeline {
|
|
53
|
+
transcribe(input: TranscribeInput): Promise<string>;
|
|
54
|
+
synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult>;
|
|
55
|
+
}
|
|
56
|
+
export interface SpeechLogger {
|
|
57
|
+
debug?(message: string, ...args: unknown[]): void;
|
|
58
|
+
warn?(message: string, ...args: unknown[]): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAChD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IAC3B,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAClD,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD"}
|
package/lib/types.js
ADDED
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/speech",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "STT/TTS for Zhin.js (Whisper + edge/openai/azure/custom TTS); optional peer of zhin.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"development": "./src/index.ts",
|
|
12
|
+
"import": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"lib",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"zhin",
|
|
23
|
+
"zhin.js",
|
|
24
|
+
"speech",
|
|
25
|
+
"stt",
|
|
26
|
+
"tts",
|
|
27
|
+
"whisper",
|
|
28
|
+
"edge-tts"
|
|
29
|
+
],
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "lc-cn",
|
|
32
|
+
"email": "admin@liucl.cn",
|
|
33
|
+
"url": "https://github.com/lc-cn"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@zhin.js/core": "1.3.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"@zhin.js/core": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"@zhin.js/core": "1.3.1"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/zhinjs/zhin.git",
|
|
51
|
+
"directory": "packages/toolkit/speech"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"registry": "https://registry.npmjs.org"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc --build",
|
|
59
|
+
"clean": "rimraf lib",
|
|
60
|
+
"test": "vitest run"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { createSpeechPipeline } from './pipeline.js';
|
|
2
|
+
export { resolveTtsProvider } from './tts/index.js';
|
|
3
|
+
export type {
|
|
4
|
+
SpeechConfig,
|
|
5
|
+
SpeechLogger,
|
|
6
|
+
SpeechPipeline,
|
|
7
|
+
STTConfig,
|
|
8
|
+
TTSConfig,
|
|
9
|
+
TranscribeInput,
|
|
10
|
+
TtsProvider,
|
|
11
|
+
TtsProviderId,
|
|
12
|
+
TtsSynthesizeInput,
|
|
13
|
+
TtsSynthesizeResult,
|
|
14
|
+
SttProviderId,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/** 动态 import 时使用的包名(与 package.json name 一致) */
|
|
18
|
+
export const SPEECH_PACKAGE = '@zhin.js/speech';
|
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { transcribeWithOllama } from './stt/ollama.js';
|
|
2
|
+
import { transcribeWithOpenAI } from './stt/openai.js';
|
|
3
|
+
import { resolveTtsProvider } from './tts/index.js';
|
|
4
|
+
import type {
|
|
5
|
+
SpeechConfig,
|
|
6
|
+
SpeechLogger,
|
|
7
|
+
SpeechPipeline,
|
|
8
|
+
TranscribeInput,
|
|
9
|
+
TtsSynthesizeInput,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SPEECH_CONFIG: SpeechConfig = {
|
|
13
|
+
stt: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
provider: 'ollama',
|
|
16
|
+
model: 'whisper',
|
|
17
|
+
host: 'http://localhost:11434',
|
|
18
|
+
},
|
|
19
|
+
tts: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
provider: 'edge',
|
|
22
|
+
voice: 'zh-CN-XiaoxiaoNeural',
|
|
23
|
+
rate: '+0%',
|
|
24
|
+
pitch: '+0Hz',
|
|
25
|
+
edgeTtsCommand: 'edge-tts',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function mergeSpeechConfig(config?: SpeechConfig): SpeechConfig {
|
|
30
|
+
return {
|
|
31
|
+
stt: { ...DEFAULT_SPEECH_CONFIG.stt, ...config?.stt },
|
|
32
|
+
tts: { ...DEFAULT_SPEECH_CONFIG.tts, ...config?.tts },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSpeechPipeline(
|
|
37
|
+
config?: SpeechConfig,
|
|
38
|
+
_logger?: SpeechLogger,
|
|
39
|
+
): SpeechPipeline {
|
|
40
|
+
const merged = mergeSpeechConfig(config);
|
|
41
|
+
const sttConfig = merged.stt || {};
|
|
42
|
+
const defaultTts = resolveTtsProvider(merged);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
async transcribe(input: TranscribeInput): Promise<string> {
|
|
46
|
+
if (sttConfig.enabled === false) {
|
|
47
|
+
throw new Error('STT is disabled in speech config');
|
|
48
|
+
}
|
|
49
|
+
const provider = sttConfig.provider || 'ollama';
|
|
50
|
+
const mimeType = input.mimeType || 'audio/wav';
|
|
51
|
+
if (provider === 'openai') {
|
|
52
|
+
return transcribeWithOpenAI(input.data, sttConfig, mimeType);
|
|
53
|
+
}
|
|
54
|
+
return transcribeWithOllama(input.data, sttConfig, mimeType);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async synthesize(input: TtsSynthesizeInput) {
|
|
58
|
+
if (merged.tts?.enabled === false) {
|
|
59
|
+
throw new Error('TTS is disabled in speech config');
|
|
60
|
+
}
|
|
61
|
+
const provider = input.provider
|
|
62
|
+
? resolveTtsProvider(merged, input.provider)
|
|
63
|
+
: defaultTts;
|
|
64
|
+
return provider.synthesize(input);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { STTConfig } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export async function transcribeWithOllama(
|
|
4
|
+
audioData: Buffer,
|
|
5
|
+
sttConfig: STTConfig,
|
|
6
|
+
_mimeType: string = 'audio/wav',
|
|
7
|
+
): Promise<string> {
|
|
8
|
+
const host = sttConfig.host || 'http://localhost:11434';
|
|
9
|
+
const model = sttConfig.model || 'whisper';
|
|
10
|
+
const base64Audio = audioData.toString('base64');
|
|
11
|
+
|
|
12
|
+
const response = await fetch(`${host}/api/chat`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model,
|
|
17
|
+
messages: [{
|
|
18
|
+
role: 'user',
|
|
19
|
+
content: '请将这段音频转写为文字,只输出转写的文字内容,不要加任何说明。',
|
|
20
|
+
images: [base64Audio],
|
|
21
|
+
}],
|
|
22
|
+
stream: false,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Ollama STT failed: ${response.status} ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = await response.json() as { message?: { content?: string } };
|
|
31
|
+
return data.message?.content?.trim() || '';
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import type { STTConfig } from '../types.js';
|
|
6
|
+
|
|
7
|
+
export async function transcribeWithOpenAI(
|
|
8
|
+
audioData: Buffer,
|
|
9
|
+
sttConfig: STTConfig,
|
|
10
|
+
mimeType: string = 'audio/wav',
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
const host = sttConfig.host || 'https://api.openai.com';
|
|
13
|
+
const model = sttConfig.model || 'whisper-1';
|
|
14
|
+
const apiKey = sttConfig.apiKey || '';
|
|
15
|
+
|
|
16
|
+
const tmpFile = join(tmpdir(), `zhin-stt-${randomBytes(8).toString('hex')}.wav`);
|
|
17
|
+
await writeFile(tmpFile, audioData);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const ext = mimeType.includes('mp3') ? 'mp3' : 'wav';
|
|
21
|
+
const formData = new FormData();
|
|
22
|
+
const blob = new Blob([new Uint8Array(audioData)], { type: mimeType });
|
|
23
|
+
formData.append('file', blob, `audio.${ext}`);
|
|
24
|
+
formData.append('model', model);
|
|
25
|
+
formData.append('language', 'zh');
|
|
26
|
+
|
|
27
|
+
const response = await fetch(`${host}/v1/audio/transcriptions`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
30
|
+
body: formData,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`OpenAI STT failed: ${response.status} ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const data = await response.json() as { text?: string };
|
|
38
|
+
return data.text?.trim() || '';
|
|
39
|
+
} finally {
|
|
40
|
+
await unlink(tmpFile).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/tts/azure.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TTSConfig, TtsProvider, TtsSynthesizeInput, TtsSynthesizeResult } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Azure Cognitive Services text-to-speech REST (output format: audio-16khz-128kbitrate-mono-mp3) */
|
|
4
|
+
export function createAzureTtsProvider(config: TTSConfig): TtsProvider {
|
|
5
|
+
return {
|
|
6
|
+
id: 'azure',
|
|
7
|
+
async synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult> {
|
|
8
|
+
const region = config.region || 'eastasia';
|
|
9
|
+
const subscriptionKey = config.subscriptionKey || '';
|
|
10
|
+
const voice = input.voice || config.voice || 'zh-CN-XiaoxiaoNeural';
|
|
11
|
+
const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`;
|
|
12
|
+
|
|
13
|
+
const ssml = `<speak version='1.0' xml:lang='zh-CN'><voice name='${voice}'>${escapeXml(input.text)}</voice></speak>`;
|
|
14
|
+
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Ocp-Apim-Subscription-Key': subscriptionKey,
|
|
19
|
+
'Content-Type': 'application/ssml+xml',
|
|
20
|
+
'X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3',
|
|
21
|
+
},
|
|
22
|
+
body: ssml,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Azure TTS failed: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
30
|
+
return { data, format: 'mp3' };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function escapeXml(text: string): string {
|
|
36
|
+
return text
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"')
|
|
41
|
+
.replace(/'/g, ''');
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SpeechConfig, TTSConfig, TtsProvider, TtsSynthesizeInput, TtsSynthesizeResult } from '../types.js';
|
|
2
|
+
|
|
3
|
+
function resolveOpenAiApiKey(config: SpeechConfig): string {
|
|
4
|
+
return config.tts?.apiKey || config.stt?.apiKey || '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createCustomTtsProvider(config: SpeechConfig): TtsProvider {
|
|
8
|
+
const ttsConfig = config.tts || {};
|
|
9
|
+
return {
|
|
10
|
+
id: 'custom',
|
|
11
|
+
async synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult> {
|
|
12
|
+
const baseUrl = (ttsConfig.baseUrl || '').replace(/\/$/, '');
|
|
13
|
+
if (!baseUrl) {
|
|
14
|
+
throw new Error('custom TTS requires speech.tts.baseUrl');
|
|
15
|
+
}
|
|
16
|
+
const model = ttsConfig.model || 'tts-1';
|
|
17
|
+
const voice = input.voice || ttsConfig.voice || 'default';
|
|
18
|
+
const apiKey = resolveOpenAiApiKey(config);
|
|
19
|
+
const format = input.format === 'wav' ? 'wav' : 'mp3';
|
|
20
|
+
|
|
21
|
+
const headers: Record<string, string> = {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
...(ttsConfig.headers || {}),
|
|
24
|
+
};
|
|
25
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
26
|
+
|
|
27
|
+
const body: Record<string, unknown> = {
|
|
28
|
+
model,
|
|
29
|
+
input: input.text,
|
|
30
|
+
voice,
|
|
31
|
+
response_format: format,
|
|
32
|
+
};
|
|
33
|
+
if (input.speed != null) body.speed = input.speed;
|
|
34
|
+
else if (ttsConfig.speed != null) body.speed = ttsConfig.speed;
|
|
35
|
+
|
|
36
|
+
const response = await fetch(baseUrl, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`Custom TTS failed: ${response.status} ${response.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
47
|
+
return { data, format };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/tts/edge.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import type { TTSConfig, TtsProvider, TtsSynthesizeInput, TtsSynthesizeResult } from '../types.js';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
export function createEdgeTtsProvider(config: TTSConfig): TtsProvider {
|
|
12
|
+
return {
|
|
13
|
+
id: 'edge',
|
|
14
|
+
async synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult> {
|
|
15
|
+
const voice = input.voice || config.voice || 'zh-CN-XiaoxiaoNeural';
|
|
16
|
+
const rate = config.rate || '+0%';
|
|
17
|
+
const pitch = config.pitch || '+0Hz';
|
|
18
|
+
const cmd = config.edgeTtsCommand || 'edge-tts';
|
|
19
|
+
const tmpFile = join(tmpdir(), `zhin-tts-${randomBytes(8).toString('hex')}.mp3`);
|
|
20
|
+
const safeText = input.text.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await execAsync(
|
|
24
|
+
`${cmd} --voice "${voice}" --rate="${rate}" --pitch="${pitch}" --text "${safeText}" --write-media "${tmpFile}"`,
|
|
25
|
+
{ timeout: 30_000 },
|
|
26
|
+
);
|
|
27
|
+
const data = await readFile(tmpFile);
|
|
28
|
+
return { data, format: 'mp3' };
|
|
29
|
+
} finally {
|
|
30
|
+
await unlink(tmpFile).catch(() => {});
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/tts/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SpeechConfig, TtsProvider, TtsProviderId } from '../types.js';
|
|
2
|
+
import { createAzureTtsProvider } from './azure.js';
|
|
3
|
+
import { createCustomTtsProvider } from './custom.js';
|
|
4
|
+
import { createEdgeTtsProvider } from './edge.js';
|
|
5
|
+
import { createOpenAiTtsProvider } from './openai.js';
|
|
6
|
+
|
|
7
|
+
export function resolveTtsProvider(config: SpeechConfig, override?: TtsProviderId): TtsProvider {
|
|
8
|
+
const ttsConfig = config.tts || {};
|
|
9
|
+
const id = override || ttsConfig.provider || 'edge';
|
|
10
|
+
|
|
11
|
+
switch (id) {
|
|
12
|
+
case 'openai':
|
|
13
|
+
return createOpenAiTtsProvider(config);
|
|
14
|
+
case 'azure':
|
|
15
|
+
return createAzureTtsProvider(ttsConfig);
|
|
16
|
+
case 'custom':
|
|
17
|
+
return createCustomTtsProvider(config);
|
|
18
|
+
case 'edge':
|
|
19
|
+
default:
|
|
20
|
+
return createEdgeTtsProvider(ttsConfig);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SpeechConfig, TTSConfig, TtsProvider, TtsSynthesizeInput, TtsSynthesizeResult } from '../types.js';
|
|
2
|
+
|
|
3
|
+
function resolveOpenAiApiKey(config: SpeechConfig): string {
|
|
4
|
+
return config.tts?.apiKey || config.stt?.apiKey || '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createOpenAiTtsProvider(config: SpeechConfig): TtsProvider {
|
|
8
|
+
const ttsConfig = config.tts || {};
|
|
9
|
+
return {
|
|
10
|
+
id: 'openai',
|
|
11
|
+
async synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult> {
|
|
12
|
+
const host = (ttsConfig.host || 'https://api.openai.com').replace(/\/$/, '');
|
|
13
|
+
const model = ttsConfig.model || 'tts-1';
|
|
14
|
+
const voice = input.voice || ttsConfig.voice || 'alloy';
|
|
15
|
+
const apiKey = resolveOpenAiApiKey(config);
|
|
16
|
+
const format = input.format === 'wav' ? 'wav' : 'mp3';
|
|
17
|
+
|
|
18
|
+
const body: Record<string, unknown> = {
|
|
19
|
+
model,
|
|
20
|
+
input: input.text,
|
|
21
|
+
voice,
|
|
22
|
+
response_format: format,
|
|
23
|
+
};
|
|
24
|
+
if (input.speed != null) body.speed = input.speed;
|
|
25
|
+
else if (ttsConfig.speed != null) body.speed = ttsConfig.speed;
|
|
26
|
+
|
|
27
|
+
const response = await fetch(`${host}/v1/audio/speech`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`OpenAI TTS failed: ${response.status} ${response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
41
|
+
return { data, format };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type SttProviderId = 'ollama' | 'openai';
|
|
2
|
+
export type TtsProviderId = 'edge' | 'openai' | 'azure' | 'custom';
|
|
3
|
+
|
|
4
|
+
export interface STTConfig {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
provider?: SttProviderId;
|
|
7
|
+
model?: string;
|
|
8
|
+
host?: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TTSConfig {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
provider?: TtsProviderId;
|
|
15
|
+
voice?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
host?: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** edge-tts */
|
|
20
|
+
rate?: string;
|
|
21
|
+
pitch?: string;
|
|
22
|
+
edgeTtsCommand?: string;
|
|
23
|
+
/** azure */
|
|
24
|
+
region?: string;
|
|
25
|
+
subscriptionKey?: string;
|
|
26
|
+
/** custom OpenAI-compatible */
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
speed?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SpeechConfig {
|
|
33
|
+
stt?: STTConfig;
|
|
34
|
+
tts?: TTSConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TtsSynthesizeInput {
|
|
38
|
+
text: string;
|
|
39
|
+
voice?: string;
|
|
40
|
+
format?: 'mp3' | 'wav';
|
|
41
|
+
speed?: number;
|
|
42
|
+
provider?: TtsProviderId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TtsSynthesizeResult {
|
|
46
|
+
data: Buffer;
|
|
47
|
+
format: 'mp3' | 'wav';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TtsProvider {
|
|
51
|
+
readonly id: TtsProviderId;
|
|
52
|
+
synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TranscribeInput {
|
|
56
|
+
data: Buffer;
|
|
57
|
+
mimeType?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpeechPipeline {
|
|
61
|
+
transcribe(input: TranscribeInput): Promise<string>;
|
|
62
|
+
synthesize(input: TtsSynthesizeInput): Promise<TtsSynthesizeResult>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SpeechLogger {
|
|
66
|
+
debug?(message: string, ...args: unknown[]): void;
|
|
67
|
+
warn?(message: string, ...args: unknown[]): void;
|
|
68
|
+
}
|