@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makuraryu/yonde",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Config-driven Bun CLI for creating bilingual Japanese listening scripts and MP3 audio",
5
5
  "type": "module",
6
6
  "bin": {
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(spec: AudioSpec, cacheDir: string, ffmpegPath: string): Promise<SynthesisResult> {
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
- if (await fileIsUsable(output)) return { path: output, cached: true };
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
- for (const entry of await readdir(cacheDir)) if (entry.endsWith(".part.mp3")) await rm(join(cacheDir, entry), { force: true });
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.0";
14
+ const VERSION = "0.4.1";
15
15
 
16
16
  export const HELP_TEXT = `Yonde ${VERSION} — 配置驱动的双语听力材料生成器
17
17