94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
import { joinPath } from "../../platform/paths";
|
|
import {
|
|
defaultVoiceForLang,
|
|
removeIfExists,
|
|
resolveExecutable,
|
|
writeTempTextFile,
|
|
} from "../../platform/executable";
|
|
import { runCommand } from "../../platform/subprocess";
|
|
import type { TtsProvider, TtsSynthesizeOptions, TtsResult } from "../types";
|
|
import { TtsError } from "../types";
|
|
import { getPref } from "../../../../../utils/prefs";
|
|
|
|
async function estimateWavDurationMs(path: string): Promise<number> {
|
|
try {
|
|
const stat = await IOUtils.stat(path);
|
|
return Math.max(1000, Math.floor((stat.size / 32000) * 1000));
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
export function createWindowsSapiProvider(): TtsProvider {
|
|
return {
|
|
id: "system-win",
|
|
label: "Windows SAPI",
|
|
kind: "local",
|
|
supportsTimestamps: false,
|
|
async synthesize(options: TtsSynthesizeOptions): Promise<TtsResult> {
|
|
const powershell = await resolveExecutable("powershell");
|
|
const voice =
|
|
options.voice ||
|
|
(getPref("ttsVoice") as string) ||
|
|
defaultVoiceForLang(options.lang);
|
|
|
|
const outWav = options.outputPath.endsWith(".wav")
|
|
? options.outputPath
|
|
: options.outputPath.replace(/\.\w+$/, ".wav");
|
|
|
|
const textPath = await writeTempTextFile(options.text, "sapi-text");
|
|
const scriptPath = joinPath(
|
|
PathUtils.tempDir,
|
|
`chatpapers-sapi-${Date.now()}.ps1`,
|
|
);
|
|
|
|
const psScript = [
|
|
"Add-Type -AssemblyName System.Speech",
|
|
"$text = Get-Content -LiteralPath $args[0] -Raw -Encoding UTF8",
|
|
"$out = $args[1]",
|
|
"$voice = $args[2]",
|
|
"$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer",
|
|
"if ($voice) { try { $synth.SelectVoice($voice) } catch {} }",
|
|
"$synth.SetOutputToWaveFile($out)",
|
|
"$synth.Speak($text)",
|
|
"$synth.Dispose()",
|
|
].join("\n");
|
|
|
|
await IOUtils.writeUTF8(scriptPath, psScript);
|
|
|
|
try {
|
|
const result = await runCommand(powershell, [
|
|
"-NoProfile",
|
|
"-ExecutionPolicy",
|
|
"Bypass",
|
|
"-File",
|
|
scriptPath,
|
|
textPath,
|
|
outWav,
|
|
voice,
|
|
]);
|
|
|
|
if (result.exitCode !== 0) {
|
|
throw new TtsError(
|
|
`PowerShell SAPI 退出码 ${result.exitCode}`,
|
|
"runtime",
|
|
);
|
|
}
|
|
if (!(await IOUtils.exists(outWav))) {
|
|
throw new TtsError("SAPI 未生成音频文件", "runtime");
|
|
}
|
|
|
|
return {
|
|
audioPath: outWav,
|
|
timestamps: [],
|
|
durationMs: await estimateWavDurationMs(outWav),
|
|
format: "wav",
|
|
};
|
|
} finally {
|
|
await removeIfExists(textPath);
|
|
await removeIfExists(scriptPath);
|
|
}
|
|
},
|
|
};
|
|
}
|