speakdrill 0.1.0 → 0.2.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/LICENSE +21 -21
- package/README.md +56 -46
- package/bin/speakdrill-gui.js +29 -0
- package/bin/speakdrill.js +35 -35
- package/package.json +45 -24
- package/scripts/install.js +89 -89
- package/scripts/kokoro_tts.py +93 -93
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,56 @@
|
|
|
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
|
-
|
|
22
|
-
speakdrill init
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
|
|
22
|
+
> 安裝/變更虛擬音效驅動後,**請重新開機**再跑 `speakdrill init`。
|
|
23
|
+
> 只重啟 App 不夠。`doctor` 會檢查:
|
|
24
|
+
> - 迴路能不能實際開啟
|
|
25
|
+
> - App 的每應用程式麥克風綁定是否一致
|
|
26
|
+
> - 是否選到真正可用的迴路
|
|
27
|
+
|
|
28
|
+
## 快速開始
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
speakdrill init # 偵測裝置、寫設定、設 VoiceMeeter 路由、拉音量
|
|
32
|
+
speakdrill doctor # 檢查環境
|
|
33
|
+
speakdrill # 終端機介面(預設)
|
|
34
|
+
speakdrill list # 列出課程
|
|
35
|
+
speakdrill-gui # 原生 GUI
|
|
36
|
+
speakdrill run --lesson <ID> --course <ID>
|
|
37
|
+
speakdrill batch --code <CODE> --limit 5
|
|
38
|
+
speakdrill reset # 還原音訊路由
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 前置需求
|
|
42
|
+
|
|
43
|
+
1. **VoiceMeeter**(Banana 或 Potato)
|
|
44
|
+
2. **不需要額外裝 TTS**:`speakdrill init` 自動下載 Piper
|
|
45
|
+
3. 目標 App 需開啟 debug port:
|
|
46
|
+
`--remote-debugging-port=9222 --remote-allow-origins=*`
|
|
47
|
+
4. 在 `ms-settings:apps-volume` 把目標 App 的**輸入裝置**設成 `Voicemeeter Out B1`
|
|
48
|
+
(這步只能用介面做,Windows 會產生綁定 GUID)
|
|
49
|
+
|
|
50
|
+
## 文件
|
|
51
|
+
|
|
52
|
+
完整說明、架構與踩坑紀錄:<https://github.com/thumb2086/speakdrill>
|
|
53
|
+
|
|
54
|
+
## 授權
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GUI 啟動器:把 `speakdrill-gui` 轉給原生 egui 視窗。
|
|
4
|
+
*/
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
const exe = process.platform === 'win32' ? 'speakdrill-gui.exe' : 'speakdrill-gui';
|
|
10
|
+
const dir = `${process.platform}-${process.arch}`;
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(__dirname, 'binaries', dir, exe),
|
|
13
|
+
path.join(__dirname, '..', '..', 'target', 'release', exe),
|
|
14
|
+
path.join(__dirname, '..', '..', 'target', 'debug', exe),
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const bin = candidates.find((p) => fs.existsSync(p));
|
|
18
|
+
|
|
19
|
+
if (!bin) {
|
|
20
|
+
console.error(
|
|
21
|
+
`[speakdrill] 找不到 GUI 執行檔(${dir})。\n` +
|
|
22
|
+
` 請重新安裝:npm rebuild speakdrill\n` +
|
|
23
|
+
` 或從 https://github.com/thumb2086/speakdrill/releases 下載`
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit', detached: false });
|
|
29
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
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,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "speakdrill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "口說課程自動化練習:TTS 產生示範語音、逐句驅動課程演練、輸出分數報告(Rust,含 TUI / 原生 GUI)",
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "speakdrill",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"speakdrill-gui": "bin/speakdrill-gui.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"scripts/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"postinstall": "node scripts/install.js"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"os": [
|
|
40
|
+
"win32"
|
|
41
|
+
],
|
|
42
|
+
"cpu": [
|
|
43
|
+
"x64"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/scripts/install.js
CHANGED
|
@@ -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
|
+
})();
|
package/scripts/kokoro_tts.py
CHANGED
|
@@ -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())
|