speakdrill 0.1.0 → 0.1.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 myet-practice contributors
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 myet-practice contributors
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 CHANGED
@@ -1,46 +1,46 @@
1
- # speakdrill
2
-
3
- 口說課程自動化練習:用 TTS 產生示範語音、逐句驅動課程演練、輸出可追蹤的分數報告。
4
-
5
- Rust 實作,含 CLI、TUI 與原生 GUI(egui,不是 Electron / Tauri webview)。
6
-
7
- > 目前只支援 **Windows x64**(相依 VoiceMeeter)。
8
-
9
- ## 安裝
10
-
11
- ```bash
12
- npm i -g speakdrill
13
- speakdrill init
14
- speakdrill doctor
15
- ```
16
-
17
- 安裝時會從 GitHub Releases 下載原生二進位(`speakdrill.exe` / `speakdrill-gui.exe`)。
18
-
19
- ## 快速開始
20
-
21
- ```bash
22
- speakdrill init # 偵測裝置、寫設定、設 VoiceMeeter 路由、拉音量
23
- speakdrill doctor # 檢查環境
24
- speakdrill tui # 終端機介面
25
- speakdrill-gui # 原生 GUI
26
- speakdrill run --lesson <ID> --course <ID>
27
- speakdrill batch --code <CODE> --limit 5
28
- speakdrill reset # 還原音訊路由
29
- ```
30
-
31
- ## 前置需求
32
-
33
- 1. **VoiceMeeter**(Banana 或 Potato)
34
- 2. **一個 TTS**:`pip install kokoro-onnx soundfile`,或自備 [piper](https://github.com/rhasspy/piper)
35
- 3. 目標 App 需開啟 debug port:
36
- `--remote-debugging-port=9222 --remote-allow-origins=*`
37
- 4. 在 `ms-settings:apps-volume` 把目標 App 的**輸入裝置**設成 `Voicemeeter Out B1`
38
- (這步只能用介面做,Windows 會產生綁定 GUID)
39
-
40
- ## 文件
41
-
42
- 完整說明、架構與踩坑紀錄:<https://github.com/thumb2086/speakdrill>
43
-
44
- ## 授權
45
-
46
- MIT
1
+ # speakdrill
2
+
3
+ 口說課程自動化練習:用 TTS 產生示範語音、逐句驅動課程演練、輸出可追蹤的分數報告。
4
+
5
+ Rust 實作,含 CLI、TUI 與原生 GUI(egui,不是 Electron / Tauri webview)。
6
+
7
+ > 目前只支援 **Windows x64**(相依 VoiceMeeter)。
8
+
9
+ ## 安裝
10
+
11
+ ```bash
12
+ npm i -g speakdrill
13
+ speakdrill init
14
+ speakdrill doctor
15
+ ```
16
+
17
+ 安裝時會從 GitHub Releases 下載原生二進位(`speakdrill.exe` / `speakdrill-gui.exe`)。
18
+
19
+ ## 快速開始
20
+
21
+ ```bash
22
+ speakdrill init # 偵測裝置、寫設定、設 VoiceMeeter 路由、拉音量
23
+ speakdrill doctor # 檢查環境
24
+ speakdrill tui # 終端機介面
25
+ speakdrill-gui # 原生 GUI
26
+ speakdrill run --lesson <ID> --course <ID>
27
+ speakdrill batch --code <CODE> --limit 5
28
+ speakdrill reset # 還原音訊路由
29
+ ```
30
+
31
+ ## 前置需求
32
+
33
+ 1. **VoiceMeeter**(Banana 或 Potato)
34
+ 2. **一個 TTS**:`pip install kokoro-onnx soundfile`,或自備 [piper](https://github.com/rhasspy/piper)
35
+ 3. 目標 App 需開啟 debug port:
36
+ `--remote-debugging-port=9222 --remote-allow-origins=*`
37
+ 4. 在 `ms-settings:apps-volume` 把目標 App 的**輸入裝置**設成 `Voicemeeter Out B1`
38
+ (這步只能用介面做,Windows 會產生綁定 GUID)
39
+
40
+ ## 文件
41
+
42
+ 完整說明、架構與踩坑紀錄:<https://github.com/thumb2086/speakdrill>
43
+
44
+ ## 授權
45
+
46
+ MIT
package/bin/speakdrill.js CHANGED
@@ -1,35 +1,35 @@
1
- #!/usr/bin/env node
2
- /**
3
- * npm 包裝器:把 `speakdrill ...` 轉給對應平台的原生二進位。
4
- *
5
- * 二進位由 scripts/install.js 從 GitHub Releases 下載到
6
- * <package>/bin/binaries/<platform>-<arch>/speakdrill[.exe]
7
- */
8
- const { spawn } = require('child_process');
9
- const path = require('path');
10
- const fs = require('fs');
11
-
12
- const exe = process.platform === 'win32' ? 'speakdrill.exe' : 'speakdrill';
13
- const dir = `${process.platform}-${process.arch}`;
14
- const candidates = [
15
- path.join(__dirname, 'binaries', dir, exe),
16
- path.join(__dirname, '..', '..', 'target', 'release', exe),
17
- path.join(__dirname, '..', '..', 'target', 'debug', exe),
18
- ];
19
-
20
- const bin = candidates.find((p) => fs.existsSync(p));
21
-
22
- if (!bin) {
23
- console.error(
24
- `[speakdrill] 找不到可執行檔(${dir})。\n` +
25
- ` 請重新安裝:npm rebuild speakdrill\n` +
26
- ` 或從 https://github.com/thumb2086/speakdrill/releases 下載`
27
- );
28
- process.exit(1);
29
- }
30
-
31
- const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' });
32
- child.on('exit', (code, signal) => {
33
- if (signal) process.kill(process.pid, signal);
34
- else process.exit(code ?? 1);
35
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm 包裝器:把 `speakdrill ...` 轉給對應平台的原生二進位。
4
+ *
5
+ * 二進位由 scripts/install.js 從 GitHub Releases 下載到
6
+ * <package>/bin/binaries/<platform>-<arch>/speakdrill[.exe]
7
+ */
8
+ const { spawn } = require('child_process');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+
12
+ const exe = process.platform === 'win32' ? 'speakdrill.exe' : 'speakdrill';
13
+ const dir = `${process.platform}-${process.arch}`;
14
+ const candidates = [
15
+ path.join(__dirname, 'binaries', dir, exe),
16
+ path.join(__dirname, '..', '..', 'target', 'release', exe),
17
+ path.join(__dirname, '..', '..', 'target', 'debug', exe),
18
+ ];
19
+
20
+ const bin = candidates.find((p) => fs.existsSync(p));
21
+
22
+ if (!bin) {
23
+ console.error(
24
+ `[speakdrill] 找不到可執行檔(${dir})。\n` +
25
+ ` 請重新安裝:npm rebuild speakdrill\n` +
26
+ ` 或從 https://github.com/thumb2086/speakdrill/releases 下載`
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' });
32
+ child.on('exit', (code, signal) => {
33
+ if (signal) process.kill(process.pid, signal);
34
+ else process.exit(code ?? 1);
35
+ });
package/package.json CHANGED
@@ -1,24 +1,44 @@
1
- {
2
- "name": "speakdrill",
3
- "version": "0.1.0",
4
- "description": "口說課程自動化練習:TTS 產生示範語音、逐句驅動課程演練、輸出分數報告(Rust,含 TUI / 原生 GUI)",
5
- "keywords": ["speaking", "shadowing", "drill", "tts", "voicemeeter", "english", "cli", "tui", "gui", "windows"],
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/thumb2086/speakdrill.git"
10
- },
11
- "homepage": "https://github.com/thumb2086/speakdrill#readme",
12
- "bin": {
13
- "speakdrill": "bin/speakdrill.js"
14
- },
15
- "files": ["bin/", "scripts/", "README.md", "LICENSE"],
16
- "scripts": {
17
- "postinstall": "node scripts/install.js"
18
- },
19
- "engines": {
20
- "node": ">=18"
21
- },
22
- "os": ["win32"],
23
- "cpu": ["x64"]
24
- }
1
+ {
2
+ "name": "speakdrill",
3
+ "version": "0.1.1",
4
+ "description": "口說課程自動化練習:TTS 產生示範語音、逐句驅動課程演練、輸出分數報告(Rust,含 TUI / 原生 GUI)",
5
+ "keywords": [
6
+ "speaking",
7
+ "shadowing",
8
+ "drill",
9
+ "tts",
10
+ "voicemeeter",
11
+ "english",
12
+ "cli",
13
+ "tui",
14
+ "gui",
15
+ "windows"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/thumb2086/speakdrill.git"
21
+ },
22
+ "homepage": "https://github.com/thumb2086/speakdrill#readme",
23
+ "bin": {
24
+ "speakdrill": "bin/speakdrill.js"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "scripts/",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "postinstall": "node scripts/install.js"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "os": [
39
+ "win32"
40
+ ],
41
+ "cpu": [
42
+ "x64"
43
+ ]
44
+ }
@@ -1,89 +1,89 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 安裝時下載對應平台的原生二進位。
4
- *
5
- * 環境變數:
6
- * SPEAKDRILL_SKIP_DOWNLOAD=1 跳過下載(本機開發用,會直接用 target/ 的執行檔)
7
- * SPEAKDRILL_VERSION 指定版本(預設讀 package.json)
8
- * SPEAKDRILL_REPO 覆寫 GitHub repo(預設 thumb2086/speakdrill)
9
- */
10
- const fs = require('fs');
11
- const path = require('path');
12
- const https = require('https');
13
- const { execFileSync } = require('child_process');
14
-
15
- const pkg = require('../package.json');
16
- const VERSION = process.env.SPEAKDRILL_VERSION || pkg.version;
17
- const REPO = process.env.SPEAKDRILL_REPO || 'thumb2086/speakdrill';
18
-
19
- if (process.env.SPEAKDRILL_SKIP_DOWNLOAD === '1') {
20
- console.log('[speakdrill] SPEAKDRILL_SKIP_DOWNLOAD=1,跳過下載');
21
- process.exit(0);
22
- }
23
-
24
- if (process.platform !== 'win32') {
25
- console.log(
26
- `[speakdrill] 目前只支援 Windows(需要 VoiceMeeter)。你的平台是 ${process.platform},跳過安裝。`
27
- );
28
- process.exit(0);
29
- }
30
-
31
- const exes = ['speakdrill.exe', 'speakdrill-gui.exe'];
32
- const dir = `${process.platform}-${process.arch}`;
33
- const outDir = path.join(__dirname, '..', 'bin', 'binaries', dir);
34
-
35
- const tag = `v${VERSION}`;
36
- const asset = `speakdrill-${tag}-${dir}.zip`;
37
- const url = `https://github.com/${REPO}/releases/download/${tag}/${asset}`;
38
-
39
- function download(u, dest) {
40
- return new Promise((resolve, reject) => {
41
- const go = (u2, depth = 0) => {
42
- if (depth > 5) return reject(new Error('太多重新導向'));
43
- https
44
- .get(u2, { headers: { 'User-Agent': 'speakdrill-installer' } }, (res) => {
45
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
46
- res.resume();
47
- return go(res.headers.location, depth + 1);
48
- }
49
- if (res.statusCode !== 200) {
50
- res.resume();
51
- return reject(new Error(`HTTP ${res.statusCode} ${u2}`));
52
- }
53
- const f = fs.createWriteStream(dest);
54
- res.pipe(f);
55
- f.on('finish', () => f.close(() => resolve()));
56
- })
57
- .on('error', reject);
58
- };
59
- go(u);
60
- });
61
- }
62
-
63
- (async () => {
64
- fs.mkdirSync(outDir, { recursive: true });
65
- const archive = path.join(outDir, asset);
66
-
67
- const existing = path.join(outDir, exes[0]);
68
- if (fs.existsSync(existing)) {
69
- console.log(`[speakdrill] 已存在 ${existing}`);
70
- return;
71
- }
72
-
73
- console.log(`[speakdrill] 下載 ${url}`);
74
- try {
75
- await download(url, archive);
76
- execFileSync(
77
- 'powershell',
78
- ['-NoProfile', '-Command',
79
- `Expand-Archive -LiteralPath '${archive}' -DestinationPath '${outDir}' -Force`],
80
- { stdio: 'inherit' }
81
- );
82
- fs.unlinkSync(archive);
83
- console.log(`[speakdrill] 安裝完成 → ${outDir}`);
84
- } catch (e) {
85
- console.error(`[speakdrill] 下載失敗:${e.message}`);
86
- console.error('[speakdrill] 可改用手動安裝,或設定 SPEAKDRILL_SKIP_DOWNLOAD=1 使用本機建置');
87
- process.exit(0); // 不讓 npm install 整個失敗
88
- }
89
- })();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 安裝時下載對應平台的原生二進位。
4
+ *
5
+ * 環境變數:
6
+ * SPEAKDRILL_SKIP_DOWNLOAD=1 跳過下載(本機開發用,會直接用 target/ 的執行檔)
7
+ * SPEAKDRILL_VERSION 指定版本(預設讀 package.json)
8
+ * SPEAKDRILL_REPO 覆寫 GitHub repo(預設 thumb2086/speakdrill)
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const https = require('https');
13
+ const { execFileSync } = require('child_process');
14
+
15
+ const pkg = require('../package.json');
16
+ const VERSION = process.env.SPEAKDRILL_VERSION || pkg.version;
17
+ const REPO = process.env.SPEAKDRILL_REPO || 'thumb2086/speakdrill';
18
+
19
+ if (process.env.SPEAKDRILL_SKIP_DOWNLOAD === '1') {
20
+ console.log('[speakdrill] SPEAKDRILL_SKIP_DOWNLOAD=1,跳過下載');
21
+ process.exit(0);
22
+ }
23
+
24
+ if (process.platform !== 'win32') {
25
+ console.log(
26
+ `[speakdrill] 目前只支援 Windows(需要 VoiceMeeter)。你的平台是 ${process.platform},跳過安裝。`
27
+ );
28
+ process.exit(0);
29
+ }
30
+
31
+ const exes = ['speakdrill.exe', 'speakdrill-gui.exe'];
32
+ const dir = `${process.platform}-${process.arch}`;
33
+ const outDir = path.join(__dirname, '..', 'bin', 'binaries', dir);
34
+
35
+ const tag = `v${VERSION}`;
36
+ const asset = `speakdrill-${tag}-${dir}.zip`;
37
+ const url = `https://github.com/${REPO}/releases/download/${tag}/${asset}`;
38
+
39
+ function download(u, dest) {
40
+ return new Promise((resolve, reject) => {
41
+ const go = (u2, depth = 0) => {
42
+ if (depth > 5) return reject(new Error('太多重新導向'));
43
+ https
44
+ .get(u2, { headers: { 'User-Agent': 'speakdrill-installer' } }, (res) => {
45
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
46
+ res.resume();
47
+ return go(res.headers.location, depth + 1);
48
+ }
49
+ if (res.statusCode !== 200) {
50
+ res.resume();
51
+ return reject(new Error(`HTTP ${res.statusCode} ${u2}`));
52
+ }
53
+ const f = fs.createWriteStream(dest);
54
+ res.pipe(f);
55
+ f.on('finish', () => f.close(() => resolve()));
56
+ })
57
+ .on('error', reject);
58
+ };
59
+ go(u);
60
+ });
61
+ }
62
+
63
+ (async () => {
64
+ fs.mkdirSync(outDir, { recursive: true });
65
+ const archive = path.join(outDir, asset);
66
+
67
+ const existing = path.join(outDir, exes[0]);
68
+ if (fs.existsSync(existing)) {
69
+ console.log(`[speakdrill] 已存在 ${existing}`);
70
+ return;
71
+ }
72
+
73
+ console.log(`[speakdrill] 下載 ${url}`);
74
+ try {
75
+ await download(url, archive);
76
+ execFileSync(
77
+ 'powershell',
78
+ ['-NoProfile', '-Command',
79
+ `Expand-Archive -LiteralPath '${archive}' -DestinationPath '${outDir}' -Force`],
80
+ { stdio: 'inherit' }
81
+ );
82
+ fs.unlinkSync(archive);
83
+ console.log(`[speakdrill] 安裝完成 → ${outDir}`);
84
+ } catch (e) {
85
+ console.error(`[speakdrill] 下載失敗:${e.message}`);
86
+ console.error('[speakdrill] 可改用手動安裝,或設定 SPEAKDRILL_SKIP_DOWNLOAD=1 使用本機建置');
87
+ process.exit(0); // 不讓 npm install 整個失敗
88
+ }
89
+ })();
@@ -1,93 +1,93 @@
1
- #!/usr/bin/env python3
2
- """
3
- kokoro_tts.py — 給 speakdrill 用的 TTS 轉接器
4
-
5
- 用法:
6
- python kokoro_tts.py <文字檔> <輸出.wav>
7
-
8
- 環境變數:
9
- KOKORO_MODEL 模型路徑(預設找常見位置)
10
- KOKORO_VOICES 聲音檔路徑
11
- KOKORO_VOICE 音色 ID(預設 af_sol)
12
- KOKORO_SPEED 語速(預設 1.0)
13
-
14
- 安裝:
15
- pip install kokoro-onnx soundfile
16
- # 模型:https://github.com/thewh1teagle/kokoro-onnx/releases
17
- """
18
- import os
19
- import sys
20
- import wave
21
-
22
- def find_model():
23
- env = os.environ.get("KOKORO_MODEL")
24
- if env and os.path.isfile(env):
25
- return env
26
- cands = [
27
- os.path.join(os.path.dirname(__file__), "..", "models", "kokoro-v1.0.onnx"),
28
- os.path.expanduser("~/.cache/kokoro/kokoro-v1.0.onnx"),
29
- r"C:\Users\CPXru\Desktop\thumb\Projects\ai_vtuber\models\kokoro\kokoro-v1.1-zh.onnx",
30
- ]
31
- for c in cands:
32
- if os.path.isfile(c):
33
- return os.path.abspath(c)
34
- return None
35
-
36
- def find_voices():
37
- env = os.environ.get("KOKORO_VOICES")
38
- if env and os.path.isfile(env):
39
- return env
40
- cands = [
41
- os.path.join(os.path.dirname(__file__), "..", "models", "voices-v1.0.bin"),
42
- os.path.expanduser("~/.cache/kokoro/voices-v1.0.bin"),
43
- r"C:\Users\CPXru\Desktop\thumb\Projects\ai_vtuber\models\kokoro\voices-v1.1-zh.bin",
44
- ]
45
- for c in cands:
46
- if os.path.isfile(c):
47
- return os.path.abspath(c)
48
- return None
49
-
50
- def main():
51
- if len(sys.argv) < 3:
52
- print("usage: kokoro_tts.py <textfile> <out.wav>", file=sys.stderr)
53
- return 2
54
-
55
- textfile, out = sys.argv[1], sys.argv[2]
56
- with open(textfile, encoding="utf-8") as f:
57
- text = f.read().strip()
58
- if not text:
59
- print("empty text", file=sys.stderr)
60
- return 2
61
-
62
- # 清掉會讓 TTS 拉長的寫法(MyET 錄音視窗只有 ~2.7 秒)
63
- import re
64
- text = text.replace("...", ", ").replace("…", ", ")
65
- text = re.sub(r"\s+", " ", text).strip()
66
-
67
- try:
68
- import numpy as np
69
- import soundfile as sf
70
- from kokoro_onnx import Kokoro
71
- except ImportError as e:
72
- print(f"缺少套件:{e}\n請執行: pip install kokoro-onnx soundfile", file=sys.stderr)
73
- return 3
74
-
75
- model, voices = find_model(), find_voices()
76
- if not model or not voices:
77
- print("找不到 Kokoro 模型檔;請設定 KOKORO_MODEL / KOKORO_VOICES", file=sys.stderr)
78
- return 3
79
-
80
- voice = os.environ.get("KOKORO_VOICE", "af_sol")
81
- speed = float(os.environ.get("KOKORO_SPEED", "1.0"))
82
-
83
- k = Kokoro(model, voices)
84
- samples, sr = k.create(text, voice=voice, speed=speed, lang="en-us")
85
- a = np.asarray(samples, dtype="float32")
86
- peak = float(np.max(np.abs(a))) if a.size else 0.0
87
- if peak > 0:
88
- a = a * (0.95 / peak)
89
- sf.write(out, a, sr)
90
- return 0
91
-
92
- if __name__ == "__main__":
93
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ kokoro_tts.py — 給 speakdrill 用的 TTS 轉接器
4
+
5
+ 用法:
6
+ python kokoro_tts.py <文字檔> <輸出.wav>
7
+
8
+ 環境變數:
9
+ KOKORO_MODEL 模型路徑(預設找常見位置)
10
+ KOKORO_VOICES 聲音檔路徑
11
+ KOKORO_VOICE 音色 ID(預設 af_sol)
12
+ KOKORO_SPEED 語速(預設 1.0)
13
+
14
+ 安裝:
15
+ pip install kokoro-onnx soundfile
16
+ # 模型:https://github.com/thewh1teagle/kokoro-onnx/releases
17
+ """
18
+ import os
19
+ import sys
20
+ import wave
21
+
22
+ def find_model():
23
+ env = os.environ.get("KOKORO_MODEL")
24
+ if env and os.path.isfile(env):
25
+ return env
26
+ cands = [
27
+ os.path.join(os.path.dirname(__file__), "..", "models", "kokoro-v1.0.onnx"),
28
+ os.path.expanduser("~/.cache/kokoro/kokoro-v1.0.onnx"),
29
+ r"C:\Users\CPXru\Desktop\thumb\Projects\ai_vtuber\models\kokoro\kokoro-v1.1-zh.onnx",
30
+ ]
31
+ for c in cands:
32
+ if os.path.isfile(c):
33
+ return os.path.abspath(c)
34
+ return None
35
+
36
+ def find_voices():
37
+ env = os.environ.get("KOKORO_VOICES")
38
+ if env and os.path.isfile(env):
39
+ return env
40
+ cands = [
41
+ os.path.join(os.path.dirname(__file__), "..", "models", "voices-v1.0.bin"),
42
+ os.path.expanduser("~/.cache/kokoro/voices-v1.0.bin"),
43
+ r"C:\Users\CPXru\Desktop\thumb\Projects\ai_vtuber\models\kokoro\voices-v1.1-zh.bin",
44
+ ]
45
+ for c in cands:
46
+ if os.path.isfile(c):
47
+ return os.path.abspath(c)
48
+ return None
49
+
50
+ def main():
51
+ if len(sys.argv) < 3:
52
+ print("usage: kokoro_tts.py <textfile> <out.wav>", file=sys.stderr)
53
+ return 2
54
+
55
+ textfile, out = sys.argv[1], sys.argv[2]
56
+ with open(textfile, encoding="utf-8") as f:
57
+ text = f.read().strip()
58
+ if not text:
59
+ print("empty text", file=sys.stderr)
60
+ return 2
61
+
62
+ # 清掉會讓 TTS 拉長的寫法(MyET 錄音視窗只有 ~2.7 秒)
63
+ import re
64
+ text = text.replace("...", ", ").replace("…", ", ")
65
+ text = re.sub(r"\s+", " ", text).strip()
66
+
67
+ try:
68
+ import numpy as np
69
+ import soundfile as sf
70
+ from kokoro_onnx import Kokoro
71
+ except ImportError as e:
72
+ print(f"缺少套件:{e}\n請執行: pip install kokoro-onnx soundfile", file=sys.stderr)
73
+ return 3
74
+
75
+ model, voices = find_model(), find_voices()
76
+ if not model or not voices:
77
+ print("找不到 Kokoro 模型檔;請設定 KOKORO_MODEL / KOKORO_VOICES", file=sys.stderr)
78
+ return 3
79
+
80
+ voice = os.environ.get("KOKORO_VOICE", "af_sol")
81
+ speed = float(os.environ.get("KOKORO_SPEED", "1.0"))
82
+
83
+ k = Kokoro(model, voices)
84
+ samples, sr = k.create(text, voice=voice, speed=speed, lang="en-us")
85
+ a = np.asarray(samples, dtype="float32")
86
+ peak = float(np.max(np.abs(a))) if a.size else 0.0
87
+ if peak > 0:
88
+ a = a * (0.95 / peak)
89
+ sf.write(out, a, sr)
90
+ return 0
91
+
92
+ if __name__ == "__main__":
93
+ sys.exit(main())