@makuraryu/yonde 0.4.0 → 0.4.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/README.md +2 -0
- package/package.json +1 -1
- package/src/audio.ts +13 -4
- package/src/main.ts +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,8 @@ package_asset = "uisfx/sounds/cinematic/select.mp3"
|
|
|
148
148
|
|
|
149
149
|
翻译缓存指纹包含输入内容、语言、模型、端点、提示词版本和分句规则,但不包含 API Key。音频缓存指纹包含文本、音色、语言、语速和音调。仅调整朗读顺序时,Yonde 会复用已有语音并重新合并。
|
|
150
150
|
|
|
151
|
+
缓存文件采用原子写入;命中缓存时只检查文件元数据,避免 iCloud 为数千个小文件执行随机头部读取。最终拼接再按朗读顺序连续读取音频内容。
|
|
152
|
+
|
|
151
153
|
翻译、语音生成和最终 MP3 合并都会显示单行进度条,包括完成比例、数量、耗时和 ETA。非交互终端按 5% 里程碑输出,避免日志刷屏。
|
|
152
154
|
|
|
153
155
|
所有语音和分隔音效会统一为 24 kHz、单声道、96 kbps,最终 MP3 采用无损快速拼接,不再把数小时音频完整重编码。
|
package/package.json
CHANGED
package/src/audio.ts
CHANGED
|
@@ -47,11 +47,18 @@ function specHash(spec: AudioSpec): string {
|
|
|
47
47
|
.slice(0, 24);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function synthesize(
|
|
50
|
+
async function synthesize(
|
|
51
|
+
spec: AudioSpec,
|
|
52
|
+
cacheDir: string,
|
|
53
|
+
ffmpegPath: string,
|
|
54
|
+
existingCacheNames: ReadonlySet<string>,
|
|
55
|
+
): Promise<SynthesisResult> {
|
|
51
56
|
const hash = specHash(spec);
|
|
52
57
|
const safeId = spec.kind.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
53
58
|
const output = join(cacheDir, `${safeId}-${hash}.mp3`);
|
|
54
|
-
|
|
59
|
+
// Cache files are atomically renamed after a successful synthesis. A single
|
|
60
|
+
// directory listing avoids thousands of slow per-file metadata reads on iCloud.
|
|
61
|
+
if (existingCacheNames.has(basename(output))) return { path: output, cached: true };
|
|
55
62
|
|
|
56
63
|
const temporary = `${output}.${process.pid}.${randomUUID()}.part.mp3`;
|
|
57
64
|
if (!/[\p{L}\p{N}]/u.test(spec.text)) {
|
|
@@ -273,7 +280,9 @@ export async function buildAudio(
|
|
|
273
280
|
if (!ffmpegPath) throw new Error("找不到 ffmpeg;请先安装 ffmpeg,或仅运行 --stage translate");
|
|
274
281
|
const cacheDir = join(stateDir, "audio-cache");
|
|
275
282
|
await mkdir(cacheDir, { recursive: true });
|
|
276
|
-
|
|
283
|
+
const cacheEntries = await readdir(cacheDir);
|
|
284
|
+
for (const entry of cacheEntries) if (entry.endsWith(".part.mp3")) await rm(join(cacheDir, entry), { force: true });
|
|
285
|
+
const existingCacheNames = new Set(cacheEntries.filter((entry) => entry.endsWith(".mp3")));
|
|
277
286
|
const outputDir = dirname(outputPath);
|
|
278
287
|
await mkdir(outputDir, { recursive: true });
|
|
279
288
|
|
|
@@ -316,7 +325,7 @@ export async function buildAudio(
|
|
|
316
325
|
let results: SynthesisResult[];
|
|
317
326
|
try {
|
|
318
327
|
results = await runPool(
|
|
319
|
-
entries.map(([, spec]) => () => synthesize(spec, cacheDir, ffmpegPath)),
|
|
328
|
+
entries.map(([, spec]) => () => synthesize(spec, cacheDir, ffmpegPath, existingCacheNames)),
|
|
320
329
|
config.audio.concurrency,
|
|
321
330
|
(result) => {
|
|
322
331
|
completed += 1;
|
package/src/main.ts
CHANGED
|
@@ -11,7 +11,7 @@ type Stage = "translate" | "audio" | "all";
|
|
|
11
11
|
type RunArgs = { command: "run"; input: string; stage: Stage; outputDir?: string; configPath?: string };
|
|
12
12
|
type CliArgs = RunArgs | { command: "init"; path: string } | { command: "config-check"; configPath?: string } | { command: "help" } | { command: "version" };
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.4.
|
|
14
|
+
const VERSION = "0.4.1";
|
|
15
15
|
|
|
16
16
|
export const HELP_TEXT = `Yonde ${VERSION} — 配置驱动的双语听力材料生成器
|
|
17
17
|
|