Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/ui/Features/Video/SpeechToText/MissingSharedLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@ public static class MissingSharedLibrary
return name.Length == 0 ? null : name;
}

/// <summary>
/// True for libraries that are part of an engine download rather than something the user
/// installs from their distro. A missing one means the engine folder is incomplete - which
/// is what happened to the whisper.cpp Linux archives in issue #13680, where whisper-cli
/// shipped without the libwhisper.so.1 and libggml.so.0 it links against.
/// </summary>
public static bool IsBundledWithEngine(string? libraryName)
{
if (string.IsNullOrEmpty(libraryName))
{
return false;
}

// dyld reports the full install name, e.g. "@rpath/libwhisper.1.dylib"
var name = libraryName;
var slash = name.LastIndexOfAny(new[] { '/', '\\' });
if (slash >= 0)
{
name = name.Substring(slash + 1);
}

foreach (var prefix in BundledLibraryPrefixes)
{
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

private static readonly string[] BundledLibraryPrefixes =
{
"libwhisper", // whisper.cpp
"libggml", // whisper.cpp / qwen3-asr.cpp / chatllm.cpp - the ggml core and its backends
"libllama", // llama.cpp based engines
"libmtmd", // llama.cpp multimodal helper
};

private static string? GetAfterMarker(string line, string marker)
{
var index = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
Expand Down
36 changes: 36 additions & 0 deletions src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,18 @@ private void ProcessChatLlmTranscription(SeAudioToText settings, ChatLlmCppEngin
/// </summary>
private static string GetMissingSharedLibraryMessage(string libraryName)
{
// Libraries that ship inside the engine download itself. Telling the user to install
// these with their package manager is a dead end - no distro packages them, and the
// real cause is a bad or incomplete engine folder (issue #13680).
if (MissingSharedLibrary.IsBundledWithEngine(libraryName))
{
return
$"The speech to text engine could not start - the shared library \"{libraryName}\" is missing.{Environment.NewLine}{Environment.NewLine}" +
"This library is part of the engine download, so the installed engine is incomplete." +
$"{Environment.NewLine}{Environment.NewLine}" +
"Re-download the engine (Download button next to the engine) and try again.";
}

var message =
$"The speech to text engine could not start - the shared library \"{libraryName}\" is missing.{Environment.NewLine}{Environment.NewLine}" +
"Install it with your package manager and try again.";
Expand Down Expand Up @@ -3674,6 +3686,26 @@ private static Process StartEngineProcess(string executable, string arguments, D
return p;
}

/// <summary>
/// Puts the engine folder on the dynamic loader's search path, so an engine that ships its
/// own shared libraries next to the executable can find them.
///
/// Windows resolves DLLs from the executable's own directory, so this is a Linux/macOS-only
/// concern - and there setting WorkingDirectory is not enough, because the loader does not
/// search the working directory. The libraries are supposed to carry an $ORIGIN/@loader_path
/// RPATH that makes this unnecessary, but whisper.cpp archives shipped for four releases
/// without one (issue #13680), so set it as a belt-and-braces measure.
/// </summary>
private static void AddEngineFolderToLibrarySearchPath(ProcessStartInfo startInfo, string engineFolder)
{
var variable = OperatingSystem.IsMacOS() ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH";
var existing = startInfo.EnvironmentVariables[variable];

startInfo.EnvironmentVariables[variable] = string.IsNullOrEmpty(existing)
? engineFolder
: engineFolder + Path.PathSeparator + existing;
}

private Process GetWhisperProcess(
ISpeechToTextEngine engine,
string waveFileName,
Expand Down Expand Up @@ -3911,6 +3943,10 @@ or WhisperChoice.CppVulkan or WhisperChoice.CppCuBlasLib or WhisperChoice.ConstM
process.StartInfo.EnvironmentVariables["Path"]?.TrimEnd(';') + ";" + whisperFolder;
}
}
else if (!string.IsNullOrEmpty(whisperFolder))
{
AddEngineFolderToLibrarySearchPath(process.StartInfo, whisperFolder);
}

if (settings.WhisperChoice != WhisperChoice.Cpp &&
settings.WhisperChoice != WhisperChoice.CppCuBlas &&
Expand Down
69 changes: 56 additions & 13 deletions src/ui/Logic/Download/DownloadHashManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,17 @@ public static class WhisperCpp

// Hashes of the unpacked main executable (whisper-cli / whisper-cli.exe) — used to detect
// the installed version when no sidecar is present (e.g. installs from older SE builds).
// Linux Vulkan and Linux CUDA produce identical whisper-cli binaries (the backend lives in
// libggml-*.so), so executable-hash fallback is intentionally Windows + Mac only.
// The Linux Vulkan and CUDA builds shipped an identical whisper-cli up to v1.8.4 (the
// backend lives in libggml-*.so), and they have differed since v1.8.5. Either way the
// lookup is unambiguous: the key comes from the engine choice being asked about, and each
// backend has its own install folder, so a hash shared by both lists is simply listed
// under both.
public const string WindowsBlasExecutable = "WhisperCpp.Windows.Blas.Executable";
public const string WindowsCuBlasExecutable = "WhisperCpp.Windows.CuBlas.Executable";
public const string WindowsVulkanExecutable = "WhisperCpp.Windows.Vulkan.Executable";
public const string MacOsExecutable = "WhisperCpp.MacOs.Executable";
public const string LinuxVulkanExecutable = "WhisperCpp.Linux.Vulkan.Executable";
public const string LinuxCudaExecutable = "WhisperCpp.Linux.Cuda.Executable";
}

// For each key, hashes are ordered newest-first. Index 0 is the latest known release.
Expand Down Expand Up @@ -1818,19 +1823,25 @@ public static class WhisperCpp
"49ef4acfaef0b4989885c258f22eb1355592c5f343897508899a2b598cd683bf", // whispercpp-185 / v1.8.5
"81dbd530f21a6daf10b1c9cece61d7e56d774e3bcb3d21af4d17299caa532a4d", // whispercpp-184 / v1.8.4
},
// whispercpp-184 through -191 shipped whisper-cli without the libwhisper.so.1 /
// libggml.so.0 it links against, so every one of those installs is broken and the
// user needs the -r2 rebuild (issue #13680). They stay listed so an existing install
// is still identified rather than reported as unknown.
[WhisperCpp.LinuxVulkan] = new[]
{
"7969c5a0ba912d0b0d8aaa2bdf911ca7896ef97a89e293d2596a96022c839e80", // whispercpp-191 / v1.9.1 (current download URL)
"10aed3a2b28e5ad40fee8267d554f0824943e50e75bfe7bceb7c16b6e3fe8a45", // whispercpp-186 / v1.8.6
"c385a01228f85764cfd9b6072e078d03e44f151d71ab145e912425e5cc9a7c8e", // whispercpp-185 / v1.8.5
"7a7d131b5fbb605fef6a8d39b6f2480480d8a26ec8a1dbec3b3740485023158d", // whispercpp-184 / v1.8.4
"0943f97f58ca98aafa26659e7c4d8d87f474c74f966f0d9ae91491989a32cf2b", // whispercpp-191-r2 / v1.9.1 (current download URL)
"7969c5a0ba912d0b0d8aaa2bdf911ca7896ef97a89e293d2596a96022c839e80", // whispercpp-191 / v1.9.1 (missing shared libraries)
"10aed3a2b28e5ad40fee8267d554f0824943e50e75bfe7bceb7c16b6e3fe8a45", // whispercpp-186 / v1.8.6 (missing shared libraries)
"c385a01228f85764cfd9b6072e078d03e44f151d71ab145e912425e5cc9a7c8e", // whispercpp-185 / v1.8.5 (missing shared libraries)
"7a7d131b5fbb605fef6a8d39b6f2480480d8a26ec8a1dbec3b3740485023158d", // whispercpp-184 / v1.8.4 (missing shared libraries)
},
[WhisperCpp.LinuxCuda] = new[]
{
"19a232255838c77c9bcddf220292d96dfb62b9a8da1e66ed402961f6a41b1661", // whispercpp-191 / v1.9.1 (current download URL)
"b8922f7fc25ff4f602c887655882c4114c005177017335f39181cc0417f0cb03", // whispercpp-186 / v1.8.6
"1aee5fddee30f8486275d3b2bd1d568e92615e7b1b063d46c635cb9f44d7d13b", // whispercpp-185 / v1.8.5
"708ea1c502ac5082d4eb9afc86c8adb9d67d76da25e70fd81cb6fae2cfcf00ce", // whispercpp-184 / v1.8.4
"02129bac653d00d85ef110c24c37eb56d5470d4fee432a09f55ac9fbc63ff768", // whispercpp-191-r2 / v1.9.1 (current download URL)
"19a232255838c77c9bcddf220292d96dfb62b9a8da1e66ed402961f6a41b1661", // whispercpp-191 / v1.9.1 (missing shared libraries)
"b8922f7fc25ff4f602c887655882c4114c005177017335f39181cc0417f0cb03", // whispercpp-186 / v1.8.6 (missing shared libraries)
"1aee5fddee30f8486275d3b2bd1d568e92615e7b1b063d46c635cb9f44d7d13b", // whispercpp-185 / v1.8.5 (missing shared libraries)
"708ea1c502ac5082d4eb9afc86c8adb9d67d76da25e70fd81cb6fae2cfcf00ce", // whispercpp-184 / v1.8.4 (missing shared libraries)
},

// SHA-256 of whisper-cli / whisper-cli.exe extracted from each archive above.
Expand Down Expand Up @@ -1862,6 +1873,25 @@ public static class WhisperCpp
"0fd752e0384484eb3a72ce644135f20963879e80624b23f7f739eda187a23359", // whispercpp-185 / v1.8.5
"11d902af004d1e79538f8f801b4a42ac3a21094370c9063f67d885d04dccdd96", // whispercpp-184 / v1.8.4
},

// The v1.8.4 hash is the same in both Linux lists on purpose - that release shipped a
// single whisper-cli for both backends. See the note on LinuxVulkanExecutable.
[WhisperCpp.LinuxVulkanExecutable] = new[]
{
"da61c0c1910c103cf8dea855855f072249689ee2310f375e2b79615cbd012c05", // whispercpp-191-r2 / v1.9.1 (current download URL)
"782fac61b9bcfe8f6db22564bb5a2cda2c22550b9ef38064ec5f188bc86dfe79", // whispercpp-191 / v1.9.1 (missing shared libraries)
"5a2343777fe57327c8956d836d1515ac422d2e7f9fd33ce4e7e62cfe4cd33cbd", // whispercpp-186 / v1.8.6 (missing shared libraries)
"e9d5318d513f9dc23ae9fe35cd2783e8def5c66642079dc6a5074b9e5f0cd61c", // whispercpp-185 / v1.8.5 (missing shared libraries)
"315f46514fc09a4fefe2f7b6ae95cfac5441a03b8be04eed1b5f567d5ccc38de", // whispercpp-184 / v1.8.4 (missing shared libraries)
},
[WhisperCpp.LinuxCudaExecutable] = new[]
{
"3a4d717745c2d8cf19ca7a954c29c0349a7f158d93a6597bb86e155f0735474a", // whispercpp-191-r2 / v1.9.1 (current download URL)
"16a838ae67e248020b9bc65b8584fcc113cf18167c9cfc0a2d98e194fa52cf95", // whispercpp-191 / v1.9.1 (missing shared libraries)
"330ff60cbabd8e77137000d05905cabca90e299c455a3282948c9edddf28bcd7", // whispercpp-186 / v1.8.6 (missing shared libraries)
"33bad46e07f0d9bb64fc03ca8c7047b212482c1f45536290f80bb0fe716c9775", // whispercpp-185 / v1.8.5 (missing shared libraries)
"315f46514fc09a4fefe2f7b6ae95cfac5441a03b8be04eed1b5f567d5ccc38de", // whispercpp-184 / v1.8.4 (missing shared libraries)
},
};

/// <summary>
Expand Down Expand Up @@ -2432,9 +2462,10 @@ public static bool IsQwen3AsrCppVulkanInstall(string installFolder)

/// <summary>
/// Resolves the WhisperCpp executable-hash key for the given engine choice on the current OS.
/// Used as a fallback when no sidecar hash exists alongside the install.
/// Returns null on Linux: the same whisper-cli binary ships in both the Vulkan and CUDA archives,
/// so executable hashing cannot disambiguate.
/// Used as a fallback when no sidecar hash exists alongside the install - without it a Linux
/// install predating the sidecar reports Unknown, so the user is never told their engine is
/// outdated. That mattered for issue #13680, where every Linux build from v1.8.4 to v1.9.1 is
/// broken and the update prompt is how affected users learn to re-download.
/// </summary>
public static string? ResolveWhisperCppExecutableKey(string? whisperChoice)
{
Expand All @@ -2459,6 +2490,18 @@ public static bool IsQwen3AsrCppVulkanInstall(string installFolder)
return whisperChoice == WhisperChoice.Cpp ? WhisperCpp.MacOsExecutable : null;
}

if (OperatingSystem.IsLinux())
{
// Same mapping as ResolveWhisperCppKey: the default Cpp backend on Linux is the
// SE-packaged Vulkan build.
return whisperChoice switch
{
WhisperChoice.Cpp => WhisperCpp.LinuxVulkanExecutable,
WhisperChoice.CppCuBlas => WhisperCpp.LinuxCudaExecutable,
_ => null,
};
}

return null;
}

Expand Down
10 changes: 8 additions & 2 deletions src/ui/Logic/Download/WhisperDownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ public class WhisperDownloadService : IWhisperDownloadService
private const string WindowsUrl = "https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.1/whisper-blas-bin-x64.zip";
private const string MacArmUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-mac.zip";
private const string MacX64Url = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-mac.zip";
private const string LinuxUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-vulkan-linux64.zip";
// The Linux archives point at whispercpp-191-r2, not whispercpp-191: the original v1.9.1
// Linux zips (like v1.8.4 and v1.8.6 before them) shipped whisper-cli without the
// libwhisper.so.1 / libggml.so.0 it links against, so the engine died on startup with
// "error while loading shared libraries" (issue #13680). The -r2 rebuild is the same
// whisper.cpp v1.9.1, repackaged with the libraries under their SONAMEs and $ORIGIN
// RPATHs. Windows and macOS were unaffected and still use whispercpp-191.
private const string LinuxUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191-r2/whisper-vulkan-linux64.zip";

private const string WindowsUrlCuBlass = "https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.1/whisper-cublas-12.4.0-bin-x64.zip";
private const string WindowsUrlCppVulkan = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-vulkan-x64.zip";
private const string LinuxUrlCuBlass = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-cuda-linux64.zip";
private const string LinuxUrlCuBlass = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191-r2/whisper-cuda-linux64.zip";

private const string DownloadUrlConstMe = "https://github.com/Const-me/Whisper/releases/download/1.12.0/cli.zip";
private const string SileroVadUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-184/ggml-silero-v6.2.0.zip";
Expand Down
34 changes: 34 additions & 0 deletions tests/UI/Features/Video/SpeechToText/MissingSharedLibraryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,38 @@ public void GetName_UnrelatedOutput_ReturnsNull(string? line)
{
Assert.Null(MissingSharedLibrary.GetName(line));
}

[Fact]
public void GetName_WhisperCppLoaderError_ReturnsLibraryName()
{
// The exact line from issue #13680 (EndeavourOS, whisper.cpp)
const string line = "/home/user/.config/Subtitle Edit/SpeechToText/Cpp/whisper-cli: " +
"error while loading shared libraries: libwhisper.so.1: " +
"cannot open shared object file: No such file or directory";

Assert.Equal("libwhisper.so.1", MissingSharedLibrary.GetName(line));
}

[Theory]
[InlineData("libwhisper.so.1")]
[InlineData("libggml.so.0")]
[InlineData("libggml-base.so.0")]
[InlineData("libllama.so")]
[InlineData("@rpath/libwhisper.1.dylib")]
public void IsBundledWithEngine_EngineOwnedLibrary_ReturnsTrue(string libraryName)
{
Assert.True(MissingSharedLibrary.IsBundledWithEngine(libraryName));
}

[Theory]
[InlineData("libopenblas.so.0")]
[InlineData("libvulkan.so.1")]
[InlineData("libcudart.so.12")]
[InlineData("libstdc++.so.6")]
[InlineData("")]
[InlineData(null)]
public void IsBundledWithEngine_SystemLibrary_ReturnsFalse(string? libraryName)
{
Assert.False(MissingSharedLibrary.IsBundledWithEngine(libraryName));
}
}
Loading
Loading