mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 09:08:46 +02:00
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { execFile, execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
let sayChecked = false;
|
|
let sayAvailable = false;
|
|
|
|
function hasSay() {
|
|
if (!sayChecked) {
|
|
sayChecked = true;
|
|
try {
|
|
execFileSync("say", ["-v", "?"], { stdio: "ignore" });
|
|
sayAvailable = true;
|
|
} catch {
|
|
console.warn("`say` not found — skipping TTS narration (subtitles still work).");
|
|
}
|
|
}
|
|
return sayAvailable;
|
|
}
|
|
|
|
const cache = new Map<string, Promise<{ path: string; durationMs: number } | null>>();
|
|
|
|
/** Synthesizes Spanish narration for a caption via macOS `say`. Returns null (never throws) if unavailable. */
|
|
export function synthesize(
|
|
text: string,
|
|
outDir: string,
|
|
index: number,
|
|
voice = "Paulina",
|
|
): Promise<{ path: string; durationMs: number } | null> {
|
|
const key = `${voice}:${text}`;
|
|
const cached = cache.get(key);
|
|
if (cached) return cached;
|
|
|
|
const promise = (async () => {
|
|
if (!hasSay()) return null;
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const file = path.join(outDir, `narration-${String(index).padStart(2, "0")}.aiff`);
|
|
try {
|
|
await execFileAsync("say", ["-v", voice, "-o", file, text]);
|
|
const { stdout } = await execFileAsync("ffprobe", [
|
|
"-v", "error",
|
|
"-show_entries", "format=duration",
|
|
"-of", "csv=p=0",
|
|
file,
|
|
]);
|
|
const durationMs = Math.round(parseFloat(stdout.trim()) * 1000);
|
|
return { path: file, durationMs };
|
|
} catch (err) {
|
|
console.warn(`Narration synthesis failed for "${text.slice(0, 40)}…": ${err}`);
|
|
fs.rmSync(file, { force: true });
|
|
return null;
|
|
}
|
|
})();
|
|
cache.set(key, promise);
|
|
return promise;
|
|
}
|