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));