diff --git a/src/ui/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAligners.cs b/src/ui/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAligners.cs index 70f762d8e38..ecce932dc3a 100644 --- a/src/ui/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAligners.cs +++ b/src/ui/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAligners.cs @@ -84,7 +84,9 @@ double Score(ForcedAlignerOption option) return CanaryLanguages.Contains(language) || language.Length == 0 ? 1 : 3; } - if (option.Choice == ForcedAlignerOption.Qwen3Choice) + // Ultra is a fine-tune of the same model with the same languages. Not yet measured on + // re-timing windows, so it ties with Qwen3 and stays behind it (stable sort). + if (option.Choice is ForcedAlignerOption.Qwen3Choice or ForcedAlignerOption.Qwen3UltraChoice) { return Qwen3Languages.Contains(language) ? 2 : 4; } diff --git a/src/ui/Features/Video/SpeechToText/Engines/ForcedAlignerOption.cs b/src/ui/Features/Video/SpeechToText/Engines/ForcedAlignerOption.cs index 017c61e63b0..f9f0f07fd3e 100644 --- a/src/ui/Features/Video/SpeechToText/Engines/ForcedAlignerOption.cs +++ b/src/ui/Features/Video/SpeechToText/Engines/ForcedAlignerOption.cs @@ -9,6 +9,7 @@ public class ForcedAlignerOption public const string BuiltInChoice = "built-in"; public const string CanaryCtcChoice = "canary-ctc"; public const string Qwen3Choice = "qwen3-forced"; + public const string Qwen3UltraChoice = "qwen3-forced-ultra"; // wav2vec2 forced aligner choices. The choice string mirrors CrispASR's // `-am wav2vec2-aligner-` alias so the persisted setting is self- @@ -78,6 +79,20 @@ public static ForcedAlignerOption Qwen3() => "https://huggingface.co/cstr/qwen3-forced-aligner-0.6b-GGUF/resolve/main/qwen3-forced-aligner-0.6b-q8_0.gguf", "986 MB"); + /// + /// Qwen3-ForcedAligner-Ultra (17slever17, Apache-2.0): the 0.6B aligner fine-tuned on + /// hand-corrected word boundaries. Same architecture and 5000-class timestamp head, so + /// CrispASR runs it through its Qwen3 aligner path; the file name must keep + /// "forced-aligner" for CrispASR to pick that path. Only an F16 conversion exists, and it + /// leaves out the optional local boundary-refinement head. + /// + public static ForcedAlignerOption Qwen3Ultra() => + new("Qwen3 forced aligner Ultra", + Qwen3UltraChoice, + "qwen3-forced-aligner-ultra-f16.gguf", + "https://huggingface.co/cstr/qwen3-forced-aligner-ultra-GGUF/resolve/main/qwen3-forced-aligner-ultra-f16.gguf", + "1.84 GB"); + /// /// 12 language-specific wav2vec2 CTC forced aligners shipped by CrispASR /// ("WhisperX aligner zoo"). Same Q4_K-quantized GGUFs that WhisperX itself @@ -199,6 +214,7 @@ public static IReadOnlyList All() BuiltIn(), CanaryCtc(), Qwen3(), + Qwen3Ultra(), }; list.AddRange(Wav2Vec2All()); return list; @@ -220,6 +236,7 @@ public static ForcedAlignerOption FromChoice(string? choice) { CanaryCtcChoice => CanaryCtc(), Qwen3Choice => Qwen3(), + Qwen3UltraChoice => Qwen3Ultra(), _ => BuiltIn(), }; } diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs index d73efa5b1c5..2ad01f94afb 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs @@ -637,6 +637,7 @@ private void UpdateForcedAlignerUi() } newOptions.Add(ForcedAlignerOption.CanaryCtc()); newOptions.Add(ForcedAlignerOption.Qwen3()); + newOptions.Add(ForcedAlignerOption.Qwen3Ultra()); // wav2vec2 "WhisperX aligner zoo" — 12 language-specific CTC aligners // that work on top of any Crisp ASR backend via `-am `. newOptions.AddRange(ForcedAlignerOption.Wav2Vec2All()); diff --git a/tests/UI/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAlignersTests.cs b/tests/UI/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAlignersTests.cs index 126646e8f43..6968aa497d6 100644 --- a/tests/UI/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAlignersTests.cs +++ b/tests/UI/Features/Tools/ImproveTimeCodes/ImproveTimeCodesAlignersTests.cs @@ -9,8 +9,8 @@ public class ImproveTimeCodesAlignersTests [InlineData("en", ForcedAlignerOption.CanaryCtcChoice, ForcedAlignerOption.Wav2Vec2EnChoice, ForcedAlignerOption.Qwen3Choice)] [InlineData("de", ForcedAlignerOption.Wav2Vec2DeChoice, ForcedAlignerOption.CanaryCtcChoice, ForcedAlignerOption.Qwen3Choice)] [InlineData("da", ForcedAlignerOption.CanaryCtcChoice, ForcedAlignerOption.Qwen3Choice, null)] - [InlineData("ja", ForcedAlignerOption.Wav2Vec2JaChoice, ForcedAlignerOption.Qwen3Choice, ForcedAlignerOption.CanaryCtcChoice)] - [InlineData("ko", ForcedAlignerOption.Qwen3Choice, ForcedAlignerOption.CanaryCtcChoice, null)] + [InlineData("ja", ForcedAlignerOption.Wav2Vec2JaChoice, ForcedAlignerOption.Qwen3Choice, ForcedAlignerOption.Qwen3UltraChoice)] + [InlineData("ko", ForcedAlignerOption.Qwen3Choice, ForcedAlignerOption.Qwen3UltraChoice, ForcedAlignerOption.CanaryCtcChoice)] [InlineData("", ForcedAlignerOption.CanaryCtcChoice, ForcedAlignerOption.Qwen3Choice, null)] public void Rank_PutsTheBestAlignersForTheLanguageFirst(string language, string first, string second, string? third) { @@ -34,6 +34,16 @@ public void Rank_OffersEveryAlignerOnce_AndNeverTheBuiltInOne() Assert.DoesNotContain(ranked, o => o.IsBuiltIn); } + [Fact] + public void Qwen3Ultra_RoundTripsThroughItsChoice_AndKeepsTheAlignerFileName() + { + var option = ForcedAlignerOption.FromChoice(ForcedAlignerOption.Qwen3UltraChoice); + + Assert.Equal(ForcedAlignerOption.Qwen3UltraChoice, option.Choice); + // CrispASR selects its Qwen3 aligner path from "forced-aligner" in the file name. + Assert.Contains("forced-aligner", option.FileName); + } + [Theory] [InlineData("en", "phonon2-q8_0.gguf")] [InlineData("EN", "phonon2-q8_0.gguf")]