diff --git a/src/ui/Features/Video/TextToSpeech/Engines/AudioCppRuntime.cs b/src/ui/Features/Video/TextToSpeech/Engines/AudioCppRuntime.cs index db0a652831..caaa0c17d3 100644 --- a/src/ui/Features/Video/TextToSpeech/Engines/AudioCppRuntime.cs +++ b/src/ui/Features/Video/TextToSpeech/Engines/AudioCppRuntime.cs @@ -1,4 +1,4 @@ -using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Config; using System; using System.IO; @@ -30,6 +30,25 @@ public static string GetSetEngineFolder() public static string GetServerExecutable() => Path.Combine(GetSetEngineFolder(), OperatingSystem.IsWindows() ? "audiocpp_server.exe" : "audiocpp_server"); + /// + /// Identity of the server binary on disk (size + last write), or empty when it is missing. + /// The engines record it when they launch a server and restart when it changes: an engine + /// update extracts a new binary while the old process keeps running, and reusing that + /// process means the new build (and any family it adds) is never actually used. + /// + public static string GetServerExecutableStamp() + { + try + { + var fi = new FileInfo(GetServerExecutable()); + return fi.Exists ? $"{fi.Length}:{fi.LastWriteTimeUtc.Ticks}" : string.Empty; + } + catch + { + return string.Empty; + } + } + /// /// ggml backend the installed archive was built for, stored at install time. The setting /// keeps its historical IndexTts25-prefixed name — it predates the runtime being shared — diff --git a/src/ui/Features/Video/TextToSpeech/Engines/FishTtsAudioCpp.cs b/src/ui/Features/Video/TextToSpeech/Engines/FishTtsAudioCpp.cs index 78036f804f..20d7217625 100644 --- a/src/ui/Features/Video/TextToSpeech/Engines/FishTtsAudioCpp.cs +++ b/src/ui/Features/Video/TextToSpeech/Engines/FishTtsAudioCpp.cs @@ -1,4 +1,4 @@ -using Nikse.SubtitleEdit.Features.Video.TextToSpeech.ModelLicense; +using Nikse.SubtitleEdit.Features.Video.TextToSpeech.ModelLicense; using Nikse.SubtitleEdit.Features.Video.TextToSpeech.Voices; using Nikse.SubtitleEdit.Logic.Config; using Nikse.SubtitleEdit.Logic.Download; @@ -138,6 +138,7 @@ public static string GetModelFileName(string? modelKey) => // Only the model and the backend are baked into the running server — voice is per request. private static string? _serverModelKey; private static string? _serverBackend; + private static string? _serverExeStamp; private static bool _processExitHooked; private static readonly StringBuilder _serverLog = new(); @@ -512,10 +513,12 @@ public async Task Speak( private static async Task EnsureServerRunningAsync(string modelKey, CancellationToken ct) { var backend = AudioCppRuntime.GetBackend(); + var exeStamp = AudioCppRuntime.GetServerExecutableStamp(); if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -525,7 +528,8 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation { if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -596,6 +600,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverPort = port; _serverModelKey = modelKey; _serverBackend = backend; + _serverExeStamp = AudioCppRuntime.GetServerExecutableStamp(); HookProcessExitOnce(); // The config uses lazy_load, so /health answers within a second or two — the 5.9 GB @@ -616,6 +621,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; throw new InvalidOperationException( $"audiocpp_server exited during startup (code {exitCode}). " + AudioCppRuntime.DescribeStartupExit(exitCode, backend) @@ -784,6 +790,7 @@ private static void StopServerInternal() _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; if (p == null) { return; diff --git a/src/ui/Features/Video/TextToSpeech/Engines/HiggsTtsAudioCpp.cs b/src/ui/Features/Video/TextToSpeech/Engines/HiggsTtsAudioCpp.cs index 635752da22..b9fc13df1f 100644 --- a/src/ui/Features/Video/TextToSpeech/Engines/HiggsTtsAudioCpp.cs +++ b/src/ui/Features/Video/TextToSpeech/Engines/HiggsTtsAudioCpp.cs @@ -1,4 +1,4 @@ -using Nikse.SubtitleEdit.Features.Video.TextToSpeech.ModelLicense; +using Nikse.SubtitleEdit.Features.Video.TextToSpeech.ModelLicense; using Nikse.SubtitleEdit.Features.Video.TextToSpeech.Voices; using Nikse.SubtitleEdit.Logic.Config; using Nikse.SubtitleEdit.Logic.Download; @@ -140,6 +140,7 @@ public static string GetModelFileName(string? modelKey) => // Only the model and the backend are baked into the running server — voice is per request. private static string? _serverModelKey; private static string? _serverBackend; + private static string? _serverExeStamp; private static bool _processExitHooked; private static readonly StringBuilder _serverLog = new(); @@ -500,10 +501,12 @@ public async Task Speak( private static async Task EnsureServerRunningAsync(string modelKey, CancellationToken ct) { var backend = AudioCppRuntime.GetBackend(); + var exeStamp = AudioCppRuntime.GetServerExecutableStamp(); if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -513,7 +516,8 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation { if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -584,6 +588,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverPort = port; _serverModelKey = modelKey; _serverBackend = backend; + _serverExeStamp = AudioCppRuntime.GetServerExecutableStamp(); HookProcessExitOnce(); // The config uses lazy_load, so /health answers within a second or two — the 4.7 GB @@ -604,6 +609,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; throw new InvalidOperationException( $"audiocpp_server exited during startup (code {exitCode}). " + AudioCppRuntime.DescribeStartupExit(exitCode, backend) @@ -772,6 +778,7 @@ private static void StopServerInternal() _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; if (p == null) { return; diff --git a/src/ui/Features/Video/TextToSpeech/Engines/IndexTts25AudioCpp.cs b/src/ui/Features/Video/TextToSpeech/Engines/IndexTts25AudioCpp.cs index 0cc89c386d..e3a1ba65bd 100644 --- a/src/ui/Features/Video/TextToSpeech/Engines/IndexTts25AudioCpp.cs +++ b/src/ui/Features/Video/TextToSpeech/Engines/IndexTts25AudioCpp.cs @@ -1,4 +1,4 @@ -using Nikse.SubtitleEdit.Features.Video.TextToSpeech.Voices; +using Nikse.SubtitleEdit.Features.Video.TextToSpeech.Voices; using Nikse.SubtitleEdit.Logic.Config; using Nikse.SubtitleEdit.Logic.Download; using Nikse.SubtitleEdit.Logic.Media; @@ -116,6 +116,7 @@ public static string GetModelFileName(string? modelKey) => // Only the model and the backend are baked into the running server — voice is per request. private static string? _serverModelKey; private static string? _serverBackend; + private static string? _serverExeStamp; private static bool _processExitHooked; private static readonly StringBuilder _serverLog = new(); @@ -522,10 +523,12 @@ public async Task Speak( private static async Task EnsureServerRunningAsync(string modelKey, CancellationToken ct) { var backend = GetBackend(); + var exeStamp = AudioCppRuntime.GetServerExecutableStamp(); if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -535,7 +538,8 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation { if (_serverProcess is { HasExited: false } && _serverPort != 0 && string.Equals(_serverModelKey, modelKey, StringComparison.OrdinalIgnoreCase) - && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase)) + && string.Equals(_serverBackend, backend, StringComparison.OrdinalIgnoreCase) + && string.Equals(_serverExeStamp, exeStamp, StringComparison.Ordinal)) { return; } @@ -608,6 +612,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverPort = port; _serverModelKey = modelKey; _serverBackend = backend; + _serverExeStamp = AudioCppRuntime.GetServerExecutableStamp(); HookProcessExitOnce(); // The config uses lazy_load, so /health answers within a second or two — the 3.3 GB @@ -628,6 +633,7 @@ private static async Task EnsureServerRunningAsync(string modelKey, Cancellation _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; throw new InvalidOperationException( $"audiocpp_server exited during startup (code {exitCode}). " + DescribeStartupExit(exitCode, backend) @@ -798,6 +804,7 @@ private static void StopServerInternal() _serverLaunchCommand = null; _serverModelKey = null; _serverBackend = null; + _serverExeStamp = null; if (p == null) { return; diff --git a/src/ui/Features/Video/TextToSpeech/TtsVoiceInstaller.cs b/src/ui/Features/Video/TextToSpeech/TtsVoiceInstaller.cs index ab00ee2e97..0ec258fe95 100644 --- a/src/ui/Features/Video/TextToSpeech/TtsVoiceInstaller.cs +++ b/src/ui/Features/Video/TextToSpeech/TtsVoiceInstaller.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Nikse.SubtitleEdit.Core.Common; using Nikse.SubtitleEdit.Features.Shared; using Nikse.SubtitleEdit.Features.Video.SpeechToText; @@ -312,6 +312,14 @@ await MessageBox.Show( return false; } + // The three audio.cpp engines share this binary. A server started from the old build + // must go before the archive is extracted: on Windows the running exe cannot be + // overwritten at all, and elsewhere the stale process would keep answering requests + // (and reject any model family the new build added) until Subtitle Edit restarts. + IndexTts25AudioCpp.StopServer(); + HiggsTtsAudioCpp.StopServer(); + FishTtsAudioCpp.StopServer(); + var dlResult = await windowService.ShowDialogAsync( window, vm => vm.StartDownloadIndexTts25AudioCppEngine(backend));