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 { 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 { 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); } }, }; }