unit uTTS;
interface
uses
winapi.Windows, System.SysUtils, System.Classes, winapi.MMSystem;
type
TPlayFinishedEvent = procedure(Sender: TObject) of object;
type
TTTS = class
private
FPiperExePath: string;
FVoicesFolder: string;
FCurrentModel: string;
FOnPlayFinished: TPlayFinishedEvent;
procedure DoPlayFinished;
function RunProcessCaptureOutput(const Exe, Args, InputText: string; out OutputText: string): Boolean;
public
constructor Create(const APiperExePath, AVoicesFolder: string);
/// Список моделей (файлы .onnx) в папке voices
function GetModelsList: TStringList;
/// Выбрать модель озвучивания (имя файла .onnx)
procedure SetModel(const ModelFileName: string);
/// Озвучить текст выбранной моделью
procedure TextToSpeech(const aText: string; isDeleteFile: Boolean = False);
property OnPlayFinished: TPlayFinishedEvent read FOnPlayFinished write FOnPlayFinished;
end;
implementation
procedure TTTS.DoPlayFinished;
begin
if Assigned(FOnPlayFinished) then
FOnPlayFinished(Self);
end;
constructor TTTS.Create(const APiperExePath, AVoicesFolder: string);
begin
inherited Create;
FPiperExePath := APiperExePath;
FVoicesFolder := AVoicesFolder;
FCurrentModel := ''; // Пока не выбрана модель
end;
function TTTS.GetModelsList: TStringList;
var
SR: TSearchRec;
begin
Result := TStringList.Create;
if not DirectoryExists(FVoicesFolder) then Exit;
if FindFirst(FVoicesFolder + PathDelim + '*.onnx', faAnyFile, SR) = 0 then
begin
repeat
Result.Add(SR.Name);
until FindNext(SR) <> 0;
FindClose(SR);
end;
end;
procedure TTTS.SetModel(const ModelFileName: string);
var
FullPath: string;
begin
FullPath := IncludeTrailingPathDelimiter(FVoicesFolder) + ModelFileName;
if not FileExists(FullPath) then
raise Exception.CreateFmt('Модель не найдена: %s', [FullPath]);
FCurrentModel := FullPath;
end;
function TTTS.RunProcessCaptureOutput(const Exe, Args, InputText: string; out OutputText: string): Boolean;
var
SecAttr: TSecurityAttributes;
StdOutRead, StdOutWrite: THandle;
StdInRead, StdInWrite: THandle;
StartupInfo: TStartupInfo;
ProcInfo: TProcessInformation;
Buffer: array [0..4095] of AnsiChar;
BytesRead: Cardinal;
ReadOK: BOOL;
Stream: TStringStream;
InheritHandles: Boolean;
BytesWritten: Cardinal;
Utf8Bytes: TBytes;
begin
Result := False;
OutputText := '';
Stream := TStringStream.Create('', TEncoding.UTF8);
try
SecAttr.nLength := SizeOf(SecAttr);
SecAttr.bInheritHandle := True;
SecAttr.lpSecurityDescriptor := nil;
if not CreatePipe(StdOutRead, StdOutWrite, @SecAttr, 0) then Exit;
try
if not SetHandleInformation(StdOutRead, HANDLE_FLAG_INHERIT, 0) then Exit;
if not CreatePipe(StdInRead, StdInWrite, @SecAttr, 0) then Exit;
try
if not SetHandleInformation(StdInWrite, HANDLE_FLAG_INHERIT, 0) then Exit;
ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
StartupInfo.cb := SizeOf(StartupInfo);
StartupInfo.hStdOutput := StdOutWrite;
StartupInfo.hStdError := StdOutWrite;
StartupInfo.hStdInput := StdInRead;
StartupInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
StartupInfo.wShowWindow := SW_HIDE;
InheritHandles := True;
if not CreateProcess(PChar(Exe), PChar('"' + Exe + '" ' + Args), nil, nil,
InheritHandles, CREATE_NO_WINDOW, nil, nil, StartupInfo, ProcInfo) then Exit;
CloseHandle(StdOutWrite);
CloseHandle(StdInRead);
if InputText <> '' then
begin
Utf8Bytes := TEncoding.UTF8.GetBytes(InputText + #10);
WriteFile(StdInWrite, Utf8Bytes[0], Length(Utf8Bytes), BytesWritten, nil);
end;
CloseHandle(StdInWrite);
repeat
ReadOK := ReadFile(StdOutRead, Buffer, SizeOf(Buffer), BytesRead, nil);
if ReadOK and (BytesRead > 0) then
Stream.Write(Buffer, BytesRead);
until not ReadOK or (BytesRead = 0);
WaitForSingleObject(ProcInfo.hProcess, INFINITE);
CloseHandle(ProcInfo.hThread);
CloseHandle(ProcInfo.hProcess);
OutputText := Stream.DataString;
Result := True;
finally
CloseHandle(StdInWrite);
CloseHandle(StdInRead);
end;
finally
CloseHandle(StdOutRead);
CloseHandle(StdOutWrite);
end;
finally
Stream.Free;
end;
end;
procedure TTTS.TextToSpeech(const aText: string; isDeleteFile: Boolean);
var
Args, Text, Output, WavFile: string;
Lines: TStringList;
i: Integer;
begin
if (FPiperExePath = '') or (FCurrentModel = '') then
raise Exception.Create('Piper.exe или модель не указаны.');
Text := Trim(aText);
if Text = '' then Exit;
// Формируем аргументы - пусть Piper сохраняет wav в текущую папку с уникальным именем
Args := '--model "' + FCurrentModel + '" -f o.wav';
if not RunProcessCaptureOutput(FPiperExePath, Args, Text, Output) then
begin
Exit;
end;
Lines := TStringList.Create;
try
Lines.Text := Output;
WavFile := '';
for i := Lines.Count - 1 downto 0 do
if (Pos('.wav', LowerCase(Lines[i])) > 0) and FileExists(Trim(Lines[i])) then
begin
WavFile := Trim(Lines[i]);
Break;
end;
if WavFile <> '' then
begin
// Запускаем в отдельном потоке, чтобы отследить окончание
TThread.CreateAnonymousThread(
procedure
begin
PlaySound(PChar(WavFile), 0, SND_FILENAME); // без SND_ASYNC — ждём окончания
if isDeleteFile then
DeleteFile(WavFile);
TThread.Synchronize(nil,
procedure
begin
DoPlayFinished;
end
);
end
).Start;
end;
finally
Lines.Free;
end;
end;
end.