From da62124fe0fb21b9cebe4d317cb842c746baabed Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 15:48:12 +0200 Subject: [PATCH 01/82] Qwen3 ASR: pass the selected language to qwen3-asr-cli qwen3-asr.cpp v0.1.9 forces the spoken language from the prompt when -l is given, the same way the qwen-asr Python package does. Auto-detection is unreliable on short clips (Danish came out as Swedish or Chinese). "auto" keeps detection; older engine builds only used -l for Korean word splitting, so the flag is harmless there. Co-Authored-By: Claude Fable 5.1 --- .../Video/SpeechToText/SpeechToTextViewModel.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs index 848bb2b3c95..82dfd150e92 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs @@ -4788,9 +4788,17 @@ private Process GetWhisperProcess( _engineExitCode = null; var qwen3ExtraArgs = engine.CommandLineParameter; + // qwen3-asr.cpp v0.1.9+ forces the spoken language from the prompt when -l is given + // (like the qwen-asr Python package); auto-detection is unreliable on short clips + // (Danish came out as Swedish or Chinese). Older builds only used -l for Korean word + // splitting, so passing it is harmless there. "auto" keeps detection. + var qwen3LanguageArg = string.IsNullOrWhiteSpace(language) || language.Equals("auto", StringComparison.OrdinalIgnoreCase) + ? string.Empty + : $"-l {language} "; + var qwen3Params = string.IsNullOrWhiteSpace(qwen3ExtraArgs) - ? $"-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\"" - : $"{qwen3ExtraArgs} -m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\""; + ? $"{qwen3LanguageArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\"" + : $"{qwen3ExtraArgs} {qwen3LanguageArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\""; return StartEngineProcess(exe, qwen3Params, dataReceivedHandler); } From 1812210d6a368afc6dfa17734196486806fb0343 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 16:18:28 +0200 Subject: [PATCH 02/82] Qwen3 ASR: bump qwen3-asr.cpp to v0.1.9 (mel filterbank fix) v0.1.9 (niksedk/qwen3-asr.cpp#11, #12) fixes the runtime-generated mel filterbank: it used the HTK mel scale sampled at integer bins while the Whisper feature extractor uses Slaney-scale filters, so the log-mel input was off by up to 1.6 and the model mis-detected languages on short clips. On 600 held-out Danish clips the stock 0.6B GGUF goes from 49.5 to 43.1 WER, and a Danish fine-tune from 26.3 to 18.8 (transformers: 18.6). The release also pads the trailing conv chunk like the reference and forces the language from the prompt when -l is given (the forced aligner shares the fixed filterbank, so timestamps improve as well). New archive and executable hashes prepended for all seven platforms so existing installs are offered the update. Co-Authored-By: Claude Fable 5.1 --- src/ui/Logic/Download/DownloadHashManager.cs | 42 ++++++++++++------- .../Download/Qwen3AsrCppDownloadService.cs | 8 +++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/ui/Logic/Download/DownloadHashManager.cs b/src/ui/Logic/Download/DownloadHashManager.cs index ab04fcb7348..1686a5cc1d2 100644 --- a/src/ui/Logic/Download/DownloadHashManager.cs +++ b/src/ui/Logic/Download/DownloadHashManager.cs @@ -1095,7 +1095,8 @@ public static class WhisperX // recognized and offered the update (issue #11375). [Qwen3AsrCpp.Windows] = new[] { - "ad9c196aa52ceb42a3f46946644e4145fc7e297c84fa54e4cdf7fd9c22556300", // v0.1.8 (current download URL) + "075e2e4a811235842ea7ce16f186c4af994dea39ebf4348a287a5b3a56876594", // v0.1.9 (current download URL) + "ad9c196aa52ceb42a3f46946644e4145fc7e297c84fa54e4cdf7fd9c22556300", // v0.1.8 "2899cfd563b8343fde6b74b617f32b17a26b611e1187c91ba416334364075af7", // v0.1.7 "4b0e23b2124c3e64c00dbc727bdfd86f7deca4a802edec9e6470701892371942", // v0.1.6 "4382904b6110e3ba636f927f484a0545a66783f4fd29fd514fd3ce05ba0f0b30", // v0.1.5 @@ -1107,7 +1108,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsVulkan] = new[] { - "a9971a24ba08765568c8a83a547924e42f15bd576b2cf594f6757fff66baf244", // v0.1.8 (current download URL) + "8ab5a8eabd2c2bd76494de5a77fe3970c6406d2cd3f7d550b96ac83436fe54e6", // v0.1.9 (current download URL) + "a9971a24ba08765568c8a83a547924e42f15bd576b2cf594f6757fff66baf244", // v0.1.8 "b1bff40ba1c7f0103c6016c4845481053686761f0c375eb71612d1f07192099b", // v0.1.7 "b5ee87d7f9290f0fec651f727fb015bff14ca4e297df83e46c64f489e2ca64f5", // v0.1.6 "c608f9c00ed5e2f568d75045b9f3079dc2f2b4b064925dce77f53e0626d06100", // v0.1.5 @@ -1117,7 +1119,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacArm64] = new[] { - "a9e9f33ff8ba651591974824e886ecd28a034d00aaf8203365a7198cd5012325", // v0.1.8 (current download URL) + "28c9f90f3e87a511f4e3685d438c862bec06b1e747399c6820a2f3f13ad243b7", // v0.1.9 (current download URL) + "a9e9f33ff8ba651591974824e886ecd28a034d00aaf8203365a7198cd5012325", // v0.1.8 "a4355282cbde03d99ddc1be7724a2e0fd2520ad6d07e875bb4a01ce4f8a83fcd", // v0.1.7 "cf594a18afc7bfa42c8e784d83842c842177310289471ad211ca58e3d3c0642a", // v0.1.6 "e01a1b3aee6105dbaaa5612b77001a1d17e3f3654bd5f0f47fdf9a38e340fdb0", // v0.1.5 @@ -1129,7 +1132,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacX64] = new[] { - "6dbb978b74829c080177f6c70e89b363a8bc24d543b718ad06ecea7a7c833c53", // v0.1.8 (current download URL) + "b44a358669d59d3e354b88554b5898420050542067ef8a046b5c0fee5044e2e0", // v0.1.9 (current download URL) + "6dbb978b74829c080177f6c70e89b363a8bc24d543b718ad06ecea7a7c833c53", // v0.1.8 "8295b7c36fd49fe6073d9a05b126a66ac1a5eaf0cc2768d45a5145f3d6a07fae", // v0.1.7 "ebe8afd2fce42c1dc7780a96f45757dc1eabf1c94dc639cf8a8664cc807f4b78", // v0.1.6 "94aef4605ebc6d0933d0d2d741f994afd2ad51e5a1f4603b1f37cb7425dda565", // v0.1.5 @@ -1140,7 +1144,8 @@ public static class WhisperX }, [Qwen3AsrCpp.Linux] = new[] { - "22b37a1917ca4083df7b57c62497a9f30c237ee13c2eee4b36a46320cf8506ed", // v0.1.8 (current download URL) + "28bcf1df65818674514fa88e7e1845b6bf15141c3931aa48a2f8607ffc7631b6", // v0.1.9 (current download URL) + "22b37a1917ca4083df7b57c62497a9f30c237ee13c2eee4b36a46320cf8506ed", // v0.1.8 "d6843919444f447b5b4cc307565f07cdec4a233e8584f1cb88ded6d5413f5caa", // v0.1.7 "6dccfe589febd6001b37b9be9248227f1278a966c94e86606b159867ca78f5d3", // v0.1.6 "abaae9b655fc8b4c50ab21df4a1e7e7354b69506aff67f0a6a0d2df4eb7a1091", // v0.1.5 @@ -1152,7 +1157,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxVulkan] = new[] { - "d881e30dfc6f5311496355a08668d22d7f592ba4f7120a266abf8024d0f59a24", // v0.1.8 (current download URL) + "4c7bf45a3d79ffe9a3eb5d9508a7121226055c013bd8c20659b9c88c4d9bb0e8", // v0.1.9 (current download URL) + "d881e30dfc6f5311496355a08668d22d7f592ba4f7120a266abf8024d0f59a24", // v0.1.8 "4c3147af8e9077f8758d9ed86c23f213b22ccbc0df1d8990f27308e34279f831", // v0.1.7 "c455b92c8143416abfd201024c1707c0b52350e410908c6b718c8cea4eae44ff", // v0.1.6 "aae44a23a2ccdd6bc0b43e8d0f97651ee215b435e07f330bc19197d715e28e3b", // v0.1.5 @@ -1162,7 +1168,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxArm64] = new[] { - "8d3a1ac745fa60f5576a6db599c7b4b9bc0302ea527cbdab9be3223f80a555e4", // v0.1.8 (current download URL) + "c74f8a79b66c4888fc6701dcb516f06c0ea5ef20f9982cbac78bd0694c008957", // v0.1.9 (current download URL) + "8d3a1ac745fa60f5576a6db599c7b4b9bc0302ea527cbdab9be3223f80a555e4", // v0.1.8 "1a67596e18da7ad5ad2adcca72ce5986033120ba25835493a92b52885a6c1903", // v0.1.7 "4127c2567b6512a4377ff5b3b077468d915fcf76ab156458ec7be81d5e5690e0", // v0.1.6 "b54a89bff6b26ac0d2471f7b1fcdb7e6be6bde6570e358e76daf60f0156516f8", // v0.1.5 @@ -1173,7 +1180,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsExecutable] = new[] { - "4774a1662b8dfd15083e8ffd1e318aeedc86a55931c0bcdec72890547e48c91b", // v0.1.8 (current download URL) + "029b520b2a2e692bdf7a8855bdd6bda80afc55b3ee15d331f5fb952e2dc9eef9", // v0.1.9 (current download URL) + "4774a1662b8dfd15083e8ffd1e318aeedc86a55931c0bcdec72890547e48c91b", // v0.1.8 "0abdde2ebb6cea8059aa8fe3b7ed61277b9c17bfa223715c431320c575e653c6", // v0.1.7 "8f9e0f8c85e2c95fe0627d442dd15908d36ae9c0076ab4109e84c00ec6f2ab78", // v0.1.6 "29dba7a3808eb18f787486104a6d542e32fd32b4c177cc4f6e9c262d5ac1b46d", // v0.1.5 @@ -1185,7 +1193,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsVulkanExecutable] = new[] { - "f0e8dd1d4de3b5a71cfe8e9d04e8e1dbaff58768ccd8b0859279035e63c9c12a", // v0.1.8 (current download URL) + "529222fe88a9f2ea7d7f0fa48c7b5770b150242800b8d441f38144b90e42ee9e", // v0.1.9 (current download URL) + "f0e8dd1d4de3b5a71cfe8e9d04e8e1dbaff58768ccd8b0859279035e63c9c12a", // v0.1.8 "eba7c228d04a3124e48bcb6342bb372a2fc5338a344e2ee8bd3971518ea4e780", // v0.1.7 "998b0577d5d3cbe35917603f86a72acf201f5010757ed611216574eb67d3570e", // v0.1.6 "38546750d753666cc767c4ccafa483e1fff83ddd5feed6f7945a8a35acea3e8f", // v0.1.5 @@ -1195,7 +1204,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacArm64Executable] = new[] { - "26e0cfb663aeb73999e6de0f946e2ddb37cb7a2a28161b41fc9e23ddb8247904", // v0.1.8 (current download URL) + "5ec504cd96f6808b4d9a31e2a9bce9e0c6a2f5548dd8358249c701aa3bdf0bc7", // v0.1.9 (current download URL) + "26e0cfb663aeb73999e6de0f946e2ddb37cb7a2a28161b41fc9e23ddb8247904", // v0.1.8 "0c637dad09da1198c755d75f4d0e52d74443220743ee4c5f396c8a95ecc41d2a", // v0.1.7 "e2cd42381858a853ece9ca6f00eab4fb940b34efbfc5d6b5a332fae6371fb90c", // v0.1.6 "2c8ea51851a6ac70ab4971e0d32c314e17eb916cb6b0ead64fb36761924e4de2", // v0.1.5 @@ -1206,7 +1216,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacX64Executable] = new[] { - "b2e55b521d97a731488caf6439b7ecc1e8166dd20fd8770d341849065cbc8172", // v0.1.8 (current download URL) + "d277fd254bf55797ff63761eda7ce08d94bf8f5dde56d87d4e09f163bc2dca5e", // v0.1.9 (current download URL) + "b2e55b521d97a731488caf6439b7ecc1e8166dd20fd8770d341849065cbc8172", // v0.1.8 "e4b9c56a373656d67ca0ef0ef383ccc75b62b788b323bdd43f922f351a3864c1", // v0.1.7 "eff1cf91866b6168f5544dec6018fe339cd60cb9acba5003fc0142e240f96172", // v0.1.6 "6ab8faa6170c202b0faef8d943f7a1ee9ae7d0e5906c6f40ba7a64944d54762f", // v0.1.5 @@ -1216,7 +1227,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxExecutable] = new[] { - "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 (current download URL) + "df95f8cd851f170a752a0dd84f72880f7ed0a8e546b921bae08911150dff558b", // v0.1.9 (current download URL) + "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 "a02d9e2ba60201ad8e778dca9a9f7ca1f5cafc145955488a1b2c611615b997ba", // v0.1.7 / v0.1.6 "b7d9f1450eba98f8e1a63ae2e9cfbf79c5ceea648d981b31636e25eb0e1d8b0b", // v0.1.5 "1378d1d968446b19452f7463007b6707cbfa703e025b1309650fc928d1972ddc", // v0.1.4 / v0.1.3 @@ -1226,7 +1238,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxVulkanExecutable] = new[] { - "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 (current download URL) + "1d1e99ef0936321216fba2257c4d31fdfaf97779519e8ae333e0295c8b433081", // v0.1.9 (current download URL) + "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 "a02d9e2ba60201ad8e778dca9a9f7ca1f5cafc145955488a1b2c611615b997ba", // v0.1.7 / v0.1.6 "b7d9f1450eba98f8e1a63ae2e9cfbf79c5ceea648d981b31636e25eb0e1d8b0b", // v0.1.5 "1378d1d968446b19452f7463007b6707cbfa703e025b1309650fc928d1972ddc", // v0.1.4 @@ -1235,7 +1248,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxArm64Executable] = new[] { - "ffd6e80c743f5e2b977c2dfd3a7e945c29104fda9d2a43c91a2f3aa0dfef548d", // v0.1.8 (current download URL) + "a1c4c2941b47f64113bbe6f7819d97974eaf3b224e4e28f6761404bd13f47a36", // v0.1.9 (current download URL) + "ffd6e80c743f5e2b977c2dfd3a7e945c29104fda9d2a43c91a2f3aa0dfef548d", // v0.1.8 "fe453c7f30efc717fbfb2b9d4ff4ff5d50f862300d8c390d6e4c0066c31e6c60", // v0.1.7 / v0.1.6 "bcb155a93474b074897c7680a2c2cfdbfe1cf4478652db5a4bec8c83cf688541", // v0.1.5 "a6087e81c1975cecb644391130e2efb3cef3442b12928abd333c8fe856a6226d", // v0.1.4 / v0.1.3 diff --git a/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs b/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs index 366f451ced7..ce7c84e3141 100644 --- a/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs +++ b/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs @@ -25,10 +25,14 @@ public class Qwen3AsrCppDownloadService : IQwen3AsrCppDownloadService // an unreadable host pointer (issue #12815) — and bumps the ggml backend to 0.17.0. v0.1.8 // fixes invalid JSON output ("start": 1,840) on Windows with comma-decimal regional formats // (French, German, ...): the v0.1.6 UTF-8 console setup let the user's LC_NUMERIC leak into - // the JSON timestamp formatting. CPU for every platform, plus Vulkan (GPU) for win64 and + // the JSON timestamp formatting. v0.1.9 fixes the mel filterbank (HTK scale → Slaney scale as + // in the Whisper feature extractor): the log-mel input was off by up to 1.6, which made the + // model mis-detect languages on short clips and cost ~9 WER points versus transformers; it + // also pads the trailing conv chunk like the reference and forces the language from the + // prompt when -l is given. CPU for every platform, plus Vulkan (GPU) for win64 and // linux-x64. When bumping: also prepend the new archive + executable hashes in // DownloadHashManager (Qwen3AsrCpp keys) so the update prompt fires for old installs. - private const string ReleaseUrlBase = "https://github.com/niksedk/qwen3-asr.cpp/releases/download/v0.1.8/"; + private const string ReleaseUrlBase = "https://github.com/niksedk/qwen3-asr.cpp/releases/download/v0.1.9/"; private const string WindowsUrl = ReleaseUrlBase + "qwen3-asr-cpp-win64.zip"; private const string WindowsVulkanUrl = ReleaseUrlBase + "qwen3-asr-cpp-win64-vulkan.zip"; private const string MacArmUrl = ReleaseUrlBase + "qwen3-asr-cpp-mac.zip"; From 7d43d948ece270950bd608c3b314a7f39aa32a71 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 17:03:28 +0200 Subject: [PATCH 03/82] Qwen3 ASR: add Danish fine-tuned 0.6B model Adds qwen3-asr-0.6b-da-q8_0.gguf (https://huggingface.co/niksedk/qwen3-asr-0.6b-danish), a LoRA fine-tune of Qwen3-ASR-0.6B on 399 h of CoRal, NST and FTSpeech, merged and quantised to q8_0. On 600 held-out Danish clips through qwen3-asr-cli v0.1.9 it scores 18.8 WER versus 43.1 for the stock 0.6B model. Needs the v0.1.9 engine (#16244); older engines run it but lose ~7 WER points to the mel filterbank bug. Co-Authored-By: Claude Fable 5.1 --- .../Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs b/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs index 2e416fad936..0e8cfd64d7d 100644 --- a/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs +++ b/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs @@ -81,6 +81,19 @@ public List Models ] }, new WhisperModel + { + // Danish fine-tune of Qwen3-ASR-0.6B (LoRA on 399 h CoRal/NST/FTSpeech, merged): + // 18.8 vs 43.1 WER for the stock 0.6B on 600 held-out Danish clips. Needs + // qwen3-asr.cpp v0.1.9+ (mel filterbank fix). Apache-2.0 with CoRal's + // OpenRAIL-D restrictions (no speech synthesis, no biometric identification). + Name = "qwen3-asr-0.6b-da-q8_0.gguf", + Size = "1.0 GB", + Urls = + [ + "https://huggingface.co/niksedk/qwen3-asr-0.6b-danish/resolve/main/qwen3-asr-0.6b-da-q8_0.gguf" + ] + }, + new WhisperModel { Name = "qwen3-asr-1.7b-f16.gguf", Size = "4.4 GB", From 53139424df30c68332eef60f3037626a2c8ca5dc Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 17:24:22 +0200 Subject: [PATCH 04/82] OCR fix engine: only read a lone slash as "I" for image matchers (#16031) PR #16207 taught the fix engine that a slash standing alone is a slanted italic "I". The fix engine runs on every OCR engine's output, so the title card "PILOTEN / OMGEKOMEN" read by llama.cpp (GLM-OCR) came out as "PILOTEN I OMGEKOMEN" in 5.4.0-beta2. - New IOcrFixEngine.FixImageMatcherMisreads flag, set by the OCR window for nOCR and binary image compare only (next to FillUnknownCharacters). Text engines (Tesseract, Paddle, Ollama, llama.cpp) never fit a glyph to "/" or "#", so the slash and hash misread rules stay off for them. - Even for image matchers a lone slash next to a number ("1 / 2") or between two all-caps words ("PILOTEN / OMGEKOMEN", "AC / DC") is kept as a separator. Co-Authored-By: Claude Fable 5.1 --- src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs | 94 ++++++++++++++++++- src/ui/Features/Ocr/OcrViewModel.cs | 8 +- .../OcrFixHashMisreadAsApostropheTests.cs | 2 +- .../FixEngine/OcrFixSlashMisreadAsLTests.cs | 39 +++++++- .../FixCommonOcrErrorsTests.cs | 1 + 5 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs index ef649135c59..9399046fb2f 100644 --- a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs +++ b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs @@ -26,6 +26,15 @@ public interface IOcrFixEngine /// makes it a dictionary word. Off by default - elsewhere "sh*t" is deliberate censoring. /// bool FillUnknownCharacters { get; set; } + + /// + /// The text comes from an image matcher (nOCR, binary image compare) whose glyph entries can + /// fit the wrong character: a slanted italic "l", "i" or "I" read as "/" ("p/ease", "/ can"), + /// a bold italic apostrophe read as "#" ("He#s"). Off by default - text engines (Tesseract, + /// Paddle, llama.cpp) never produce those, so a "/" or "#" from them is the real thing + /// ("PILOTEN / OMGEKOMEN", "C#"). + /// + bool FixImageMatcherMisreads { get; set; } } public partial class OcrFixEngine : IOcrFixEngine, IDoSpell @@ -47,6 +56,8 @@ public partial class OcrFixEngine : IOcrFixEngine, IDoSpell public bool FillUnknownCharacters { get; set; } + public bool FixImageMatcherMisreads { get; set; } + public OcrFixEngine(ISpellChecker spellCheckManager) { _spellCheckManager = spellCheckManager; @@ -93,8 +104,12 @@ public OcrFixLineResult FixOcrErrors(int index, string text, bool doTryToGuessUn text = FillUnknownCharactersInWords(text); } - text = FixHashMisreadAsApostrophe(text); - text = FixSlashMisreadAsL(text); + if (FixImageMatcherMisreads) + { + text = FixHashMisreadAsApostrophe(text); + text = FixSlashMisreadAsL(text); + } + var replacedLine = ReplaceLineFixes(index, text, wordsToIgnore); if (Configuration.Settings.Tools.OcrFixUseHardcodedRules) { @@ -334,7 +349,8 @@ private string FixHashMisreadAsApostrophe(string text) /// line: every slash to "l", and an "I" after a lowercase letter to "l" ("wi/I", "teI/ing"); /// failing that, the other readings of the slashes ("i" anywhere, "I" at the start of the /// word). Each is kept only when the dictionary or names list accepts the result. A real - /// slash between two words ("and/or", "AC/DC") is left alone. + /// slash between two words ("and/or", "AC/DC") is left alone, and so is a lone slash + /// separating numbers or two all-caps words ("1 / 2", "PILOTEN / OMGEKOMEN" on a title card). /// private string FixSlashMisreadAsL(string text) { @@ -345,7 +361,7 @@ private string FixSlashMisreadAsL(string text) if (LoneSlash().IsMatch(text) && IsSpelledCorrect("I")) { - text = LoneSlash().Replace(text, "I"); + text = LoneSlash().Replace(text, m => IsSeparatorSlash(text, m.Index) ? "/" : "I"); } return WordWithSlash().Replace(text, m => @@ -422,6 +438,76 @@ private string FixSlashMisreadAsL(string text) private const int MaxSlashesToRead = 4; + /// + /// A lone slash next to a number ("1 / 2") or between two all-caps words ("PILOTEN / + /// OMGEKOMEN") is a separator, not a slanted italic "I" - an italic "I" sits between + /// ordinary words ("/ can", "and / will"). + /// + private static bool IsSeparatorSlash(string text, int index) + { + var before = NeighbourWord(text, index, -1); + var after = NeighbourWord(text, index, 1); + if (before.Any(char.IsDigit) || after.Any(char.IsDigit)) + { + return true; + } + + return IsAllCaps(before) && IsAllCaps(after); + } + + private static bool IsAllCaps(string word) + { + return word.Any(char.IsUpper) && !word.Any(char.IsLower); + } + + /// + /// The run of non-whitespace characters before ( -1) or after + /// (+1) , skipping whitespace and tags; empty at a line edge. + /// + private static string NeighbourWord(string text, int index, int direction) + { + var i = index + direction; + while (i >= 0 && i < text.Length) + { + if (char.IsWhiteSpace(text[i])) + { + i += direction; + } + else if (direction < 0 && text[i] == '>') + { + var open = text.LastIndexOf('<', i); + if (open < 0) + { + break; + } + + i = open - 1; + } + else if (direction > 0 && text[i] == '<') + { + var close = text.IndexOf('>', i); + if (close < 0) + { + break; + } + + i = close + 1; + } + else + { + break; + } + } + + var end = i; + while (end >= 0 && end < text.Length && !char.IsWhiteSpace(text[end]) && text[end] != '<' && text[end] != '>') + { + end += direction; + } + + return direction < 0 ? text.Substring(end + 1, i - end) : text.Substring(i, end - i); + } + /// /// Nothing but whitespace, dashes, quotes and tags between the start of the text or the end /// of a sentence and . diff --git a/src/ui/Features/Ocr/OcrViewModel.cs b/src/ui/Features/Ocr/OcrViewModel.cs index 6339e5635f5..d22b8c913f3 100644 --- a/src/ui/Features/Ocr/OcrViewModel.cs +++ b/src/ui/Features/Ocr/OcrViewModel.cs @@ -2699,8 +2699,12 @@ await MessageBox.Show( _ocrFixEngine.Initialize(_fixEngineSubtitle, threeLetterCode, SelectedDictionary); - // nOCR and binary image compare write "*" for a glyph they could not match. - _ocrFixEngine.FillUnknownCharacters = ocrEngine.EngineType is OcrEngineType.nOcr or OcrEngineType.BinaryImageCompare; + // nOCR and binary image compare write "*" for a glyph they could not match, and can + // fit a slanted "l"/"i"/"I" to "/" or an apostrophe to "#"; text engines never do, + // so their "/" and "#" are real (#16031: "PILOTEN / OMGEKOMEN" became "PILOTEN I OMGEKOMEN"). + var isImageMatcher = ocrEngine.EngineType is OcrEngineType.nOcr or OcrEngineType.BinaryImageCompare; + _ocrFixEngine.FillUnknownCharacters = isImageMatcher; + _ocrFixEngine.FixImageMatcherMisreads = isImageMatcher; } else { diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixHashMisreadAsApostropheTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixHashMisreadAsApostropheTests.cs index cabe0a94153..247dcb3120e 100644 --- a/tests/UI/Features/Ocr/FixEngine/OcrFixHashMisreadAsApostropheTests.cs +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixHashMisreadAsApostropheTests.cs @@ -60,7 +60,7 @@ public void FixOcrErrors_RealHash_IsLeftAlone(string input) private static IOcrFixEngine CreateEngine() { - IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()); + IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()) { FixImageMatcherMisreads = true }; engine.Initialize(new Subtitle(), "eng", new SpellCheckDictionaryDisplay()); return engine; } diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs index 83fd014284d..5b9576344a9 100644 --- a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs @@ -89,9 +89,44 @@ public void FixOcrErrors_RealSlash_IsLeftAlone(string input) Assert.Equal(input, result.GetText()); } - private static IOcrFixEngine CreateEngine() + // A lone slash next to a number or between two all-caps words is a separator (a title card + // "PILOTEN / OMGEKOMEN" became "PILOTEN I OMGEKOMEN", #16031), not a slanted italic "I". + [Theory] + [InlineData("PILOTEN / OMGEKOMEN")] + [InlineData("PILOTEN / OMGEKOMEN")] + [InlineData("PILOTEN /\nOMGEKOMEN")] + [InlineData("AC / DC")] + [InlineData("1 / 2")] + [InlineData("Scene 3 / Take 2")] + public void FixOcrErrors_LoneSeparatorSlash_IsLeftAlone(string input) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + // Only image matchers (nOCR, binary image compare) read a slanted stroke as "/"; text engines + // (Tesseract, Paddle, llama.cpp) never do, so the rule is off for their output and every + // slash they produce stays. + [Theory] + [InlineData("PILOTEN / OMGEKOMEN")] + [InlineData("/ can see it.")] + [InlineData("One moment, p/ease.")] + [InlineData("/t's according to plan.")] + public void FixOcrErrors_TextEngineOutput_SlashIsLeftAlone(string input) + { + var engine = CreateEngine(fixImageMatcherMisreads: false); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + private static IOcrFixEngine CreateEngine(bool fixImageMatcherMisreads = true) { - IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()); + IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()) { FixImageMatcherMisreads = fixImageMatcherMisreads }; engine.Initialize(new Subtitle(), "eng", new SpellCheckDictionaryDisplay()); return engine; } diff --git a/tests/UI/Features/Tools/FixCommonErrors/FixCommonOcrErrorsTests.cs b/tests/UI/Features/Tools/FixCommonErrors/FixCommonOcrErrorsTests.cs index c633237e5b8..95ac20f1eb6 100644 --- a/tests/UI/Features/Tools/FixCommonErrors/FixCommonOcrErrorsTests.cs +++ b/tests/UI/Features/Tools/FixCommonErrors/FixCommonOcrErrorsTests.cs @@ -63,6 +63,7 @@ public void SkipAll(string word) { } public void AddName(string name) { } public List ReloadNames() => new(); public bool FillUnknownCharacters { get; set; } + public bool FixImageMatcherMisreads { get; set; } } private sealed class FakeCallbacks : IFixCallbacks From b7b4dcb347849becf33a1faf2a6152e1fd04d3d0 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 17:31:33 +0200 Subject: [PATCH 05/82] OCR fix engine: an "l" counts as uppercase in the separator-slash guard nOCR reads the sans-serif capital I as "l" ("PlLOTEN / OMGEKOMEN"), and the all-caps l->I rule only runs after the slash rule, so the guard saw a lowercase letter and still turned the slash into "I". Verified on nl.sup: the title card now keeps its slash, no other line changes. Co-Authored-By: Claude Fable 5.1 --- src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs | 4 +++- tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs index 9399046fb2f..7575323b8aa 100644 --- a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs +++ b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs @@ -455,9 +455,11 @@ private static bool IsSeparatorSlash(string text, int index) return IsAllCaps(before) && IsAllCaps(after); } + // Image matchers read the sans-serif capital "I" as "l" ("PlLOTEN"); the per-word rules only + // repair that after this runs, so an "l" counts as uppercase here. private static bool IsAllCaps(string word) { - return word.Any(char.IsUpper) && !word.Any(char.IsLower); + return word.Any(char.IsUpper) && word.All(c => !char.IsLower(c) || c == 'l'); } /// diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs index 5b9576344a9..22161998675 100644 --- a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs @@ -93,6 +93,7 @@ public void FixOcrErrors_RealSlash_IsLeftAlone(string input) // "PILOTEN / OMGEKOMEN" became "PILOTEN I OMGEKOMEN", #16031), not a slanted italic "I". [Theory] [InlineData("PILOTEN / OMGEKOMEN")] + [InlineData("PlLOTEN / OMGEKOMEN")] // capital I read as "l", repaired by a later rule [InlineData("PILOTEN / OMGEKOMEN")] [InlineData("PILOTEN /\nOMGEKOMEN")] [InlineData("AC / DC")] From 1ae6fb9bdbdd865455f0736127babb8ff2012d49 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 17:52:59 +0200 Subject: [PATCH 06/82] OCR fix engine: case-based I/l repair and italic spacing for image matchers (#16031) nOCR fits the sans-serif capital I and lowercase l to the same bar, so on the Dutch Blu-ray names came out as "CowsIip", "HeinkeIs", "FIight" and all-caps words as "JUNl", "lFF", "WAARSCHlJNLlJK". No dictionary knows the names, so for a word that is unknown anyway the letters around the bar decide (TryFixBarMisreadByCase): an I after a lowercase letter, or between the capital and the first lowercase letter, is an l; an l among capitals (two, or one with the bar in front) is an I. Mc/Mac, "Il" and words with another lowercase letter are left alone. Only the English replace list had the all-caps rule; this covers every language. Two italic spacing artifacts are repaired on the line: "20. 000" -> "20.000" (one-to-three digit group, never after a year) and the space before a closing quote after a sentence end ("voorbij. '"). All four run only for image-matcher output (FixImageMatcherMisreads). nl.sup: 45 -> 18 lines differing from the GLM reference, 9 of them GLM's own errors. Before/after on 19 other discs (English, Danish, Polish): every changed line is a repair (RIDDICK, DIDN'T, FBI-enhed, Statler, mathlete, Allenwood, trailing quotes). Co-Authored-By: Claude Fable 5.1 --- src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs | 171 ++++++++++++++++++ .../FixEngine/OcrFixBarMisreadByCaseTests.cs | 139 ++++++++++++++ .../Ocr/FixEngine/OcrFixItalicSpacingTests.cs | 125 +++++++++++++ .../FixEngine/OcrFixSlashMisreadAsLTests.cs | 13 +- 4 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 tests/UI/Features/Ocr/FixEngine/OcrFixBarMisreadByCaseTests.cs create mode 100644 tests/UI/Features/Ocr/FixEngine/OcrFixItalicSpacingTests.cs diff --git a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs index 7575323b8aa..a7a6bcb02f8 100644 --- a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs +++ b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs @@ -108,6 +108,8 @@ public OcrFixLineResult FixOcrErrors(int index, string text, bool doTryToGuessUn { text = FixHashMisreadAsApostrophe(text); text = FixSlashMisreadAsL(text); + text = FixSpacedThousandsSeparator(text); + text = FixSpaceBeforeClosingQuote(text); } var replacedLine = ReplaceLineFixes(index, text, wordsToIgnore); @@ -438,6 +440,44 @@ private string FixSlashMisreadAsL(string text) private const int MaxSlashesToRead = 4; + // "20. 000": a one-to-three digit group, a period, a space and a three-digit group. A four-digit + // number before the period ("1940. 300") is a year ending a sentence, not a thousands group. + [GeneratedRegex(@"(? + /// Italic digits lean into the thousands separator, so the space detector puts a word space + /// after the period: "20. 000" -> "20.000", "klim tot 19. 000 voet" -> "19.000". + /// + internal static string FixSpacedThousandsSeparator(string text) + { + if (string.IsNullOrEmpty(text) || !text.Contains(". ", StringComparison.Ordinal)) + { + return text; + } + + return SpacedThousandsSeparator().Replace(text, "$1.$2"); + } + + // Sentence end, a space, then a closing quote followed by whitespace, a tag or the end. + [GeneratedRegex(@"([.!?…]) ([""'”’»])(?=\s|<|$)")] + private static partial Regex SpaceBeforeClosingQuote(); + + /// + /// A closing quote after an italic period gets a word space in front of it ("voorbij. '", + /// "beginnen. \""): drop it. A quote that opens the next word ("Kom.' 'Nee.'", "'s avonds") + /// is not followed by whitespace and stays. + /// + internal static string FixSpaceBeforeClosingQuote(string text) + { + if (string.IsNullOrEmpty(text) || !text.Contains(' ')) + { + return text; + } + + return SpaceBeforeClosingQuote().Replace(text, "$1$2"); + } + /// /// A lone slash next to a number ("1 / 2") or between two all-caps words ("PILOTEN / /// OMGEKOMEN") is a separator, not a slanted italic "I" - an italic "I" sits between @@ -906,10 +946,141 @@ private void CheckAndFixWord(OcrFixLinePartResult word, OcrFixLineResult splitLi } } + // An image matcher fits the sans-serif "I" and "l" to the same bar, so for a word nothing + // knows, the letters around the bar decide: a capital I after a lowercase letter, or + // between a capital and a lowercase letter at the start, is an "l" ("CowsIip", "controI", + // "FIight"); an "l" among capitals is an "I" ("JUNl", "lFF", "AUSTRALlERS"). Names that + // no dictionary has ("Cowslip", "Heinkels") only get this shape rule - the word was + // unknown before it too, and a real word never has that case pattern (Mc/Mac aside). + if (!isWordCorrect && FixImageMatcherMisreads && TryFixBarMisreadByCase(result, out var barFixed)) + { + result = barFixed; + word.GuessUsed = true; + isWordCorrect = IsSpelledCorrect(result) || _spellCheckWordLists.HasName(result.Trim('\'', '-')); + } + word.FixedWord = result; word.IsSpellCheckedOk = isWordCorrect; } + /// + /// Per run of letters in : in a run of capitals (two or more besides the + /// bars, or one with a bar in front) every "l" becomes "I"; otherwise an "I" right after a lowercase letter, or as the + /// second letter between a capital and a lowercase letter, becomes "l". "Il", "lk" and + /// "McIntyre" are left alone. + /// + internal static bool TryFixBarMisreadByCase(string word, out string fixedWord) + { + fixedWord = word; + if (string.IsNullOrEmpty(word) || word.IndexOf('I') < 0 && word.IndexOf('l') < 0) + { + return false; + } + + var chars = word.ToCharArray(); + var start = 0; + while (start < chars.Length) + { + if (!char.IsLetter(chars[start])) + { + start++; + continue; + } + + var end = start; + while (end < chars.Length && char.IsLetter(chars[end])) + { + end++; + } + + FixBarsInLetterRun(chars, start, end); + start = end; + } + + fixedWord = new string(chars); + return fixedWord != word; + } + + private static void FixBarsInLetterRun(char[] chars, int start, int end) + { + var uppers = 0; + var lowersBesidesL = 0; + var bars = 0; + for (var i = start; i < end; i++) + { + var c = chars[i]; + if (c == 'l') + { + bars++; + } + else if (char.IsUpper(c)) + { + uppers++; + } + else if (char.IsLower(c)) + { + lowersBesidesL++; + } + } + + // Two capitals besides the bars make a run all-caps ("JUNl", "lFF"); with one, only a bar + // in front of the capital does ("lN", "lS") - "Il" is Italian and a roman numeral. + if (bars > 0 && lowersBesidesL == 0 && (uppers >= 2 || uppers == 1 && chars[start] == 'l')) + { + for (var i = start; i < end; i++) + { + if (chars[i] == 'l') + { + chars[i] = 'I'; + } + } + + return; + } + + if (lowersBesidesL == 0 && bars == 0) + { + return; // all caps without a bar, or "I" alone + } + + for (var i = start; i < end; i++) + { + if (chars[i] != 'I') + { + continue; + } + + if (i > start && char.IsLower(chars[i - 1])) + { + // "McIntyre", "MacIsaac": the capital belongs to the name. + var isMc = i - start >= 2 && chars[i - 2] == 'M' && chars[i - 1] == 'c'; + var isMac = i - start >= 3 && chars[i - 3] == 'M' && chars[i - 2] == 'a' && chars[i - 1] == 'c'; + if (!isMc && !isMac) + { + chars[i] = 'l'; + } + } + else if (i == start + 1 && char.IsUpper(chars[start])) + { + // "FIight", "AIvar", "AIIenwood": the bars between the capital and the first + // lowercase letter. + var j = i; + while (j < end && chars[j] == 'I') + { + j++; + } + + if (j < end && char.IsLower(chars[j])) + { + for (var k = i; k < j; k++) + { + chars[k] = 'l'; + } + } + } + } + } + // Tries every combination of the word's 'i'/'I' letters replaced by 'l' (fewest swaps first) // and returns the first one the dictionary or names list accepts. Only words of at least five // letters are considered, like the other unknown-word guesses, and a word with more than six diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixBarMisreadByCaseTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixBarMisreadByCaseTests.cs new file mode 100644 index 00000000000..39df2674eec --- /dev/null +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixBarMisreadByCaseTests.cs @@ -0,0 +1,139 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Features.Ocr.FixEngine; +using Nikse.SubtitleEdit.Features.SpellCheck; +using System; +using System.Collections.Generic; +using System.IO; +using Nikse.SubtitleEdit.UiLogic.Ocr.FixEngine; +using Nikse.SubtitleEdit.UiLogic.SpellCheck; + +namespace UITests.Features.Ocr.FixEngine; + +// nOCR fits the sans-serif capital I and the lowercase l to the same bar (#16031, Dutch Blu-ray): +// names come out as "CowsIip", "HeinkeIs", "FIight", all-caps words as "JUNl", "lFF", +// "WAARSCHlJNLlJK". No dictionary knows the names, so for a word that is unknown anyway the +// letters around the bar decide - only when the text comes from an image matcher. +public class OcrFixBarMisreadByCaseTests : IDisposable +{ + private readonly Func _originalSpellCheckDictionariesFolder; + private readonly string _tempDictionariesFolder; + + public OcrFixBarMisreadByCaseTests() + { + _originalSpellCheckDictionariesFolder = SpellCheckConfig.DictionariesFolder; + + _tempDictionariesFolder = Path.Combine( + Path.GetTempPath(), + "SeOcrFixBarMisreadByCaseTest_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDictionariesFolder); + SpellCheckConfig.DictionariesFolder = () => _tempDictionariesFolder; + File.WriteAllText( + Path.Combine(_tempDictionariesFolder, "nld_OCRFixReplaceList.xml"), + ""); + } + + [Theory] + [InlineData("hier is CowsIip.", "hier is Cowslip.")] + [InlineData("Begrepen, Turkey-controI.", "Begrepen, Turkey-control.")] + [InlineData("HeinkeIs, 20 plus.", "Heinkels, 20 plus.")] + [InlineData("Rabbit-eskader. HaIIaIi, Sectie rood.", "Rabbit-eskader. Hallali, Sectie rood.")] + [InlineData("voor A FIight.", "voor A Flight.")] + [InlineData("gelezen door AIvar LiddeII.", "gelezen door Alvar Liddell.")] + [InlineData("in AIIenwood, Pennsylvania,", "in Allenwood, Pennsylvania,")] + [InlineData("Dat is CUCKOOlNG.", "Dat is CUCKOOING.")] + [InlineData("JUNl 1940", "JUNI 1940")] + [InlineData("lN POSlTlE", "IN POSITIE")] + [InlineData("Geen lFF.", "Geen IFF.")] + [InlineData("1 - lSRAELlERS - 0", "1 - ISRAELIERS - 0")] + [InlineData("(WAARSCHlJNLlJK OMGEKOMEN)", "(WAARSCHIJNLIJK OMGEKOMEN)")] + public void FixOcrErrors_UnknownWordWithMisreadBar_IsFixedByCase(string input, string expected) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(expected, result.GetText()); + } + + [Theory] + [InlineData("Ik zie McIntyre.")] // Mc/Mac keep their capital + [InlineData("MacIsaac komt.")] + [InlineData("Deel Il is klaar.")] // a roman numeral, and Italian "Il": capital first, bar last + [InlineData("lk ben hier.")] // lowercase word: the dictionary-gated rules decide + [InlineData("I ben hier.")] + [InlineData("Dat is McDONALD.")] // a real lowercase letter among capitals + public void FixOcrErrors_NoBarPattern_IsLeftAlone(string input) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + [Theory] + [InlineData("hier is CowsIip.")] + [InlineData("JUNl 1940")] + public void FixOcrErrors_TextEngineOutput_IsLeftAlone(string input) + { + var engine = CreateEngine(fixImageMatcherMisreads: false); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + [Fact] + public void FixOcrErrors_FixedBarWordKnownToDictionary_IsSpelledOk() + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, "Turkey-controI", doTryToGuessUnknownWords: false); + + Assert.Equal("Turkey-control", result.GetText()); + } + + private static IOcrFixEngine CreateEngine(bool fixImageMatcherMisreads = true) + { + IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()) { FixImageMatcherMisreads = fixImageMatcherMisreads }; + engine.Initialize(new Subtitle(), "nld", new SpellCheckDictionaryDisplay()); + return engine; + } + + private sealed class FakeSpellChecker : ISpellChecker + { + private static readonly HashSet Words = new(StringComparer.Ordinal) + { + "I", "hier", "is", "begrepen", "plus", "sectie", "rood", "voor", "a", "gelezen", "door", "geen", + "ik", "zie", "komt", "deel", "klaar", "ben", "dat", "control", "flight", "in", "positie", + "waarschijnlijk", "omgekomen", "turkey", "rabbit", "eskader", + }; + + public bool Initialize(string dictionaryFile, string twoLetterLanguageCode) => true; + + public bool IsWordCorrect(string word) + { + if (string.IsNullOrWhiteSpace(word)) + { + return true; + } + + return Words.Contains(word) || Words.Contains(word.ToLowerInvariant()); + } + + public List GetSuggestions(string word) => new(); + } + + public void Dispose() + { + SpellCheckConfig.DictionariesFolder = _originalSpellCheckDictionariesFolder; + try + { + Directory.Delete(_tempDictionariesFolder, recursive: true); + } + catch + { + // Best-effort cleanup. + } + } +} diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixItalicSpacingTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixItalicSpacingTests.cs new file mode 100644 index 00000000000..d740f254c08 --- /dev/null +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixItalicSpacingTests.cs @@ -0,0 +1,125 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Features.Ocr.FixEngine; +using Nikse.SubtitleEdit.Features.SpellCheck; +using System; +using System.Collections.Generic; +using System.IO; +using Nikse.SubtitleEdit.UiLogic.Ocr.FixEngine; +using Nikse.SubtitleEdit.UiLogic.SpellCheck; + +namespace UITests.Features.Ocr.FixEngine; + +// Italic glyphs lean into the next one, so nOCR's space detector puts a word space after a +// thousands-separator period ("20. 000") and before a closing quote ("voorbij. '") (#16031). +// Both are repaired on the line, only for image-matcher output. +public class OcrFixItalicSpacingTests : IDisposable +{ + private readonly Func _originalSpellCheckDictionariesFolder; + private readonly string _tempDictionariesFolder; + + public OcrFixItalicSpacingTests() + { + _originalSpellCheckDictionariesFolder = SpellCheckConfig.DictionariesFolder; + + _tempDictionariesFolder = Path.Combine( + Path.GetTempPath(), + "SeOcrFixItalicSpacingTest_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDictionariesFolder); + SpellCheckConfig.DictionariesFolder = () => _tempDictionariesFolder; + File.WriteAllText( + Path.Combine(_tempDictionariesFolder, "nld_OCRFixReplaceList.xml"), + ""); + } + + [Theory] + [InlineData("en klim tot 20. 000.", "en klim tot 20.000.")] + [InlineData("op 20. 000 voet richting west.", "op 20.000 voet richting west.")] + [InlineData("klim naar 1. 500 voet", "klim naar 1.500 voet")] + public void FixOcrErrors_SpacedThousandsSeparator_IsJoined(string input, string expected) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(expected, result.GetText()); + } + + [Theory] + [InlineData("Het was 1940. 300 vliegtuigen kwamen.")] // a year ends the sentence + [InlineData("Het was 20. 30 vliegtuigen kwamen.")] // not a three-digit group + [InlineData("Het was 20. 3000 vliegtuigen kwamen.")] + [InlineData("1.000. 000")] // already a separator before the period + public void FixOcrErrors_SentenceEndingInNumber_IsLeftAlone(string input) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + [Theory] + [InlineData("\"Generaal Weygand zei:\n'De Franse strijd is voorbij. '", "\"Generaal Weygand zei:\n'De Franse strijd is voorbij.'")] + [InlineData("\"De Engelse strijd gaat nu beginnen. \"", "\"De Engelse strijd gaat nu beginnen.\"")] + [InlineData("Hij zei: 'Kom. ' 'Nee.'", "Hij zei: 'Kom.' 'Nee.'")] + public void FixOcrErrors_SpaceBeforeClosingQuote_IsRemoved(string input, string expected) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(expected, result.GetText()); + } + + [Theory] + [InlineData("Hij zei: 'Kom.' 'Nee.'")] // the quote opens the next word + [InlineData("Hij komt. 's Avonds.")] // apostrophe of the next word + [InlineData("Hij komt. \"Nee.\"")] + public void FixOcrErrors_QuoteOpeningNextWord_IsLeftAlone(string input) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + [Theory] + [InlineData("en klim tot 20. 000.")] + [InlineData("\"De Engelse strijd gaat nu beginnen. \"")] + public void FixOcrErrors_TextEngineOutput_IsLeftAlone(string input) + { + var engine = CreateEngine(fixImageMatcherMisreads: false); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + private static IOcrFixEngine CreateEngine(bool fixImageMatcherMisreads = true) + { + IOcrFixEngine engine = new OcrFixEngine(new AcceptAllSpellChecker()) { FixImageMatcherMisreads = fixImageMatcherMisreads }; + engine.Initialize(new Subtitle(), "nld", new SpellCheckDictionaryDisplay()); + return engine; + } + + private sealed class AcceptAllSpellChecker : ISpellChecker + { + public bool Initialize(string dictionaryFile, string twoLetterLanguageCode) => true; + public bool IsWordCorrect(string word) => true; + public List GetSuggestions(string word) => new(); + } + + public void Dispose() + { + SpellCheckConfig.DictionariesFolder = _originalSpellCheckDictionariesFolder; + try + { + Directory.Delete(_tempDictionariesFolder, recursive: true); + } + catch + { + // Best-effort cleanup. + } + } +} diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs index 22161998675..5fdd262e07d 100644 --- a/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixSlashMisreadAsLTests.cs @@ -93,7 +93,6 @@ public void FixOcrErrors_RealSlash_IsLeftAlone(string input) // "PILOTEN / OMGEKOMEN" became "PILOTEN I OMGEKOMEN", #16031), not a slanted italic "I". [Theory] [InlineData("PILOTEN / OMGEKOMEN")] - [InlineData("PlLOTEN / OMGEKOMEN")] // capital I read as "l", repaired by a later rule [InlineData("PILOTEN / OMGEKOMEN")] [InlineData("PILOTEN /\nOMGEKOMEN")] [InlineData("AC / DC")] @@ -108,6 +107,18 @@ public void FixOcrErrors_LoneSeparatorSlash_IsLeftAlone(string input) Assert.Equal(input, result.GetText()); } + // The capital I of the title card is read as "l"; the all-caps rule repairs that afterwards, + // so the slash guard has to count the "l" as a capital or the slash is gone before it runs. + [Fact] + public void FixOcrErrors_LoneSlashBetweenAllCapsWordsWithMisreadBar_KeepsSlash() + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, "PlLOTEN / OMGEKOMEN", doTryToGuessUnknownWords: false); + + Assert.Equal("PILOTEN / OMGEKOMEN", result.GetText()); + } + // Only image matchers (nOCR, binary image compare) read a slanted stroke as "/"; text engines // (Tesseract, Paddle, llama.cpp) never do, so the rule is off for their output and every // slash they produce stays. From 09a70adc63c5f2527258e8ed383f078b96ed888f Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 18:12:40 +0200 Subject: [PATCH 07/82] OCR fix engine: split an unknown word at an overhanging f/r/t before j/v/w An overhanging f, r or t leans over the j, v or w next to it, so nOCR's space detector swallows the word space: "Hoorje", "kentje", "achterje", "Vijfvan", "ofwe". For a word the dictionary does not know, split at that boundary when both halves are words and the right half is a function word of at most three letters - without the cap "Getverdikkeme" (not in the dictionary) became "Get verdikkeme". Image-matcher output only. nl.sup: 18 -> 13 lines differing from the GLM reference; the other discs gain one repair ("Alt vel") and lose nothing. Co-Authored-By: Claude Fable 5.1 --- src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs | 48 ++++++++ .../Ocr/FixEngine/OcrFixOverhangSplitTests.cs | 116 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 tests/UI/Features/Ocr/FixEngine/OcrFixOverhangSplitTests.cs diff --git a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs index a7a6bcb02f8..31122ae2de0 100644 --- a/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs +++ b/src/libuilogic/Ocr/FixEngine/OcrFixEngine.cs @@ -959,10 +959,58 @@ private void CheckAndFixWord(OcrFixLinePartResult word, OcrFixLineResult splitLi isWordCorrect = IsSpelledCorrect(result) || _spellCheckWordLists.HasName(result.Trim('\'', '-')); } + // An overhanging f, r or t leans over the j, v or w next to it, so the space detector + // swallows the word space: "Hoorje", "kentje", "Vijfvan", "ofwe". Split there when the + // dictionary knows both halves (#16031). + if (!isWordCorrect && FixImageMatcherMisreads && TrySplitAtOverhang(result, out var splitAtOverhang)) + { + result = splitAtOverhang; + word.GuessUsed = true; + isWordCorrect = true; + } + word.FixedWord = result; word.IsSpellCheckedOk = isWordCorrect; } + /// + /// Splits a word of letters at the first "f", "r" or "t" followed by "j", "v" or "w" where both + /// halves are dictionary words or names and the right half is a short function word of at + /// most three letters ("je", "we", "van"): "Hoorje" -> "Hoor je", "Vijfvan" -> "Vijf van". The + /// length cap keeps an unlisted compound or interjection whole ("Getverdikkeme" is not "Get + /// verdikkeme"). A word with digits or punctuation is left alone. + /// + private bool TrySplitAtOverhang(string word, out string split) + { + split = word; + if (word.Length < 4 || !word.All(char.IsLetter)) + { + return false; + } + + for (var i = Math.Max(2, word.Length - MaxOverhangRightHalf); i <= word.Length - 2; i++) + { + var before = char.ToLowerInvariant(word[i - 1]); + var after = char.ToLowerInvariant(word[i]); + if (before is not ('f' or 'r' or 't') || after is not ('j' or 'v' or 'w')) + { + continue; + } + + var left = word.Substring(0, i); + var right = word.Substring(i); + if (IsWordOrName(left) && IsWordOrName(right)) + { + split = left + " " + right; + return true; + } + } + + return false; + } + + private const int MaxOverhangRightHalf = 3; + /// /// Per run of letters in : in a run of capitals (two or more besides the /// bars, or one with a bar in front) every "l" becomes "I"; otherwise an "I" right after a lowercase letter, or as the diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixOverhangSplitTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixOverhangSplitTests.cs new file mode 100644 index 00000000000..b633a07d7e2 --- /dev/null +++ b/tests/UI/Features/Ocr/FixEngine/OcrFixOverhangSplitTests.cs @@ -0,0 +1,116 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Features.Ocr.FixEngine; +using Nikse.SubtitleEdit.Features.SpellCheck; +using System; +using System.Collections.Generic; +using System.IO; +using Nikse.SubtitleEdit.UiLogic.Ocr.FixEngine; +using Nikse.SubtitleEdit.UiLogic.SpellCheck; + +namespace UITests.Features.Ocr.FixEngine; + +// An overhanging f, r or t leans over the j, v or w after it, so nOCR's space detector swallows +// the word space: "Hoorje", "kentje", "Vijfvan", "ofwe" (#16031, Dutch Blu-ray). An unknown word +// is split there when the dictionary knows both halves - only for image-matcher output. +public class OcrFixOverhangSplitTests : IDisposable +{ + private readonly Func _originalSpellCheckDictionariesFolder; + private readonly string _tempDictionariesFolder; + + public OcrFixOverhangSplitTests() + { + _originalSpellCheckDictionariesFolder = SpellCheckConfig.DictionariesFolder; + + _tempDictionariesFolder = Path.Combine( + Path.GetTempPath(), + "SeOcrFixOverhangSplitTest_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDictionariesFolder); + SpellCheckConfig.DictionariesFolder = () => _tempDictionariesFolder; + File.WriteAllText( + Path.Combine(_tempDictionariesFolder, "nld_OCRFixReplaceList.xml"), + ""); + } + + [Theory] + [InlineData("Hoorje me niet?", "Hoor je me niet?")] + [InlineData("Hij kentje man.", "Hij kent je man.")] + [InlineData("Ox, achterje.", "Ox, achter je.")] + [InlineData("Vijfvan elk eskader...", "Vijf van elk eskader...")] + [InlineData("We rukken in ofwe ontploffen.", "We rukken in of we ontploffen.")] + public void FixOcrErrors_UnknownWordWithSwallowedSpace_IsSplit(string input, string expected) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(expected, result.GetText()); + } + + [Theory] + [InlineData("De software werkt.")] // a known word is never split + [InlineData("Hij kentjex man.")] // the right half is no word + [InlineData("Hij xkentje man.")] // the left half is no word + [InlineData("Hij kentje2 man.")] // digits + [InlineData("Getverdikkeme. Je jatten.")] // both halves are words, but the right one is no short function word + public void FixOcrErrors_NoDictionaryHalves_IsLeftAlone(string input) + { + var engine = CreateEngine(); + + var result = engine.FixOcrErrors(0, input, doTryToGuessUnknownWords: false); + + Assert.Equal(input, result.GetText()); + } + + [Fact] + public void FixOcrErrors_TextEngineOutput_IsLeftAlone() + { + var engine = CreateEngine(fixImageMatcherMisreads: false); + + var result = engine.FixOcrErrors(0, "Hoorje me niet?", doTryToGuessUnknownWords: false); + + Assert.Equal("Hoorje me niet?", result.GetText()); + } + + private static IOcrFixEngine CreateEngine(bool fixImageMatcherMisreads = true) + { + IOcrFixEngine engine = new OcrFixEngine(new FakeSpellChecker()) { FixImageMatcherMisreads = fixImageMatcherMisreads }; + engine.Initialize(new Subtitle(), "nld", new SpellCheckDictionaryDisplay()); + return engine; + } + + private sealed class FakeSpellChecker : ISpellChecker + { + private static readonly HashSet Words = new(StringComparer.Ordinal) + { + "hoor", "je", "me", "niet", "hij", "kent", "man", "ox", "achter", "vijf", "van", "elk", "eskader", + "we", "rukken", "in", "of", "ontploffen", "de", "software", "werkt", "get", "verdikkeme", "jatten", + }; + + public bool Initialize(string dictionaryFile, string twoLetterLanguageCode) => true; + + public bool IsWordCorrect(string word) + { + if (string.IsNullOrWhiteSpace(word)) + { + return true; + } + + return Words.Contains(word) || Words.Contains(word.ToLowerInvariant()); + } + + public List GetSuggestions(string word) => new(); + } + + public void Dispose() + { + SpellCheckConfig.DictionariesFolder = _originalSpellCheckDictionariesFolder; + try + { + Directory.Delete(_tempDictionariesFolder, recursive: true); + } + catch + { + // Best-effort cleanup. + } + } +} From 0e43e9b95c37048a494b6266edda13220acb5057 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 18:42:53 +0200 Subject: [PATCH 08/82] Qwen3 ASR: bump qwen3-asr.cpp to v0.1.10 and use beam search (3) v0.1.10 (niksedk/qwen3-asr.cpp#13) adds beam search. Subtitle Edit now passes --beam-size 3, which took a Danish fine-tune from 25.2 to 23.4 WER on 300 held-out clips for about twice the decode time (beam 5: 23.1 at four times). Older engine builds reject unknown arguments, so the flag is only passed when the installed qwen3-asr-cli advertises it in --help; the probe runs once per executable. New archive and executable hashes prepended for all seven platforms. Co-Authored-By: Claude Fable 5.1 --- .../SpeechToText/Engines/Qwen3AsrCppEngine.cs | 66 +++++++++++++++++++ .../SpeechToText/SpeechToTextViewModel.cs | 9 ++- src/ui/Logic/Download/DownloadHashManager.cs | 42 ++++++++---- .../Download/Qwen3AsrCppDownloadService.cs | 7 +- 4 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs b/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs index 0e8cfd64d7d..4e8b1c53976 100644 --- a/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs +++ b/src/ui/Features/Video/SpeechToText/Engines/Qwen3AsrCppEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -319,6 +320,71 @@ public string GetWhisperModelDownloadFileName(WhisperModel whisperModel, string return fileName; } + private static readonly Dictionary BeamSearchSupportCache = new(); + + /// + /// True when the installed qwen3-asr-cli understands --beam-size (v0.1.10+). Older builds + /// reject unknown arguments, so the flag must not be passed to them. Probed once per + /// executable (path + timestamp) via --help, which does not load a model. + /// + public bool SupportsBeamSearch + { + get + { + var exe = GetExecutable(); + if (!File.Exists(exe)) + { + return false; + } + + var key = exe + "|" + File.GetLastWriteTimeUtc(exe).Ticks; + lock (BeamSearchSupportCache) + { + if (BeamSearchSupportCache.TryGetValue(key, out var cached)) + { + return cached; + } + } + + var supported = false; + try + { + var psi = new ProcessStartInfo(exe, "--help") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + using var process = Process.Start(psi); + if (process != null) + { + var stdErr = process.StandardError.ReadToEndAsync(); + var stdOut = process.StandardOutput.ReadToEndAsync(); + if (process.WaitForExit(5000)) + { + supported = (stdErr.Result + stdOut.Result).Contains("--beam-size", StringComparison.Ordinal); + } + else + { + try { process.Kill(); } catch { /* ignore */ } + } + } + } + catch + { + supported = false; + } + + lock (BeamSearchSupportCache) + { + BeamSearchSupportCache[key] = supported; + } + + return supported; + } + } + public string GetExecutableFileName() { if (OperatingSystem.IsWindows()) diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs index 82dfd150e92..b5bebcc2f88 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs @@ -4796,9 +4796,14 @@ private Process GetWhisperProcess( ? string.Empty : $"-l {language} "; + // qwen3-asr.cpp v0.1.10+ beam search: beam 3 trims about 2 WER points for roughly + // twice the decode time (greedy = 1). Older engine builds reject unknown arguments, + // so the flag is only passed when the installed build advertises it. + var qwen3BeamArg = qwen3Asr.SupportsBeamSearch ? "--beam-size 3 " : string.Empty; + var qwen3Params = string.IsNullOrWhiteSpace(qwen3ExtraArgs) - ? $"{qwen3LanguageArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\"" - : $"{qwen3ExtraArgs} {qwen3LanguageArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\""; + ? $"{qwen3LanguageArg}{qwen3BeamArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\"" + : $"{qwen3ExtraArgs} {qwen3LanguageArg}{qwen3BeamArg}-m \"{qwen3Asr.GetModelForCmdLine(model)}\" --aligner-model \"{qwen3Asr.GetModelForCmdLine(alignerModel.Name)}\" -f \"{waveFileName}\" --transcribe-align -o \"{_qwen3AsrOutputJsonPath}\""; return StartEngineProcess(exe, qwen3Params, dataReceivedHandler); } diff --git a/src/ui/Logic/Download/DownloadHashManager.cs b/src/ui/Logic/Download/DownloadHashManager.cs index 1686a5cc1d2..bbac689f7fe 100644 --- a/src/ui/Logic/Download/DownloadHashManager.cs +++ b/src/ui/Logic/Download/DownloadHashManager.cs @@ -1095,7 +1095,8 @@ public static class WhisperX // recognized and offered the update (issue #11375). [Qwen3AsrCpp.Windows] = new[] { - "075e2e4a811235842ea7ce16f186c4af994dea39ebf4348a287a5b3a56876594", // v0.1.9 (current download URL) + "5f80872f619d62539fe3fd7747a92160e36c308b65423bc3e14f8dcd7728874a", // v0.1.10 (current download URL) + "075e2e4a811235842ea7ce16f186c4af994dea39ebf4348a287a5b3a56876594", // v0.1.9 "ad9c196aa52ceb42a3f46946644e4145fc7e297c84fa54e4cdf7fd9c22556300", // v0.1.8 "2899cfd563b8343fde6b74b617f32b17a26b611e1187c91ba416334364075af7", // v0.1.7 "4b0e23b2124c3e64c00dbc727bdfd86f7deca4a802edec9e6470701892371942", // v0.1.6 @@ -1108,7 +1109,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsVulkan] = new[] { - "8ab5a8eabd2c2bd76494de5a77fe3970c6406d2cd3f7d550b96ac83436fe54e6", // v0.1.9 (current download URL) + "1dcb24785e282c9d2b4416ff9c9d140b74f0575c687e01547ced7748df293af8", // v0.1.10 (current download URL) + "8ab5a8eabd2c2bd76494de5a77fe3970c6406d2cd3f7d550b96ac83436fe54e6", // v0.1.9 "a9971a24ba08765568c8a83a547924e42f15bd576b2cf594f6757fff66baf244", // v0.1.8 "b1bff40ba1c7f0103c6016c4845481053686761f0c375eb71612d1f07192099b", // v0.1.7 "b5ee87d7f9290f0fec651f727fb015bff14ca4e297df83e46c64f489e2ca64f5", // v0.1.6 @@ -1119,7 +1121,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacArm64] = new[] { - "28c9f90f3e87a511f4e3685d438c862bec06b1e747399c6820a2f3f13ad243b7", // v0.1.9 (current download URL) + "e48f6d91180acdd8073b596e2e4a28fdc38bf188c677b01265a64bc9bd0e325c", // v0.1.10 (current download URL) + "28c9f90f3e87a511f4e3685d438c862bec06b1e747399c6820a2f3f13ad243b7", // v0.1.9 "a9e9f33ff8ba651591974824e886ecd28a034d00aaf8203365a7198cd5012325", // v0.1.8 "a4355282cbde03d99ddc1be7724a2e0fd2520ad6d07e875bb4a01ce4f8a83fcd", // v0.1.7 "cf594a18afc7bfa42c8e784d83842c842177310289471ad211ca58e3d3c0642a", // v0.1.6 @@ -1132,7 +1135,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacX64] = new[] { - "b44a358669d59d3e354b88554b5898420050542067ef8a046b5c0fee5044e2e0", // v0.1.9 (current download URL) + "bcd7ada8a31ee17afe85d3d4526584b9bd9cbbcde6faebc3404959fda09d083f", // v0.1.10 (current download URL) + "b44a358669d59d3e354b88554b5898420050542067ef8a046b5c0fee5044e2e0", // v0.1.9 "6dbb978b74829c080177f6c70e89b363a8bc24d543b718ad06ecea7a7c833c53", // v0.1.8 "8295b7c36fd49fe6073d9a05b126a66ac1a5eaf0cc2768d45a5145f3d6a07fae", // v0.1.7 "ebe8afd2fce42c1dc7780a96f45757dc1eabf1c94dc639cf8a8664cc807f4b78", // v0.1.6 @@ -1144,7 +1148,8 @@ public static class WhisperX }, [Qwen3AsrCpp.Linux] = new[] { - "28bcf1df65818674514fa88e7e1845b6bf15141c3931aa48a2f8607ffc7631b6", // v0.1.9 (current download URL) + "5de2c78636b7a9fc8bd603e797a5974cff7cdc44414357fe2363031159289ec3", // v0.1.10 (current download URL) + "28bcf1df65818674514fa88e7e1845b6bf15141c3931aa48a2f8607ffc7631b6", // v0.1.9 "22b37a1917ca4083df7b57c62497a9f30c237ee13c2eee4b36a46320cf8506ed", // v0.1.8 "d6843919444f447b5b4cc307565f07cdec4a233e8584f1cb88ded6d5413f5caa", // v0.1.7 "6dccfe589febd6001b37b9be9248227f1278a966c94e86606b159867ca78f5d3", // v0.1.6 @@ -1157,7 +1162,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxVulkan] = new[] { - "4c7bf45a3d79ffe9a3eb5d9508a7121226055c013bd8c20659b9c88c4d9bb0e8", // v0.1.9 (current download URL) + "0641a35ba38c1eaba0b25f9f168ee1194a3ab6ccd4120af971caae38966a9528", // v0.1.10 (current download URL) + "4c7bf45a3d79ffe9a3eb5d9508a7121226055c013bd8c20659b9c88c4d9bb0e8", // v0.1.9 "d881e30dfc6f5311496355a08668d22d7f592ba4f7120a266abf8024d0f59a24", // v0.1.8 "4c3147af8e9077f8758d9ed86c23f213b22ccbc0df1d8990f27308e34279f831", // v0.1.7 "c455b92c8143416abfd201024c1707c0b52350e410908c6b718c8cea4eae44ff", // v0.1.6 @@ -1168,7 +1174,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxArm64] = new[] { - "c74f8a79b66c4888fc6701dcb516f06c0ea5ef20f9982cbac78bd0694c008957", // v0.1.9 (current download URL) + "1411d484f12ebccce4d682e63ba15ff675d74436115f9872d1977611d0d0ddae", // v0.1.10 (current download URL) + "c74f8a79b66c4888fc6701dcb516f06c0ea5ef20f9982cbac78bd0694c008957", // v0.1.9 "8d3a1ac745fa60f5576a6db599c7b4b9bc0302ea527cbdab9be3223f80a555e4", // v0.1.8 "1a67596e18da7ad5ad2adcca72ce5986033120ba25835493a92b52885a6c1903", // v0.1.7 "4127c2567b6512a4377ff5b3b077468d915fcf76ab156458ec7be81d5e5690e0", // v0.1.6 @@ -1180,7 +1187,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsExecutable] = new[] { - "029b520b2a2e692bdf7a8855bdd6bda80afc55b3ee15d331f5fb952e2dc9eef9", // v0.1.9 (current download URL) + "b79f34d119ab7a3400284e780502a2c2e09b63d17958ed0b85cffd58cc3b7716", // v0.1.10 (current download URL) + "029b520b2a2e692bdf7a8855bdd6bda80afc55b3ee15d331f5fb952e2dc9eef9", // v0.1.9 "4774a1662b8dfd15083e8ffd1e318aeedc86a55931c0bcdec72890547e48c91b", // v0.1.8 "0abdde2ebb6cea8059aa8fe3b7ed61277b9c17bfa223715c431320c575e653c6", // v0.1.7 "8f9e0f8c85e2c95fe0627d442dd15908d36ae9c0076ab4109e84c00ec6f2ab78", // v0.1.6 @@ -1193,7 +1201,8 @@ public static class WhisperX }, [Qwen3AsrCpp.WindowsVulkanExecutable] = new[] { - "529222fe88a9f2ea7d7f0fa48c7b5770b150242800b8d441f38144b90e42ee9e", // v0.1.9 (current download URL) + "d25487a319f7bc6a17ac6bc88b951da1361fb1e61710face5ae2b6cc7fa96411", // v0.1.10 (current download URL) + "529222fe88a9f2ea7d7f0fa48c7b5770b150242800b8d441f38144b90e42ee9e", // v0.1.9 "f0e8dd1d4de3b5a71cfe8e9d04e8e1dbaff58768ccd8b0859279035e63c9c12a", // v0.1.8 "eba7c228d04a3124e48bcb6342bb372a2fc5338a344e2ee8bd3971518ea4e780", // v0.1.7 "998b0577d5d3cbe35917603f86a72acf201f5010757ed611216574eb67d3570e", // v0.1.6 @@ -1204,7 +1213,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacArm64Executable] = new[] { - "5ec504cd96f6808b4d9a31e2a9bce9e0c6a2f5548dd8358249c701aa3bdf0bc7", // v0.1.9 (current download URL) + "37fbec028c82debade4cba2c07bfc535d1c00bfd3e3ec5ff1295ee92e9d6e6dc", // v0.1.10 (current download URL) + "5ec504cd96f6808b4d9a31e2a9bce9e0c6a2f5548dd8358249c701aa3bdf0bc7", // v0.1.9 "26e0cfb663aeb73999e6de0f946e2ddb37cb7a2a28161b41fc9e23ddb8247904", // v0.1.8 "0c637dad09da1198c755d75f4d0e52d74443220743ee4c5f396c8a95ecc41d2a", // v0.1.7 "e2cd42381858a853ece9ca6f00eab4fb940b34efbfc5d6b5a332fae6371fb90c", // v0.1.6 @@ -1216,7 +1226,8 @@ public static class WhisperX }, [Qwen3AsrCpp.MacX64Executable] = new[] { - "d277fd254bf55797ff63761eda7ce08d94bf8f5dde56d87d4e09f163bc2dca5e", // v0.1.9 (current download URL) + "dc086961834263589a1b53fb362914eacea04dcb65eb1c58f497d801e2f03e4b", // v0.1.10 (current download URL) + "d277fd254bf55797ff63761eda7ce08d94bf8f5dde56d87d4e09f163bc2dca5e", // v0.1.9 "b2e55b521d97a731488caf6439b7ecc1e8166dd20fd8770d341849065cbc8172", // v0.1.8 "e4b9c56a373656d67ca0ef0ef383ccc75b62b788b323bdd43f922f351a3864c1", // v0.1.7 "eff1cf91866b6168f5544dec6018fe339cd60cb9acba5003fc0142e240f96172", // v0.1.6 @@ -1227,7 +1238,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxExecutable] = new[] { - "df95f8cd851f170a752a0dd84f72880f7ed0a8e546b921bae08911150dff558b", // v0.1.9 (current download URL) + "b657c6baf867d9f832dca8f48155c59e4dc8ce91efba87b5ad86ff29d41cbc20", // v0.1.10 (current download URL) + "df95f8cd851f170a752a0dd84f72880f7ed0a8e546b921bae08911150dff558b", // v0.1.9 "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 "a02d9e2ba60201ad8e778dca9a9f7ca1f5cafc145955488a1b2c611615b997ba", // v0.1.7 / v0.1.6 "b7d9f1450eba98f8e1a63ae2e9cfbf79c5ceea648d981b31636e25eb0e1d8b0b", // v0.1.5 @@ -1238,7 +1250,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxVulkanExecutable] = new[] { - "1d1e99ef0936321216fba2257c4d31fdfaf97779519e8ae333e0295c8b433081", // v0.1.9 (current download URL) + "75da7aca5cd8067f310ac639302cd0539de4d5dc9b86e18fca01d75be377bf78", // v0.1.10 (current download URL) + "1d1e99ef0936321216fba2257c4d31fdfaf97779519e8ae333e0295c8b433081", // v0.1.9 "1f768c0f8dfe3ac7ed21b3e6a4510fb9ee421b9949e813e1de54ecee57a18051", // v0.1.8 "a02d9e2ba60201ad8e778dca9a9f7ca1f5cafc145955488a1b2c611615b997ba", // v0.1.7 / v0.1.6 "b7d9f1450eba98f8e1a63ae2e9cfbf79c5ceea648d981b31636e25eb0e1d8b0b", // v0.1.5 @@ -1248,7 +1261,8 @@ public static class WhisperX }, [Qwen3AsrCpp.LinuxArm64Executable] = new[] { - "a1c4c2941b47f64113bbe6f7819d97974eaf3b224e4e28f6761404bd13f47a36", // v0.1.9 (current download URL) + "4e7a82912dceaac238776a32035ebf751afac1f6584f81cfcb79258b6ae6b732", // v0.1.10 (current download URL) + "a1c4c2941b47f64113bbe6f7819d97974eaf3b224e4e28f6761404bd13f47a36", // v0.1.9 "ffd6e80c743f5e2b977c2dfd3a7e945c29104fda9d2a43c91a2f3aa0dfef548d", // v0.1.8 "fe453c7f30efc717fbfb2b9d4ff4ff5d50f862300d8c390d6e4c0066c31e6c60", // v0.1.7 / v0.1.6 "bcb155a93474b074897c7680a2c2cfdbfe1cf4478652db5a4bec8c83cf688541", // v0.1.5 diff --git a/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs b/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs index ce7c84e3141..debd581cd86 100644 --- a/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs +++ b/src/ui/Logic/Download/Qwen3AsrCppDownloadService.cs @@ -29,10 +29,11 @@ public class Qwen3AsrCppDownloadService : IQwen3AsrCppDownloadService // in the Whisper feature extractor): the log-mel input was off by up to 1.6, which made the // model mis-detect languages on short clips and cost ~9 WER points versus transformers; it // also pads the trailing conv chunk like the reference and forces the language from the - // prompt when -l is given. CPU for every platform, plus Vulkan (GPU) for win64 and - // linux-x64. When bumping: also prepend the new archive + executable hashes in + // prompt when -l is given. v0.1.10 adds beam search (--beam-size); SE passes --beam-size 3, + // which took a Danish fine-tune from 25.2 to about 23 WER at roughly 2x decode time. + // CPU for every platform, plus Vulkan (GPU) for win64 and linux-x64. When bumping: also prepend the new archive + executable hashes in // DownloadHashManager (Qwen3AsrCpp keys) so the update prompt fires for old installs. - private const string ReleaseUrlBase = "https://github.com/niksedk/qwen3-asr.cpp/releases/download/v0.1.9/"; + private const string ReleaseUrlBase = "https://github.com/niksedk/qwen3-asr.cpp/releases/download/v0.1.10/"; private const string WindowsUrl = ReleaseUrlBase + "qwen3-asr-cpp-win64.zip"; private const string WindowsVulkanUrl = ReleaseUrlBase + "qwen3-asr-cpp-win64-vulkan.zip"; private const string MacArmUrl = ReleaseUrlBase + "qwen3-asr-cpp-mac.zip"; From aac5fed602fd99d4ba31608c92e03bf3b093350c Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 19:44:44 +0200 Subject: [PATCH 09/82] Qwen3 ASR: strip the marker from engine output The model prefixes its output with "language ". qwen3-asr.cpp v0.1.9+ strips the marker, but older engine builds leak "" into the aligned word list for GGUFs that store the token as an ordinary token (the Danish fine-tune does), so it appeared at the start of subtitle lines. Strip it in the JSON parser regardless of engine version and drop empty tokens. Co-Authored-By: Claude Fable 5.1 --- .../Video/SpeechToText/SpeechToTextViewModel.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs index b5bebcc2f88..5210ee97f7a 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs @@ -1396,8 +1396,19 @@ await MessageBox.Show(Window!, $"Unknown argument: {badArgs}", var words = new List(); foreach (var word in jsonDoc.RootElement.GetProperty("words").EnumerateArray()) { + // The model prefixes its output with "language "; qwen3-asr.cpp + // v0.1.9+ strips it, but older builds leak the "" marker into the word + // list for GGUFs that store that token as an ordinary (non-control) token, so it + // showed up at the start of subtitle lines. Strip it here regardless of engine. + var wordText = (word.GetProperty("word").GetString() ?? string.Empty) + .Replace("", string.Empty, StringComparison.Ordinal); + if (string.IsNullOrWhiteSpace(wordText)) + { + continue; + } + words.Add(new Qwen3AsrWord( - word.GetProperty("word").GetString() ?? string.Empty, + wordText, word.GetProperty("start").GetDouble(), word.GetProperty("end").GetDouble())); } From 4f13e74cfd97d172ff2e2447f783747209a33a4c Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 20:11:52 +0200 Subject: [PATCH 10/82] ASSA/SSA styles: category picker back, font name + color columns, resizable lists (#16251) - The saved styles list gets its category picker back, with new/rename/ delete category beside it. Saved styles are kept per series in categories; with hundreds of them a flat list with a category column was unusable, and the filter and commands were still in the view model. - Both style lists show the font name and a color sample ("Aa" in the primary color on the outline color, italic when the style is), so the colors of every style are visible without selecting it - as SE 5.3.0 and SE4's advanced styles list showed them. - A splitter between the lists and the preview/editor lets the lists pane grow; its width is remembered (SeAssa.StylesListsWidth, default 480 instead of a fixed 360) and shared by the ASSA and SSA windows. Co-Authored-By: Claude Fable 5.1 --- src/ui/Features/Assa/AssaStylesWindow.cs | 74 ++++++++++++----- src/ui/Features/Assa/StylesDialogHelper.cs | 97 +++++++++++++++++++++- src/ui/Features/Ssa/SsaStylesWindow.cs | 29 +++---- src/ui/Logic/Config/SeAssa.cs | 5 ++ 4 files changed, 166 insertions(+), 39 deletions(-) diff --git a/src/ui/Features/Assa/AssaStylesWindow.cs b/src/ui/Features/Assa/AssaStylesWindow.cs index b85360fe5c4..93be6b8808e 100644 --- a/src/ui/Features/Assa/AssaStylesWindow.cs +++ b/src/ui/Features/Assa/AssaStylesWindow.cs @@ -42,7 +42,7 @@ public AssaStylesWindow(AssaStylesViewModel vm) Mode = BindingMode.TwoWay, }); CanResize = true; - Width = 1300; + Width = 1400; Height = 850; MinWidth = 1050; MinHeight = 600; @@ -53,14 +53,9 @@ public AssaStylesWindow(AssaStylesViewModel vm) vm.Window = this; DataContext = vm; - var content = new Grid - { - ColumnDefinitions = - { - new ColumnDefinition(new GridLength(360)), - new ColumnDefinition(new GridLength(1, GridUnitType.Star)), - }, - }; + // Lists on the left, preview + editor on the right, a splitter between them: the lists + // show font names and colors too, so their width is the user's call (#16251) + var content = new Grid(); // Right side: the preview across the full width on top (a 16:9 frame), the editor // sections in three columns under it - a tall preview column was mostly empty space @@ -77,8 +72,7 @@ public AssaStylesWindow(AssaStylesViewModel vm) right.Add(MakeEditorPanel(vm), 2, 0); right.Add(MakeEditingBanner(vm), 1, 0); - content.Add(MakeListsPanel(vm), 0, 0); - content.Add(right, 0, 1); + StylesDialogHelper.AddListsSplitter(this, content, MakeListsPanel(vm), right); var mainGrid = new Grid { @@ -267,13 +261,14 @@ private static Grid MakeFileStyles(AssaStylesViewModel vm) Binding = new Binding(nameof(StyleDisplay.Name)), Width = new GridLength(1, GridUnitType.Star), }); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); dataGrid.Columns.Add(new SeTableViewColumn { Header = Se.Language.General.FontSize, CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.FontSize)), - Width = new GridLength(70), + Width = new GridLength(56), }); dataGrid.Columns.Add(new SeTableViewColumn { @@ -281,8 +276,9 @@ private static Grid MakeFileStyles(AssaStylesViewModel vm) CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.UsageCount)), - Width = new GridLength(70), + Width = new GridLength(56), }); + dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); dataGrid.Bind(TableView.SelectedItemProperty, new Binding(nameof(vm.SelectedFileStyle)) { Source = vm }); dataGrid.SelectionChanged += vm.FileStylesChanged; @@ -330,16 +326,48 @@ private static void AddMenuItem(MenuFlyout flyout, AssaStylesViewModel vm, strin private static Grid MakeStorageStyles(AssaStylesViewModel vm) { - // No category picker: the list always shows every saved style (the category is a column). - // Creating or moving to a category switches the view model's filter - put it back. - vm.SelectedStorageCategory = Se.Language.Assa.AllCategories; - vm.PropertyChanged += (_, e) => + // Category picker: saved styles are kept per series/project in categories, and with + // hundreds of them a flat list is unusable - the filter is back, with its actions (#16251) + var comboBoxCategory = UiUtil.MakeComboBox(vm.StorageCategories, vm, nameof(vm.SelectedStorageCategory)); + comboBoxCategory.HorizontalAlignment = HorizontalAlignment.Stretch; + comboBoxCategory.MinWidth = 120; + AutomationProperties.SetName(comboBoxCategory, Se.Language.General.Category); + + Button CategoryButton(string icon, string name, System.Windows.Input.ICommand command, bool needsCategory) { - if (e.PropertyName == nameof(vm.SelectedStorageCategory) && vm.SelectedStorageCategory != Se.Language.Assa.AllCategories) + var button = EditorChrome.CreateToolButton(icon, name, command, 24); + if (needsCategory) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => vm.SelectedStorageCategory = Se.Language.Assa.AllCategories); + button.Bind(Button.IsEnabledProperty, new Binding(nameof(vm.IsCategoryActionVisible)) { Source = vm }); } + + return button; + } + + var categoryPicker = new Grid + { + ColumnSpacing = 2, + ColumnDefinitions = + { + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(new GridLength(1, GridUnitType.Star)), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(GridLength.Auto), + }, }; + categoryPicker.Add(new TextBlock + { + Text = Se.Language.General.Category, + FontSize = 12, + Foreground = EditorChrome.DimTextBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 4, 0), + }, 0, 0); + categoryPicker.Add(comboBoxCategory, 0, 1); + categoryPicker.Add(CategoryButton("fa-solid fa-folder-plus", Se.Language.Assa.NewCategory, vm.NewCategoryCommand, false), 0, 2); + categoryPicker.Add(CategoryButton("fa-solid fa-pen", Se.Language.Assa.RenameCategory, vm.RenameCategoryCommand, true), 0, 3); + categoryPicker.Add(CategoryButton("fa-solid fa-folder-minus", Se.Language.Assa.DeleteCategory, vm.DeleteCategoryCommand, true), 0, 4); // No header sorting: the storage style order is persisted to settings in // list order on OK, so the collection order is not presentation-only. @@ -356,14 +384,16 @@ private static Grid MakeStorageStyles(AssaStylesViewModel vm) Binding = new Binding(nameof(StyleDisplay.Name)), Width = new GridLength(1, GridUnitType.Star), }); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn(120)); dataGrid.Columns.Add(new SeTableViewColumn { Header = Se.Language.General.Category, CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.CategoryDisplay)), - Width = new GridLength(100), + Width = new GridLength(90), }); + dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); // The default style gets a yellow star instead of "True"/"False" dataGrid.Columns.Add(new SeTableViewColumn { @@ -383,7 +413,7 @@ private static Grid MakeStorageStyles(AssaStylesViewModel vm) { Converter = new FuncValueConverter(isDefault => isDefault ? Se.Language.General.IsDefault : string.Empty), }, - Width = new GridLength(70), + Width = new GridLength(44), }); dataGrid.Bind(TableView.SelectedItemProperty, new Binding(nameof(vm.SelectedStorageStyle)) { Source = vm }); @@ -418,7 +448,7 @@ private static Grid MakeStorageStyles(AssaStylesViewModel vm) MenuItem(Se.Language.General.Export, "fa-solid fa-file-export", vm.StorageExportCommand), ])); - return MakeListSection(Se.Language.Assa.StylesSaved, actions, null, dataGrid, first: false); + return MakeListSection(Se.Language.Assa.StylesSaved, actions, categoryPicker, dataGrid, first: false); } private static Border MakeEditorPanel(AssaStylesViewModel vm) diff --git a/src/ui/Features/Assa/StylesDialogHelper.cs b/src/ui/Features/Assa/StylesDialogHelper.cs index 016568a485b..a25e567f82c 100644 --- a/src/ui/Features/Assa/StylesDialogHelper.cs +++ b/src/ui/Features/Assa/StylesDialogHelper.cs @@ -1,7 +1,11 @@ -using Avalonia; +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Data.Converters; using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; using Nikse.SubtitleEdit.Core.Common; using Nikse.SubtitleEdit.Features.Shared; using Nikse.SubtitleEdit.Logic; @@ -22,6 +26,97 @@ namespace Nikse.SubtitleEdit.Features.Assa; /// public static class StylesDialogHelper { + + /// + /// Default and smallest width of the style lists pane, left of the splitter. The lists + /// show name, font name, size, usages/category and a color sample, so a narrow pane + /// cuts the names off (#16251). + /// + public const double DefaultListsWidth = 480; + public const double MinListsWidth = 300; + + /// + /// "Font name" list column. + /// + public static SeTableViewColumn MakeFontNameColumn(double width = 130) => new() + { + Header = Se.Language.General.FontName, + CellTheme = UiUtil.TableViewCellTheme, + HeaderTheme = UiUtil.TableViewColumnHeaderTheme, + Binding = new Binding(nameof(StyleDisplay.FontName)), + Width = new GridLength(width), + }; + + /// + /// Color sample list column: "Aa" in the primary color on the outline color, so the + /// colors of every style show without selecting it - as SE4's advanced styles list did (#16251). + /// + public static SeTableViewColumn MakeColorSampleColumn() => new() + { + Header = Se.Language.General.Color, + CellTheme = UiUtil.TableViewCellTheme, + HeaderTheme = UiUtil.TableViewColumnHeaderTheme, + CellTemplate = new FuncDataTemplate((_, _) => new Border + { + Width = 46, + Height = 18, + CornerRadius = new CornerRadius(3), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + [!Border.BackgroundProperty] = new Binding(nameof(StyleDisplay.ColorOutline)) { Converter = ColorToBrush }, + Child = new TextBlock + { + Text = "Aa", + FontSize = 11.5, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + [!TextBlock.ForegroundProperty] = new Binding(nameof(StyleDisplay.ColorPrimary)) { Converter = ColorToBrush }, + [!TextBlock.FontStyleProperty] = new Binding(nameof(StyleDisplay.Italic)) { Converter = ItalicToFontStyle }, + }, + }), + NameBinding = new Binding(nameof(StyleDisplay.ColorPrimary)) { Converter = ColorToName }, + Width = new GridLength(58), + }; + + private static readonly IValueConverter ColorToBrush = new FuncValueConverter(c => new SolidColorBrush(c)); + private static readonly IValueConverter ItalicToFontStyle = new FuncValueConverter(italic => italic ? FontStyle.Italic : FontStyle.Normal); + private static readonly IValueConverter ColorToName = new FuncValueConverter(c => c.FromColorToHex(false)); + + /// + /// Puts the lists pane and the right side in with a draggable + /// splitter between them. The pane width is remembered across windows (ASSA and SSA share it) + /// and saved when closes. + /// + public static void AddListsSplitter(Window window, Grid content, Control lists, Control right) + { + var width = Se.Settings.Assa.StylesListsWidth >= MinListsWidth ? Se.Settings.Assa.StylesListsWidth : DefaultListsWidth; + content.ColumnDefinitions.Clear(); + content.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(width)) { MinWidth = MinListsWidth }); + content.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + content.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)) { MinWidth = 560 }); + + var splitter = new GridSplitter + { + Width = UiUtil.SplitterWidthOrHeight, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Stretch, + Background = EditorChrome.LineBrush, + }; + + content.Add(lists, 0, 0); + content.Add(splitter, 0, 1); + content.Add(right, 0, 2); + + window.Closing += delegate + { + var actual = content.ColumnDefinitions[0].ActualWidth; + if (actual >= MinListsWidth) + { + Se.Settings.Assa.StylesListsWidth = (int)Math.Round(actual); + } + }; + } /// /// Asks how to resolve a name clash: returns Custom1 (overwrite), Custom2 (keep both) or /// anything else (cancel), plus whether the answer applies to all remaining clashes. diff --git a/src/ui/Features/Ssa/SsaStylesWindow.cs b/src/ui/Features/Ssa/SsaStylesWindow.cs index 25cb31387d8..2aa1f4203a0 100644 --- a/src/ui/Features/Ssa/SsaStylesWindow.cs +++ b/src/ui/Features/Ssa/SsaStylesWindow.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Avalonia; using Avalonia.Automation; @@ -43,7 +43,7 @@ public SsaStylesWindow(SsaStylesViewModel vm) Mode = BindingMode.TwoWay, }); CanResize = true; - Width = 1200; + Width = 1400; Height = 800; MinWidth = 1000; MinHeight = 600; @@ -54,14 +54,8 @@ public SsaStylesWindow(SsaStylesViewModel vm) vm.Window = this; DataContext = vm; - var content = new Grid - { - ColumnDefinitions = - { - new ColumnDefinition(new GridLength(360)), - new ColumnDefinition(new GridLength(1, GridUnitType.Star)), - }, - }; + // Lists on the left, preview + editor on the right, a splitter between them (#16251) + var content = new Grid(); var right = new Grid { @@ -76,8 +70,7 @@ public SsaStylesWindow(SsaStylesViewModel vm) right.Add(MakeEditingBanner(vm), 1, 0); right.Add(MakeEditorPanel(vm), 2, 0); - content.Add(MakeListsPanel(vm), 0, 0); - content.Add(right, 0, 1); + StylesDialogHelper.AddListsSplitter(this, content, MakeListsPanel(vm), right); var mainGrid = new Grid { @@ -260,8 +253,10 @@ private static Grid MakeFileStyles(SsaStylesViewModel vm) dataGrid.DataContext = vm; dataGrid.ItemsSource = vm.FileStyles; dataGrid.Columns.Add(Column(Se.Language.General.Name, nameof(StyleDisplay.Name), new GridLength(1, GridUnitType.Star))); - dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(70))); - dataGrid.Columns.Add(Column(Se.Language.General.Usages, nameof(StyleDisplay.UsageCount), new GridLength(70))); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); + dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(56))); + dataGrid.Columns.Add(Column(Se.Language.General.Usages, nameof(StyleDisplay.UsageCount), new GridLength(56))); + dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); dataGrid.Bind(TableView.SelectedItemProperty, new Binding(nameof(vm.SelectedFileStyle)) { Source = vm }); dataGrid.SelectionChanged += vm.FileStylesChanged; @@ -309,7 +304,9 @@ private static Grid MakeStorageStyles(SsaStylesViewModel vm) dataGrid.DataContext = vm; dataGrid.ItemsSource = vm.StorageStyles; dataGrid.Columns.Add(Column(Se.Language.General.Name, nameof(StyleDisplay.Name), new GridLength(1, GridUnitType.Star))); - dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(70))); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); + dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(56))); + dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); // The default style gets a yellow star instead of "True"/"False" dataGrid.Columns.Add(new SeTableViewColumn @@ -330,7 +327,7 @@ private static Grid MakeStorageStyles(SsaStylesViewModel vm) { Converter = new FuncValueConverter(isDefault => isDefault ? Se.Language.General.IsDefault : string.Empty), }, - Width = new GridLength(70), + Width = new GridLength(44), }); dataGrid.Bind(TableView.SelectedItemProperty, new Binding(nameof(vm.SelectedStorageStyle)) { Source = vm }); diff --git a/src/ui/Logic/Config/SeAssa.cs b/src/ui/Logic/Config/SeAssa.cs index 1e309ef2a0f..4f58a6a13de 100644 --- a/src/ui/Logic/Config/SeAssa.cs +++ b/src/ui/Logic/Config/SeAssa.cs @@ -69,6 +69,11 @@ public class SeAssa public bool HideLayersFromVideoPreview { get; set; } public bool FontCollectorTrimFonts { get; set; } + /// + /// ASSA/SSA styles windows: width of the style lists pane (left of the splitter), 0 = default. + /// + public int StylesListsWidth { get; set; } + public SeAssa() { AutoSetResolution = true; From 2bd925cf90dcfa12549d60a2295ec9d4b65b196a Mon Sep 17 00:00:00 2001 From: TOKUZO Date: Tue, 20 Oct 2026 03:14:55 +0900 Subject: [PATCH 11/82] Update Japanese.json for 5.4 Beta3 Updated the Japanese translation for Subtitle Edit 5.4.0 Beta2, in preparation for Beta3. --- src/ui/Assets/Languages/Japanese.json | 73 +++++++++++---------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/src/ui/Assets/Languages/Japanese.json b/src/ui/Assets/Languages/Japanese.json index a2720bfd48d..e88755e9c8e 100644 --- a/src/ui/Assets/Languages/Japanese.json +++ b/src/ui/Assets/Languages/Japanese.json @@ -1,49 +1,9 @@ { "title": "Subtitle Edit", - "version": "v5.4.0-beta2", + "version": "v5.4.0-beta3", "translatedBy": "hisui3393", "cultureName": "jp-JP", "general": { - "prompt": "プロンプト", - "preset": "プリセット", - "output": "出力", - "outputFormat": "出力形式", - "other": "その他", - "timeCodeFormat": "タイムコード形式", - "topBottomMargin": "上下余白", - "theme": "テーマ", - "brightness": "明るさ", - "chooseColor": "色を選択", - "lineX": "行{0}", - "playLine": "行を再生", - "maxLineLength": "最大行長", - "maxCharsPerSec": "最大文字数/秒", - "maxNumberOfLines": "最大行数", - "subtitles": "字幕", - "shotChanges": "ショットチェンジ", - "chapters": "チャプター", - "audio": "音声", - "interjections": "感動詞", - "blankLines": "空行", - "editWholeText": "テキスト全体を編集", - "noTextInClipboard": "クリップボードにテキストがありません", - "noSubtitlesFoundTitle": "字幕が見つかりません", - "replacedXOccurrences": "{0}件を置換しました", - "invalidRegularExpressionX": "正規表現が無効です: {0}", - "discardChangesTitle": "変更を破棄しますか?", - "downloadingXDotDotDot": "{0} をダウンロード中...", - "importTimeCodesDotDotDot": "表示タイミングをインポート...", - "original": "元の状態", - "nextChange": "次の変更", - "previousChange": "前の変更", - "changeXOfY": "{1} 件中 {0} 件目の変更", - "tooLong": "長すぎる", - "tooShort": "短すぎる", - "overlapping": "重複している", - "detail": "詳細", - "fixOcrErrors": "OCRエラーを修正", - "runningOcrXY": "OCR処理中... {0}/{1}", - "llamaCppDownloadEnginePrompt": "llama.cpp を使用するには、llama-server エンジンのダウンロードが必要です。今すぐダウンロードしますか?", "abort": "中止", "actor": "声色", "actors": "声色", @@ -700,6 +660,9 @@ "styles": "スタイル", "subtitleFile": "字幕ファイル", "subtitleFileChangedOutsideX": "ファイル「{0}」がSubtitle Edit以外で変更されました。\n\n再読み込みしますか?(現在の変更内容は失われます)", + "subtitleFileChangedOutsideReloadX": "ファイル「{0}」がSubtitle Edit以外で変更されました。\n\n再読み込みしますか?", + "subtitleFileChangedOutsideOverwriteX": "ファイル「{0}」は読み込み後にSubtitle Edit以外で変更されました。\n\nSubtitle Editで編集中の字幕で上書きしますか?", + "subtitleFileChangedOutsideAutoSaveSkippedX": "自動保存をスキップしました:{0}はSubtitle Edit以外で変更されました", "subtitleFileName": "字幕ファイル名", "subtitleFileSaved": "字幕ファイルが保存されました", "subtitleFileSavedToX": "字幕ファイルが{0}に保存されました", @@ -1133,6 +1096,7 @@ "seekAmountHint": "移動量 {0}", "seekVideo": "動画シーク(<< >>)", "configureToolbarItems": "ツールバー項目の設定...", + "ruleProfileHint": "ルールプロファイル {0}", "textPreviousHint": "前の字幕を再生し、終端で停止 {0}", "textPlayHint": "現在の字幕を再生し、終端で停止 {0}", "textPauseHint": "再生を一時停止 {0}", @@ -1197,6 +1161,11 @@ "copyTextFromOriginalToCurrent": "並べた別字幕から作業字幕へテキストをコピーします", "insertedXTextsFromSubtitleY": "字幕ファイル{1}から{0}個のテキストを挿入しました", "italicHint": "選択した行/文字を斜体に", + "recordSpeechHint": "マイクから音声を録音してテキストボックスに入力します(もう一度クリックすると停止)", + "recordingSpeechClickToStop": "録音中... 停止するにはマイクボタンをもう一度クリックしてください", + "recordingFailedX": "録音に失敗しました:{0}", + "noMicrophoneFound": "マイクが見つかりません", + "noSpeechRecognized": "音声を認識できませんでした", "joinedSubtitleLoaded": "結合された字幕が読み込まれました", "lineXTextAndTimingChanged": "行{0}: テキストとタイミングが変更されました", "lineXTextChangedFromYToZ": "行{0}: テキストが{1}から{2}に変更されました", @@ -1476,6 +1445,7 @@ "addNewLineAfterTimeCode": "表示タイミングの後に改行を追加", "addNewLineAfterText": "テキストの後に改行を追加", "addLineBetweenSubtitles": "字幕間に空行を追加", + "mergeAllLines": "すべての字幕行を結合", "titleExportDCinemaInteropPng": "D-Cinema(Interop/png)で書き出し", "titleExportDCinemaSmpte2014Png": "D-Cinema(SMPTE 2014/png)で書き出し", "titleExportDCinemaSmpte2014Mxf": "D-Cinema(SMPTE 2014/MXF)で書き出し", @@ -1803,7 +1773,13 @@ "findWhat": "検索文字列", "descriptionOptional": "説明(任意)", "regularExpressionTooSlowX": "正規表現が{0}秒でタイムアウトしました - このルールはスキップされます。より単純なパターンを試してください", - "moveToCategory": "カテゴリーに移動" + "moveToCategory": "カテゴリーに移動", + "addRules": "ルールを追加", + "addRulesDotDotDot": "ルールを追加...", + "addRulesToX": "「{0}」にルールを追加", + "xRules": "{0:#,##0} 件のルール", + "discardXRules": "入力した{0}件のルールを破棄しますか?", + "enterAddsRule": "Enterキーでルールをリストに追加し、OKでリストをカテゴリに追加します(Ctrl+Enter)" }, "find": { "searchTextWatermark": "検索中...", @@ -2673,6 +2649,7 @@ "selectModel": "モデルを選択", "addCustomModelDotDotDot": "カスタムモデルを追加...", "customModelHelp": "独自のモデルを使用できます。モデルファイル(whisper.cpp の ggml「.bin」ファイル)またはモデルフォルダー(「model.bin」を含む faster-whisper フォルダー)を選択してください。\n\n選択したモデルは models フォルダーにコピーされ、上の一覧に追加されます", + "customModelHelpGguf": "独自のモデルを使用できます。qwen3-asr.cpp用の「.gguf」ファイルを選択してください(音声エンコーダーと言語モデルの両方を1つにまとめたファイル。Hugging Face上のOpenVoiceOSやFlippyDoraなどから入手できます)。ファイルはモデルフォルダーにコピーされ、上のリストに追加されます。llama.cpp、transcribe.cpp、CapsWriter用に作成されたGGUFファイルは構成が異なるため、使用できません。", "viewToolsLogFile": "Whisperログファイルを表示", "updateXTitle": "{0}を更新しますか?", "updateXMessage": "{0}の新しいバージョンが利用可能です。{1}{1}今すぐアップデートをダウンロードしてインストールしますか?", @@ -2866,7 +2843,6 @@ "removeOriginalSpeechDescription": "元の動画の音楽や効果音を残したまま、話し声だけを除去し、新しい音声を重ねます。GPU使用時でも動画とほぼ同じ時間がかかり、GPUがない場合は何倍も時間がかかります。音量のダッキングを有効にすると、その音量は音楽や効果音にも適用されます。音声を動画ファイルに追加する場合のみ適用されます。", "removingOriginalSpeech": "元の音声を除去中...", "removeOriginalSpeechFailed": "元の音声を除去できませんでした。代わりに元の音声を残したまま動画に追加しました。", - "speechRemovalModel": "モデル", "speechRemovalModelDescription": "「最高品質」は音楽や効果音をできるだけ多く残します。「高速」は処理が約3倍速くなりますが、音楽がやや多く失われます。", "speechRemovalBestQuality": "最高品質(Mel-Band RoFormer)", "speechRemovalFast": "高速(HTDemucs)", @@ -3139,6 +3115,8 @@ "remuxVideoMp4CaptionsHint": "字幕はATSC A/53のSEIユーザーデータとしてH.264/H.265動画に格納されます。1つ目の字幕ファイルはCC1、2つ目はCC3になります。動画と音声は再エンコードせず、そのままコピーされます。", "remuxVideoMp4MaxTwoCaptionTracks": "動画内に格納できるクローズドキャプションは最大2トラック(CC1とCC3)です。字幕ファイルを削除し、1つまたは2つだけ残してください。", "remuxVideoMp4CaptionsNeedH264X": "MP4ファイル内のクローズドキャプションにはH.264またはH.265の動画が必要ですが、現在の動画は{0}です。代わりに字幕トラックを選択するか、出力形式に.mpgを指定してください。", + "remuxVideoCaptionOffset": "字幕のズレ:", + "remuxVideoCaptionOffsetHint": "字幕のタイムコードから差し引く値です。放送用の.scc/.mccファイルは、映像がゼロから始まるのに対し、字幕が01:00:00:00から始まることがよくあります。最初の字幕の時刻が映像の終了時刻を超えている場合、ズレの値が提案されます。", "embedSubtitlesDotDotDot": "埋め込み字幕の追加/削除", "syncAudioTrackDotDotDot": "音声トラックの同期...", "syncAudioTrackTitle": "音声トラックの同期", @@ -3444,6 +3422,7 @@ "continuationStyleLeadingTrailingDash": "「―」", "continuationStyleLeadingTrailingDashDots": "続きは「—」 間は「…」", "cpsLineLengthStyle": "読みやすさに合わせた行の 長さ調整", + "cpsLineLengthIgnoreTagsRegex": "カスタムタグを無視(正規表現)", "cpsLineLengthStyleCalcAll": "全ての文字をカウント", "cpsLineLengthStyleCalcNoSpaceCpsOnly": "スペースを除くすべての文字をカウント (CPSのみ)", "cpsLineLengthStyleCalcNoSpace": "スペースを除くすべての文字をカウント", @@ -3666,7 +3645,7 @@ "timeCodeModeFrames": "フレーム(hh:mm:ss:ff)", "timeCodeModeFrameNumbers": "フレーム番号", "frameModeSnapToVideoFrames": "時間を動画のフレームに合わせて保存(各秒内のフレームではなく)", - "textBoxLimitNewLines": "テキストボックスの行数を制限します", + "textBoxLimitNewLines": "テキストボックスの行数を制限", "textBoxAutoWrapWhileTyping": "入力中に自動で折り返す", "mpvOpenGl": "GPU (OpenGL):高速,滑らか", "mpvSoftwareRendering": "CPUのみ:互換性重視(低速)", @@ -3692,6 +3671,10 @@ "subtitleDoubleClickAction": "字幕一覧で ダブルクリックした時の動作", "subtitleGridCenterSelectedRow": "字幕一覧で前/次行選択時に中央表示", "saveAsBehavior": "\"名前を付けて保存\"の動作", + "fileChangedOutsideAction": "別のプログラムによる字幕ファイルの変更", + "fileChangedOutsideReloadWhenUnmodified": "未保存の変更がなければ再読み込みし、ある場合は確認する", + "fileChangedOutsideAlwaysAsk": "常に確認する", + "fileChangedOutsideNever": "何もしない", "defaultSaveLocation": "既定の保存先", "defaultSaveLocationSourceFileFolder": "字幕/動画ファイルのフォルダー", "defaultSaveLocationLastUsedFolder": "前回使用したフォルダー", @@ -3752,6 +3735,7 @@ "textBoxButtonShowColor": "テキスト欄: カラーボタンを表示", "textBoxButtonShowRemoveFormatting": "テキスト欄: 書式設定削除ボタンを表示", "textBoxButtonShowAiAssistant": "テキストボックス: AIアシスタントボタンを表示", + "textBoxButtonShowRecordSpeech": "テキストボックス:音声入力ボタンを表示(マイク入力をテキスト化)", "waveformSingleClickAction": "波形シングルクリックアクション", "waveformDoubleClickAction": "波形ダブルクリックアクション(シングルクリックアクション後)", "allSettings": "すべての設定", @@ -3837,6 +3821,7 @@ "alsoSetVideoPosition": "ビデオ位置も設定", "generalGoToVideoPosition": "ビデオの位置へ移動", "generalToggleItalic": "斜体のオン/オフ", + "generalRecordSpeechToTextBox": "テキストボックスに音声入力(開始/停止)", "generalToggleBold": "太字のオン/オフ", "generalToggleUnderline": "下線のオン/オフ", "generalToggleBox": "ボックスの切り替え(EBU STL)", From edd01061037c09586e085f7c507aba503efeb878 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 20:22:11 +0200 Subject: [PATCH 12/82] ASSA/SSA styles: file list Name 150 + Font name 110, Size and Usages 70 Co-Authored-By: Claude Fable 5.1 --- src/ui/Features/Assa/AssaStylesWindow.cs | 8 ++++---- src/ui/Features/Ssa/SsaStylesWindow.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/Features/Assa/AssaStylesWindow.cs b/src/ui/Features/Assa/AssaStylesWindow.cs index 93be6b8808e..5d88531782b 100644 --- a/src/ui/Features/Assa/AssaStylesWindow.cs +++ b/src/ui/Features/Assa/AssaStylesWindow.cs @@ -259,16 +259,16 @@ private static Grid MakeFileStyles(AssaStylesViewModel vm) CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.Name)), - Width = new GridLength(1, GridUnitType.Star), + Width = new GridLength(150), }); - dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn(110)); dataGrid.Columns.Add(new SeTableViewColumn { Header = Se.Language.General.FontSize, CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.FontSize)), - Width = new GridLength(56), + Width = new GridLength(70), }); dataGrid.Columns.Add(new SeTableViewColumn { @@ -276,7 +276,7 @@ private static Grid MakeFileStyles(AssaStylesViewModel vm) CellTheme = UiUtil.TableViewCellTheme, HeaderTheme = UiUtil.TableViewColumnHeaderTheme, Binding = new Binding(nameof(StyleDisplay.UsageCount)), - Width = new GridLength(56), + Width = new GridLength(70), }); dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); diff --git a/src/ui/Features/Ssa/SsaStylesWindow.cs b/src/ui/Features/Ssa/SsaStylesWindow.cs index 2aa1f4203a0..b3a39613be0 100644 --- a/src/ui/Features/Ssa/SsaStylesWindow.cs +++ b/src/ui/Features/Ssa/SsaStylesWindow.cs @@ -252,10 +252,10 @@ private static Grid MakeFileStyles(SsaStylesViewModel vm) AutomationProperties.SetName(dataGrid, Se.Language.Assa.StylesInFile); dataGrid.DataContext = vm; dataGrid.ItemsSource = vm.FileStyles; - dataGrid.Columns.Add(Column(Se.Language.General.Name, nameof(StyleDisplay.Name), new GridLength(1, GridUnitType.Star))); - dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); - dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(56))); - dataGrid.Columns.Add(Column(Se.Language.General.Usages, nameof(StyleDisplay.UsageCount), new GridLength(56))); + dataGrid.Columns.Add(Column(Se.Language.General.Name, nameof(StyleDisplay.Name), new GridLength(150))); + dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn(110)); + dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(70))); + dataGrid.Columns.Add(Column(Se.Language.General.Usages, nameof(StyleDisplay.UsageCount), new GridLength(70))); dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); dataGrid.Bind(TableView.SelectedItemProperty, new Binding(nameof(vm.SelectedFileStyle)) { Source = vm }); @@ -305,7 +305,7 @@ private static Grid MakeStorageStyles(SsaStylesViewModel vm) dataGrid.ItemsSource = vm.StorageStyles; dataGrid.Columns.Add(Column(Se.Language.General.Name, nameof(StyleDisplay.Name), new GridLength(1, GridUnitType.Star))); dataGrid.Columns.Add(StylesDialogHelper.MakeFontNameColumn()); - dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(56))); + dataGrid.Columns.Add(Column(Se.Language.General.FontSize, nameof(StyleDisplay.FontSize), new GridLength(70))); dataGrid.Columns.Add(StylesDialogHelper.MakeColorSampleColumn()); // The default style gets a yellow star instead of "True"/"False" From cb74f7bfc99a6264fa84668ee835f77e5468f813 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:39:18 +0200 Subject: [PATCH 13/82] Update Italian.json --- src/ui/Assets/Languages/Italian.json | 564 ++++++++++++++++++++------- 1 file changed, 419 insertions(+), 145 deletions(-) diff --git a/src/ui/Assets/Languages/Italian.json b/src/ui/Assets/Languages/Italian.json index f40955c8b76..a5db6075bf8 100644 --- a/src/ui/Assets/Languages/Italian.json +++ b/src/ui/Assets/Languages/Italian.json @@ -1,47 +1,9 @@ { "title": "SubtitleEdit", - "version": "v5.3.0", - "translatedBy": "v. 12.10.2026 di bovirus e GitGianluc", + "version": "v5.4.0-beta2", + "translatedBy": "v. 19.10.2026 di bovirus e GitGianluc", "cultureName": "it-IT", "general": { - "temperature": "Temperatura", - "preset": "Profilo", - "outputFormat": "Formato destinazione", - "other": "Altro", - "timeCodeFormat": "Formato time code", - "topBottomMargin": "Margine superiore/inferiore", - "theme": "Tema", - "brightness": "Luminosità", - "chooseColor": "Scegli colore", - "lineX": "Riga {0}", - "playLine": "Riproduci riga", - "maxLineLength": "Lunghezza massima riga", - "maxCharsPerSec": "Caratteri/sec massimi", - "maxNumberOfLines": "Numero massimo di righe", - "subtitles": "Sottotitoli", - "shotChanges": "Cambiamenti inquadratura", - "chapters": "Capitoli", - "interjections": "Interiezioni", - "blankLines": "Righe vuote", - "editWholeText": "Modifica intero testo", - "noTextInClipboard": "Nessun testo negli appunti", - "noSubtitlesFoundTitle": "Nessun sottotitolo trovato", - "replacedXOccurrences": "Sostituite {0} occorrenze", - "invalidRegularExpressionX": "Espressione regolare non valida: {0}", - "discardChangesTitle": "Vuoi eliminare le modifiche?", - "downloadingXDotDotDot": "Download di {0}...", - "importTimeCodesDotDotDot": "Importa codici temporali...", - "original": "Originale", - "nextChange": "Modifica successiva", - "previousChange": "Modifica precedente", - "changeXOfY": "Modifica {0} / {1}", - "tooLong": "Troppo lunga", - "tooShort": "Troppo corta", - "overlapping": "Sovrapposizione", - "detail": "Dettagli", - "fixOcrErrors": "Correggi errori OCR", - "runningOcrXY": "Esecuzione OCR... {0}/{1}", - "llamaCppDownloadEnginePrompt": "Per usare llama.cpp è necessario scaricare il motore llama-server. Vuoi scaricarlo?", "abort": "Annulla", "actor": "Attore", "actors": "Attori", @@ -527,6 +489,7 @@ "reason": "Motivo", "recentFiles": "File recenti", "recentlyUsedVideoOffsets": "Offset video usati di recente", + "recent": "Recente", "redo": "Ripeti azione", "redownload": "Scarica nuovamente...", "release": "Release", @@ -697,6 +660,9 @@ "styles": "Stili", "subtitleFile": "File sottotitoli", "subtitleFileChangedOutsideX": "Il file '{0}' è stato modificato al di fuori di Subtitle Edit.\r\n\r\nVuoi ricaricarlo e perdere le modifiche?", + "subtitleFileChangedOutsideReloadX": "Il file '{0}' è stato modificato al di fuori di Subtitle Edit.\r\nVuoi ricaricarlo?", + "subtitleFileChangedOutsideOverwriteX": "Il file '{0}' è stato modificato fuori da Subtitle Edit dopo essere stato caricato.\r\nVuoi sovrascriverlo con il sottotitolo in Subtitle Edit?", + "subtitleFileChangedOutsideAutoSaveSkippedX": "Salvataggio automatico saltato: '{0}' è stato modificato al di fuori di Subtitle Edit", "subtitleFileName": "Nome file sottotitoli", "subtitleFileSaved": "File sottotitoli salvato", "subtitleFileSavedToX": "File sottotitoli salvato in {0}", @@ -752,6 +718,8 @@ "undelete": "Annulla eliminazione", "underline": "Sottolineato", "undo": "Annulla operazione", + "uninstall": "Disinstallare", + "uninstalled": "Disinstallato", "unknown": "Sconosciuto", "unknownNoInstallRecord": "Sconosciuto (nessuna registrazione installazione)", "unknownSubtitleFormat": "Formato sottotitoli sconosciuto", @@ -864,8 +832,6 @@ "openAiCompatibleSttExtraHeaders": "Intestazioni aggiuntive", "openAiCompatibleSttTimeout": "Timeout (secondi)", "openAiCompatibleSttLanguage": "Suggerimento lingua", - "openAiCompatibleSttTemperature": "Temperatura", - "openAiCompatibleSttPrompt": "Prompt", "openAiCompatibleSttModelRejectedHint": "Il server ha rifiutato il nome del modello.\r\nNon tutti gli endpoint compatibili con OpenAI accettano un modello: per esempio, https://api.x.ai/v1/stt di xAI, non ha alcun parametro 'modello'.\r\nProva a cancellare il campo Modello.", "dashScopeSttRegion": "Regione", "dashScopeSttEnableWords": "Timestamp a livello di parola", @@ -891,7 +857,48 @@ "transcriptionProgressServerFormat": "Server: {0}", "transcriptionProgressModelFormat": "Modello: {0}", "transcriptionProgressReceivedSegmentsFormat": "Segmenti ricevuti ({0})", - "transcriptionProgressModelAuto": "(automatico)" + "transcriptionProgressModelAuto": "(automatico)", + "prompt": "Richiesta", + "temperature": "Temperatura", + "preset": "Profilo", + "output": "Output", + "outputFormat": "Formato destinazione", + "other": "Altro", + "timeCodeFormat": "Formato time code", + "topBottomMargin": "Margine superiore/inferiore", + "theme": "Tema", + "brightness": "Luminosità", + "chooseColor": "Scegli colore", + "lineX": "Riga {0}", + "playLine": "Riproduci riga", + "maxLineLength": "Lunghezza massima riga", + "maxCharsPerSec": "Caratteri/sec massimi", + "maxNumberOfLines": "Numero massimo di righe", + "subtitles": "Sottotitoli", + "shotChanges": "Cambiamenti inquadratura", + "chapters": "Capitoli", + "audio": "Audio", + "interjections": "Interiezioni", + "blankLines": "Righe vuote", + "editWholeText": "Modifica intero testo", + "noTextInClipboard": "Nessun testo negli appunti", + "noSubtitlesFoundTitle": "Nessun sottotitolo trovato", + "replacedXOccurrences": "Sostituite {0} occorrenze", + "invalidRegularExpressionX": "Espressione regolare non valida: {0}", + "discardChangesTitle": "Vuoi eliminare le modifiche?", + "downloadingXDotDotDot": "Download di {0}...", + "importTimeCodesDotDotDot": "Importa codici temporali...", + "original": "Originale", + "nextChange": "Modifica successiva", + "previousChange": "Modifica precedente", + "changeXOfY": "Modifica {0} / {1}", + "tooLong": "Troppo lunga", + "tooShort": "Troppo corta", + "overlapping": "Sovrapposizione", + "detail": "Dettagli", + "fixOcrErrors": "Correggi errori OCR", + "runningOcrXY": "Esecuzione OCR... {0}/{1}", + "llamaCppDownloadEnginePrompt": "Per usare llama.cpp è necessario scaricare il motore llama-server. Vuoi scaricarlo?" }, "main": { "menu": { @@ -1089,6 +1096,7 @@ "seekAmountHint": "Cerca importo {0}", "seekVideo": "Cerca video (<< >>)", "configureToolbarItems": "Configura elementi barra strumenti...", + "ruleProfileHint": "Profilo regola {0}", "textPreviousHint": "Riproduci sottotitolo precedente e alla fine fermati {0}", "textPlayHint": "Riproduci sottotitolo attuale e alla fine fermati {0}", "textPauseHint": "Metti in pausa la riproduzione {0}", @@ -1153,6 +1161,11 @@ "copyTextFromOriginalToCurrent": "Copia testo dai sottotitoli originali ai sottotitoli attuali", "insertedXTextsFromSubtitleY": "Inseriti '{0}' testi dal file sottotitoli '{1}'", "italicHint": "Righe/testo selezionati in corsivo", + "recordSpeechHint": "Registra il discorso dal microfono nella casella di testo (per interrompere fai di nuovo clic)", + "recordingSpeechClickToStop": "Registrazione... per interrompere fai nuovamente clic sul pulsante del microfono", + "recordingFailedX": "Registrazione non riuscita: {0}", + "noMicrophoneFound": "Nessun microfono trovato", + "noSpeechRecognized": "Nessun discorso riconosciuto", "joinedSubtitleLoaded": "Uniti sottotitoli caricati", "lineXTextAndTimingChanged": "Riga {0}: testo e tempistica modificati", "lineXTextChangedFromYToZ": "Riga {0}: testo modificato da '{1}' a '{2}'", @@ -1160,7 +1173,6 @@ "loadingWaveInfoFromCache": "Caricamento info onda dalla cache...", "clickToGenerateWaveform": "Fai clic per generare la forma d'onda", "waveformFfmpegNotFoundClickToSetUp": "FFmpeg non trovato: per configurare FFmpeg e generare la forma d'onda fai clic sulla forma d'onda", - "noTextInClipboard": "Nessun testo negli appunti", "numberOfLinesEvenlyDistributedX": "{0} righe uniformemente distribuite", "oneLineCopiedFromOriginal": "Una riga copiata dal sottotitolo originale", "oneLineCopiedToOriginal": "Una riga copiata nel sottotitolo originale", @@ -1256,7 +1268,6 @@ "toolXChangedYLines": "{0}: {1} righe modificate", "transcriptionCompletedWithXLines": "Trascrizione completata con {0} righe", "openSubtitleFileX": "Vuoi aprire il file sottotitoli '{0}'?", - "replacedXOccurrences": "Sostituite {0} occorrenze", "ffmpegDownloadedAndInstalledToX": "ffmpeg scaricato e installato in {0}", "nothingToSave": "Niente da salvare", "formatLimitWarningTitle": "Superati limiti del formato", @@ -1323,10 +1334,11 @@ "maxLengthErrorTeletextColored": "Riga {0}-{1}: 36 (non {2}) dovrebbe essere il numero massimo di caratteri per il testo colorato a doppia altezza", "maxLengthErrorTeletextWhite": "Riga {0}-{1}: 37 (non {2}) dovrebbe essere il numero massimo di caratteri per il testo bianco a doppia altezza", "maxLengthErrorTeletextSingleHeight": "Riga {0}: 38 (non {1}) dovrebbe essere il numero massimo di caratteri per il testo bianco ad altezza singola", - "discSequenceNumber": "Numero sequenza disco" + "discSequenceNumber": "Numero sequenza disco", + "programme": "Programma", + "discAndDisplay": "Disco e display" }, "import": { - "importTimeCodes": "Importa codici temporali...", "plainTextDotDotDot": "Testo semplice", "titleImportPlainText": "Importa testo semplice", "csvXlsxCustomColumnsDotDotDot": "CSV/XLSX con colonne personalizzate...", @@ -1360,12 +1372,10 @@ "dvdMenuIfoNoTitleSetsX": "Nessun set titoli (VTS_xx_0.IFO) trovato accanto a {0}", "dvdEncryptedXOfY": "{0} dei {1} pacchetti di sottotitoli sono ancora crittografati CSS: questi file VOB sono stati copiati senza decrittografarli, quindi alcune immagini dei sottotitoli saranno confuse.\r\n\r\nPer un risultato ottimale prima decripta il DVD con uno strumento di estrazione DVD.\r\n\r\nVuoi continuare?", "splitTextAt": "Dividi il testo in", - "blankLines": "Righe vuote", "oneLineIsOneSubtitle": "Una riga è un sottotitolo", "twoLinesAreOneSubtitle": "Due righe sono un sottotitolo", "importFilesDotDotDot": "Importa file...", "multipleFiles": "Importa da più file di testo (un file è un sottotitolo)", - "maxLineLength": "Lunghezza massima riga", "mergeShortLines": "Unisci righe brevi", "fixed": "Corretta", "numberOfSubtitlesX": "Numero sottotitoli: {0}", @@ -1389,7 +1399,6 @@ "export": { "exportImagesProfiles": "Esporta profili immagini", "leftRightMargin": "Margine sinistro/destro", - "topBottomMargin": "Margine superiore/inferiore", "lineSpacingPercent": "Interlinea %", "paddingLeftRight": "Riempimento sinistra/destra", "paddingTopBottom": "Riempimento alto/basso", @@ -1411,14 +1420,22 @@ "exportDvbTeletextSubtitleType": "Tipo sottotitoli", "exportDvbTeletextSubtitleTypeNormal": "Normale", "exportDvbTeletextSubtitleTypeHearingImpaired": "Non udenti", + "exportPacCodePage": "Scegli tabella codici PAC", + "exportPacSecondaryCodePage": "Pagina codici secondaria", "customTextFormatsDotDotDot": "Formati testo personali_zzati...", "plainTextDotDotDot": "_Testo semplice...", + "dialogueListDotDotDot": "Elenco elenco dialoghi Excel (.xlsx)...", + "titleExportDialogueList": "Esporta elenco dialoghi Excel", + "dialogueListLineBreaksInCell": "Mantieni le interruzioni di riga all'interno della cella (disattivato: unisce le righe con ' | ')", + "dialogueListHeaderRow": "Riga intestazione", + "dialogueListFramesFormat": "oo:mm:ss:ff (fotogrammi)", + "dialogueListNoColumns": "Scegli almeno una colonna.", + "dialogueListEnd": "Fine", "customTextFormats": "Formati testo personalizzati", "titleExportCustomFormat": "Esporta formato testo personalizzato", "editCustomFormat": "Modifica formato testo personalizzato", "newCustomFormat": "Nuovo formato testo personalizzato", "deleteSelectedCustomTextFormatX": "Vuoi eliminare il formato testo personalizzato selezionato '{0}'?", - "timeCodeFormat": "Formato codice temporale", "newLineFormat": "Nuovo formato riga", "pleaseEnterNameForTheCustomFormat": "Inserisci il nome per il formato personalizzato", "titleExportPlainText": "Esporta testo semplice", @@ -1428,8 +1445,11 @@ "addNewLineAfterTimeCode": "Dopo il codice temporale aggiungi una nuova riga", "addNewLineAfterText": "Aggiungi una nuova riga dopo il testo", "addLineBetweenSubtitles": "Aggiungi una linea tra i sottotitoli", + "mergeAllLines": "Unisci tutte le righe", "titleExportDCinemaInteropPng": "Interoperabilità/png D-Cinema", "titleExportDCinemaSmpte2014Png": "D-Cinema SMPTE 2014/png", + "titleExportDCinemaSmpte2014Mxf": "D-Cinema SMPTE 2014 (MXF)", + "titleExportDCinemaSmpte2014PngMxf": "D-Cinema SMPTE 2014/png (MXF)", "imageBasedSubtitleSaved": "Sottotitoli basati su immagini salvati", "fullFrameImage": "Immagine a fotogramma intero", "fullFrameImageHint": "Crea immagini delle dimensioni del quadro video, mantenendo i sottotitoli nella loro posizione", @@ -1447,7 +1467,6 @@ "plane3DXNoDepth": "{0} (nessun valore profondità)", "textEffect": "Effetto testo", "textEffectSettingsTitle": "Impostazioni effetto testo", - "textEffectPreset": "Profilo", "textEffectStrength": "Intensità", "textEffectLetterSpacing": "Spazio lettere", "textEffectArcBend": "Curva", @@ -1520,7 +1539,6 @@ "gapMaximum": "Gap - massimo: {0:#,##0} ms", "gapAverage": "Gap - media: {0:#,##0.##} ms", "gapExceedingMinimum": "Gap - inferiore al minimo ({0:#,##0} ms): {1} ({2:0.00}%)", - "subtitles": "Sottotitoli", "words": "Parole", "totalDurationShort": "Durata totale", "timingAndPacing": "Timing e ritmo", @@ -1550,7 +1568,6 @@ "restoreXFromY": "Vuoi ripristinare '{0}' da {1}?", "deleteAllSubtitleBackups": "Vuoi eliminare tutti i file backup sottotitoli?", "deleteAll": "Elimina tutto", - "subtitles": "Sottotitoli", "settingsBackupInfo": "All'avvio di Subtitle Edit viene eseguito il backup di Settings.json al massimo una volta ogni {0} giorno/i.\r\nVengono conservati i {1} backup più recenti.", "settingsBackupInfoEveryStart": "Ogni volta che si avvia la modifica dei sottotitoli, se le impostazioni sono cambiate, verrà eseguito il backup di Settings.json. Verranno conservati i {0} backup più recenti.", "settingsBackupOff": "Il backup automatico delle impostazioni è disattivato (Opzioni → Impostazioni → File).", @@ -1580,14 +1597,18 @@ "startTime": "Tempo iniziale", "fontId": "Identificativo del carattere", "fontUri": "URI font", - "chooseColor": "Scegli colore", "fontEffect": "Effetto font", "effectColor": "Colore effetto", - "topBottomMargin": "Margine superiore/inferiore", "fadeUpTime": "Durata dissolvenza in alto", "fadeDownTime": "Durata dissolvenza in basso", "zPosition": "Posizione asse Z", - "zPositionHelp": "I valori positivi allontanano il testo, quelli negativi lo avvicinano. Se la posizione dell'asse z è pari a zero, la visualizzazione è in 2D" + "zPositionHelp": "I valori positivi allontanano il testo, quelli negativi lo avvicinano. Se la posizione dell'asse z è pari a zero, la visualizzazione è in 2D", + "fontFile": "File font", + "chooseFontFile": "Scegli file font", + "fontFileMissing": "Scegli il file font TrueType/OpenType da incorporare nel file traccia MXF.", + "reelDuration": "Durata reel", + "reelDurationHint": "OO:MM:SS:FF - la lunghezza dell'immagine che deve avere ogni risorsa di una reel DCP.\r\nVuoto = fino all'ultimo sottotitolo.", + "reelDurationInvalid": "La durata del reel deve essere vuota o OO:MM:SS:FF." }, "webVtt": { "setStylesForSelectedLinesTitle": "Imposta stili WebVTT per le righe selezionate", @@ -1675,7 +1696,6 @@ "saveCompareHtmlTitle": "Salva file HTML confronto", "pickMatroskaTrackX": "Scegli traccia Matroska - {0}", "pickTransportStreamTrackX": "Seleziona traccia Transport Stream - {0}", - "chapters": "Capitoli", "dvdVobFilesMissingX": "File VOB mancanti ({0}% trovati)", "pickMp4TrackX": "Scegli traccia MP4 - {0}", "pickMxfTrackX": "Scegli la traccia MXF - {0}", @@ -1688,7 +1708,6 @@ "propertyDefaultStyle": "Stile predefinito", "propertyDefaultRegion": "Regione predefinita", "propertyStyleAttributeName": "Nome attributo stile", - "propertyTimeCodeFormat": "Formato time code", "propertyTopOrigin": "Origine superiore", "propertyTopExtent": "Misura massima", "propertyBottomOrigin": "Origine inferiore", @@ -1719,8 +1738,8 @@ "moreThanTwoLines": "Più di due righe", "bookmarked": "Segnato", "forced": "Forzate", + "hasErrors": "Presenta errori (evidenziati nell'elenco)", "bookmarkContains": "Il segnalibro contiene", - "blankLines": "Righe vuote", "hearingImpaired": "Non udenti (SDH)", "hearingImpairedBrackets": "Parentesi quadre [...]", "hearingImpairedCurlyBrackets": "Parentesi graffe {...}", @@ -1730,8 +1749,8 @@ "hearingImpairedUppercaseLine": "Riga in maiuscolo", "hearingImpairedLineContains": "La riga contiene", "hearingImpairedMusicSymbols": "Solo simboli musicali", - "hearingImpairedInterjections": "Interiezioni", "hearingImpairedSettingsHint": "I dettagli provengono da Strumenti > Rimuovi testo per non udenti", + "selection": "Selezione", "selectionNew": "Nuova selezione", "selectionAdd": "Aggiungi alla selezione", "selectionSubtract": "Sottrai dalla selezione", @@ -1753,9 +1772,14 @@ "deleteRuleConfirm": "Vuoi eliminare la regola '{0}'?", "findWhat": "Trova questo", "descriptionOptional": "Descrizione (facoltativa)", - "invalidRegularExpressionX": "Espressione regolare non valida: {0}", "regularExpressionTooSlowX": "L'espressione regolare è stata interrotta dopo {0} secondi: la regola è stata saltata, prova uno schema più semplice", - "moveToCategory": "Sposta nella categoria" + "moveToCategory": "Sposta nella categoria", + "addRules": "Aggiungi regole", + "addRulesDotDotDot": "Aggiungi regole...", + "addRulesToX": "Aggiungi regole a '{0}'", + "xRules": "{0:#,##0} regole", + "discardXRules": "Vuoi scartare le '{0}' regole inserite?", + "enterAddsRule": "'Invio' aggiunge la regola all'elenco, 'OK' aggiunge l'elenco alla categoria (Ctrl+Invio)" }, "find": { "searchTextWatermark": "Cerca testo...", @@ -1769,8 +1793,7 @@ "replaceInTextAndOriginal": "Testo e testo originale", "replaceInTextOnly": "Solo testo", "replaceInOriginalOnly": "Solo testo originale", - "replacedOneOccurrence": "Sostituita un'occorrenza", - "replacedXOccurrences": "Sostituite {0} occorrenze" + "replacedOneOccurrence": "Sostituita un'occorrenza" }, "regularExpressionContextMenu": { "wordBoundary": "Limite parola (\\b)", @@ -1793,7 +1816,6 @@ "pleaseSelectExactlyOneSubtitle": "Seleziona uno specifico sottotitolo." }, "sourceView": { - "discardChangesTitle": "Vuoi eliminare le modifiche?", "discardChanges": "Il testo sorgente è stato modificato.\nVuoi eliminare le modifiche?", "xLinesYSubtitles": "{0} righe, {1} sottotitoli", "xLinesYSubtitlesZErrors": "{0} righe, {1} sottotitoli, {2} errore/i", @@ -1827,9 +1849,7 @@ "categoryGrammar": "Grammatica", "categoryPunctuation": "Punteggiatura", "categoryCasing": "Involucro", - "categoryOther": "Altro", "xNeedACloserLook": "{0} suggerimento/i necessitano di uno sguardo più attento", - "lineX": "Riga {0}", "linesXToY": "Righe {0}-{1}", "largeChangeWarning": "Grandi cambiamenti: sembra una riscrittura, rivedi attentamente", "mismatchWarning": "Non somiglia all'originale, potrebbe appartenere ad una riga diversa", @@ -2032,12 +2052,10 @@ "title": "Abbellisci codici temporali", "snapToFrames": "Aggancia ai fotogrammi", "minDuration": "Durata minima (ms)", - "original": "Originale", "beautified": "Abbellito", "subtitlesCount": "Sottotitoli", "changedCount": "Cambiati", "shotChangesCount": "Cambio inquadratura", - "changeXOfY": "Modifica {0} / {1}", "noChanges": "Nessun cambiamento: ogni indicazione è già nella destinazione", "snappedToShotChange": "Agganciato al cambia inquadratura", "snappedToFrame": "agganciato al fotogramma", @@ -2045,8 +2063,6 @@ "minDurationEnforced": "min. durata forzata", "maxDurationEnforced": "massima durata forzata", "noReasonNote": "—", - "previousChange": "Modifica precedente", - "nextChange": "Modifica successiva", "editProfile": "Modifica profilo...", "batchSnapToShotChanges": "Aggancia i marcatori ai cambi di inquadratura (se disponibili)", "batchFrameRateFromVideo": "Usa la frequenza fotogrammi del file video che presenta lo stesso nome del file dei sottotitoli", @@ -2089,7 +2105,6 @@ "showSpeechOnly": "Visualizza solo parlato", "showSpeechOnlyHint": "Disegna la forma d'onda allineata dall'audio rimuovendo la musica e gli effetti sonori; la forma d'onda originale rimane così com'è, per il confronto. Disponibile una volta isolato il parlato - qui o con 'Visualizza solo parlato' nella forma d'onda della finestra principale.", "align": "Allinea", - "original": "Originale", "aligned": "Allineato", "intro": "Per ascoltare ogni riga dell'audio e stringerne i time code scegli un allineatore e 'Allinea'.", "extractingAudio": "Estrazione audio...", @@ -2101,9 +2116,6 @@ "toCheckX": "Da controllare: {0}", "largeMovesToCheckX": "{0} righe si sposterebbero molto da sole e rimarrebbero deselezionate: riproduci originale/allineato e seleziona quelle giuste.", "linesToCheckX": "{0} righe vengono lasciata deselezionata per controllarle: ascoltale (F5 riproduce la riga allineata, Maiusc+F5 l'originale) e seleziona quelle giuste.", - "changeXOfY": "Modifica '{0}' di '{1}'", - "previousChange": "Modifica precedente", - "nextChange": "Modifica successiva", "startShift": "Inizio", "endShift": "Fine", "statusRetimed": "Riprogrammato", @@ -2115,12 +2127,10 @@ "statusFailed": "Mantenuto: allineatore fallito", "statusAdjustedByHand": "Regolato a mano", "alignedWaveformHint": "Trascina una riga per spostarla o trascinane i bordi per modificarne l'inizio o la fine.", - "playLine": "Riproduci riga", "undoAdjustment": "Annulla regolazione", "playPause": "Riproduci/pausa", "playAligned": "Allineato", "playAlignedHint": "Riproduci la riga selezionata con i time code allineati", - "apply": "Applica", "applyHint": "Per mantenere i time code originali deseleziona una riga. Per riprodurla fai doppio clic su una riga." }, "beautifyTimeCodesProfile": { @@ -2244,10 +2254,8 @@ "addVideoFiles": "Aggiungi file video", "skipVideoFiles": "Solo file sottotitoli", "doNotAskAgainVideoFiles": "Non chiedere più (modifica impostazioni conversione batch)", - "settingsSectionOutput": "Output", "settingsSectionAddingFiles": "Aggiunta file", "settingsSectionImageBasedSubtitles": "Sottotitoli basati su immagini (OCR)", - "settingsSectionOther": "Altro", "keepSourceFileTimestamp": "Usa data/ora file sorgente per i file destinazione", "preventSleepWhileConverting": "Impedisce al computer di andare in stop durante la conversione", "scanningFolderX": "Scansione {0}...", @@ -2272,7 +2280,6 @@ "xNotProcessed": "{0:#,###,##0} non elaborato", "removeUnicodeControlCharactersTitle": "Rimuovi caratteri di controllo Unicode", "removeUnicodeControlCharactersInfo": "Rimuove dal testo i caratteri di controllo Unicode invisibili (LRM, RLM, LRE, RLE, PDF, LRO e RLO) e sostituisce gli spazi di non interruzione con spazi normali.", - "preset": "Profilo", "savePresetDotDotDot": "Salva profilo...", "deletePreset": "Elimina profilo", "presetName": "Nome profilo", @@ -2313,7 +2320,6 @@ "loadFromCurrentText": "Carica dal testo attuale", "loadFromCurrentTranslation": "Carico dalla traduzione attuale", "loadFromFile": "Carica da file...", - "outputFormat": "Formato destinazione", "outputFormatSubRip": "SubRip (.srt)", "outputFormatAssa": "Advanced Substation Alfa (.ass)", "style1": "Stile 1", @@ -2324,7 +2330,8 @@ "title": "Dividi sottotitolo", "numberOfEqualParts": "Numero parti uguali", "saveSplitParts": "_Salva parti divise", - "xPartsSavedInFormatYToFolder": "{0} parti salvate nel formato {1} nella cartella:" + "xPartsSavedInFormatYToFolder": "{0} parti salvate nel formato {1} nella cartella:", + "splitBy": "Diviso per" }, "splitBreakLongLines": { "title": "Dividi/riequilibra righe lunghe", @@ -2381,13 +2388,9 @@ "ellipsesNotThreeDots": "Usa ellissi (non tre punti)", "onlyAllowedGlyphs": "Sono consentiti solo glifi", "italics": "Corsivo", - "maxCharsSec": "Caratteri massimi/sec", "maxDuration": "Durata massima", - "maxLineLength": "Lunghezza massima riga", "minDuration": "Durata minima", - "maxNumberOfLines": "Numero massimo di righe", "oneToTenSpellOut": "Indica da uno a dieci", - "shotChanges": "Cambi di scena", "startNumberSpellOut": "Indica il numero di partenza", "textforHiUseBrackets": "Testo per HI, usa parentesi", "twoFrameGrap": "Gap tra due fotogrammi", @@ -2436,7 +2439,6 @@ "adjustBrightnessDotDotDot": "Regola luminosità...", "adjustAlphaDotDotDot": "Regola alfa...", "cropImages": "Ritaglia immagini", - "importTimeCodes": "Importa codici temporali...", "setText": "Imposta testo", "screenWidth": "Larghezza schermo", "screenHeight": "Altezza schermo", @@ -2444,7 +2446,6 @@ "resetToDefaults": "Ripristina impostazioni predefinite", "alphaAdjustmentInfo": "Regolazione alfa: aggiungi/sottrai dal canale alfa.\r\n\r\nL'anteprima si aggiorna automaticamente e viene visualizzata con uno sfondo a scacchi per visualizzare la trasparenza.", "adjustBrightness": "Regola luminosità", - "brightness": "Luminosità", "contrast": "Contrasto", "gamma": "Gamma", "brightnessAdjustmentInfo": "Regola i cursori per modificare luminosità, contrasto e gamma.\r\n\r\nL'anteprima si aggiorna automaticamente e visualizza il primo sottotitolo selezionato.", @@ -2492,7 +2493,6 @@ "useVideoFrameRateXInsteadOfY": "La frequenza fotogrammi del video è {0}, ma i sottotitoli sono impostati su {1}.\r\nVuoi usare la frequenza fotogrammi del video ({0})?" }, "removeTextForHearingImpaired": { - "interjections": "Interiezioni", "skipIfStartWith": "Salta se inizia con", "removeTextBetween": "Rimuovi testo tra", "brackets": "Parentesi quadre", @@ -2518,8 +2518,6 @@ "pickFontNameTitle": "Scegli nome font", "pickFontNameInstalledFonts": "Font installate", "pickFontNameCollectedFonts": "Raccolta font", - "colorPickerTitle": "Scegli colore", - "colorPickerBrightness": "Luminosità", "filterLayersTitle": "Filtra livelli per la visualizzazione", "filterLayersHideFromWaveform": "Nascondi dalla forma d'onda/spettrogramma", "filterLayersHideFromSubtitleGrid": "Nascondi dalla griglia sottotitoli", @@ -2533,6 +2531,7 @@ "spellCheck": "Controllo ortografico", "getDictionariesTitle": "Controllo ortografico: download dizionari", "getDictionaryInstructions": "Scegli la lingua e seleziona 'Download'", + "uninstallDictionaryX": "Vuoi disinstallare il dizionario '{0}'?", "addNameToUserDictionary": "Aggiungi nome al dizionario utente", "addNameToNamesList": "Aggiungi il nome all'elenco nomi", "addNamesToNamesList": "Aggiungi nomi all'elenco nomi", @@ -2560,7 +2559,6 @@ "pickLiveSpellCheckDictionary": "Scegli dizionario controllo ortografico in tempo reale", "undoX": "Annulla operazione: {0}", "playCurrentLineHint": "Riproduci nel lettore video la riga attuale e alla fine metti in pausa", - "editWholeText": "Modifica intero testo", "editWholeTextTitle": "Controllo ortografico: modifica l'intero testo", "continueFromCurrentLine": "Vuoi continuare il controllo ortografico dalla riga attuale?\r\n\r\n\r\nSì = continua dalla riga attuale\r\nNo = comincia dall'inizio", "continueFromTop": "Il controllo ortografico ha raggiunto la fine del sottotitolo.\r\n\r\n\r\nVuoi continuare dall'inizio?", @@ -2595,8 +2593,6 @@ "bitRate": "Bitrate", "totalBitRateX": "Bitrate totale: {0}", "sampleRate": "Freq. campionamento", - "audio": "Audio", - "preset": "Profilo", "tune": "Sintonizza", "pixelFormat": "Formato pixel", "crf": "CRF", @@ -2611,7 +2607,10 @@ "oneBox": "Un box", "logoInfo": "Scegli un'immagine PNG e trascinala per posizionarla sul video.", "noSubtitlesGenerateAnyway": "Non ci sono righe sottotitoli da inserire.\r\n\r\nVuoi generare il video senza sottotitoli?", - "noSubtitlesInXItemsGenerateAnyway": "{0} elementi batch non hanno righe sottotitoli da inserire.\r\n\r\nVuoi generare quei video senza sottotitoli?" + "noSubtitlesInXItemsGenerateAnyway": "{0} elementi batch non hanno righe sottotitoli da inserire.\r\n\r\nVuoi generare quei video senza sottotitoli?", + "subtitleStyle": "Stile sottotitoli", + "videoOutput": "Uscita video", + "targetSize": "Dimensione obiettivo" }, "videoTransparent": { "title": "Genera video trasparenti con sottotitoli" @@ -2650,6 +2649,7 @@ "selectModel": "Seleziona modello", "addCustomModelDotDotDot": "Aggiungi modello personalizzato...", "customModelHelp": "Puoi usare un modello: scegli il file del modello (whisper.cpp ggml '.bin') o la cartella del modello (cartella con file 'model.bin' all'interno).\r\nVerrà copiato nella cartella dei modelli e aggiunto all'elenco qui sopra.", + "customModelHelpGguf": "Puoi usare il modello: scegli un file qwen3-asr.cpp '.gguf' (un file che contiene sia il codificatore audio che il modello linguistico, ad esempio da OpenVoiceOS o FlippyDora in Hugging Face).\r\nViene copiato nella cartella dei modelli e aggiunto all'elenco sopra.\r\nI file GGUF creati per llama.cpp, transcribe.cpp o CapsWriter usano un layout diverso e non possono essere selezionati.", "viewToolsLogFile": "Visualizza file registro Whisper", "updateXTitle": "Vuoi aggiornare {0}?", "updateXMessage": "È disponibile una nuova versione di {0}.{1}{1}Vuoi scaricare e installare l'aggiornamento?", @@ -2673,11 +2673,8 @@ "qualityReportLinesChecked": "Righe controllate", "qualityReportIssuesFound": "Problemi rilevati", "qualityReportLinesRemoved": "Righe rimosse", - "qualityReportTooShort": "Troppo corto", "qualityReportTooShortHint": "Tempo visualizzazione inferiore al minimo o velocità lettura superiore al massimo", - "qualityReportTooLong": "Troppo lungo", "qualityReportTooLongHint": "Tempo visualizzazione più lungo del massimo o pochissime parole per un lungo periodo (spesso un'allucinazione)", - "qualityReportOverlap": "Sovrapposizione", "qualityReportOverlapHint": "Termina dopo l'inizio della riga successiva", "qualityReportNonSpeech": "Non parlato", "qualityReportNonSpeechHint": "Solo una descrizione del suono o della musica, ad esempio '[Musica]'", @@ -2686,7 +2683,6 @@ "qualityReportRemoved": "Rimosso", "qualityReportAll": "Tutti", "qualityReportIssue": "Problema", - "qualityReportDetail": "Dettagli", "qualityReportTip": "Nel nuovo sottotitolo le righe sono elencate con i loro numeri. Usa 'Correggi errori comuni' o le impostazioni di post-elaborazione per correggerli in modo più automatico o se sono interessate molte righe prova un altro modello/motore.", "qualityReportDoNotShowAgain": "Non visualizzare più questo rapporto", "engineSettings": "Impostazioni motore sintesi vocale", @@ -2706,7 +2702,6 @@ "testVoice": "Test voce", "addAudioToVideoFile": "Aggiungi audio al file video", "engineAndVoice": "Motore e voce", - "output": "Output", "xLinesFromY": "{0} righe da {1}", "xLines": "{0} righe", "xVoices": "{0} voci", @@ -2731,11 +2726,9 @@ "instructions": "Istruzioni", "instructionsHint": "Stile conversazione opzionale, ad es. 'Parla con voce calma e calda'. Inviato solo ai modelli che lo supportano (non tts-1/tts-1-hd)", "openAiSpeedHint": "Velocità parlato da 0,25 a 4,0 (1,0 è normale). Non tutti i modelli lo supportano.", - "outputFormat": "Formato destinazione", "outputFormatHint": "Formato audio richiesto dal server.\r\n'Automatico' richiede MP3 e passa a PCM per i modelli che restituiscono solo PCM (ad esempio Gemini TTS)", "regenerateAudio": "Rigenera audio", "autoContinuePlaying": "Continua automaticamente la riproduzione", - "playLine": "Riproduci riga", "fitDurationToGeneratedAudio": "Adatta durata all'audio generato", "resetTiming": "Ripristina tempi", "addingAudioToVideoFileDotDotDot": "Aggiunta audio al file video...", @@ -2797,7 +2790,6 @@ "installToEngine": "Installa nel motore", "downloadAndInstall": "Scarica e installa", "selectAtLeastOneVoicePack": "Seleziona almeno un pacchetto voci", - "downloadingXDotDotDot": "Download di {0}...", "installingXDotDotDot": "Installazione di {0}...", "xVoicesInstalledYSkipped": "{0} voci installate ({1} già presenti)", "useSpeechToTextDotDotDot": "Usa sintesi vocale...", @@ -2806,7 +2798,6 @@ "autoCastSpeakersSubtitle": "Assegna un nome a ciascun oratore: il nome diventa l'attore nel sottotitolo e il nome della voce clonata. Per unire i due oratori in un'unica voce dai a due oratori lo stesso nome.", "autoCastFoundXSpeakersInYLines": "{0} oratori in {1} righe", "autoCastLines": "Righe", - "autoCastAudio": "Audio", "autoCastSays": "Dice", "autoCastNeedsVideo": "Per trovare le voci è necessario un video aperto da ascoltare.", "autoCastNoSpeakersFound": "Non è stato trovato alcun oratore. La trascrizione deve provenire da un motore di sintesi vocale che distingue gli oratori, come Crisp ASR MOSS Diarize.", @@ -2834,7 +2825,6 @@ "voiceCloneConsentDeclined": "La clonazione vocale non sarà disponibile finché non verranno accettati questi termini.", "advancedTtsSettings": "Impostazioni TTS avanzate", "advancedTtsAudioProcessing": "Elaborazione audio", - "advancedTtsOutput": "Output", "advancedTtsVoiceCloning": "Clonazione vocale", "cleanCloneReferences": "Riferimenti di cloni vocali puliti (Sidon)", "cleanCloneReferencesDescription": "Rimuove musica, rumore e riverbero dall'audio da cui viene clonata una voce: clip prese dal video e registrazioni importate nel gestore vocale. Usa il modello di ripristino del parlato Sidon su audio.cpp (download circa 940 MB). La voce clonata non copia più lo sfondo, ma i discorsi molto bassi o sovrapposti possono risultare ovattati.", @@ -2853,7 +2843,6 @@ "removeOriginalSpeechDescription": "Mantieni la musica e gli effetti sonori del video originale ma rimuove il parlato e vi aggiunge il nuovo parlato. Impiega con una GPU circa lo stesso tempo del video stesso e molte volte di più senza GPU. Con l'audio ducking attivo, il suo volume si applica alla musica e agli effetti. Si applica solo quando il parlato viene aggiunto al file video.", "removingOriginalSpeech": "Rimozione parlato originale...", "removeOriginalSpeechFailed": "Impossibile rimuovere il parlato originale: il parlato è stato aggiunto al video senza di esso.", - "speechRemovalModel": "Modello", "speechRemovalModelDescription": "'Migliore qualità' mantiene la maggior parte della musica e degli effetti sonori. 'Veloce' è circa tre volte più veloce, con un po' più di musica persa.", "speechRemovalBestQuality": "Migliore qualità (Mel-Band RoFormer)", "speechRemovalFast": "Veloce (HTDemucs)", @@ -2941,13 +2930,10 @@ "shotChangesList": "Elenco cambi inquadrature", "shotChangesClearQuestion": "Vuoi eliminare tutte i cambi inquadrature?", "deleteSelectedShotChangeQuestion": "Vuoi eliminare il cambio inquadratura selezionato?", - "shotChanges": "Cambiamenti inquadratura", "exportShotChanges": "Esporta modifiche inquadratura" }, "chapters": { - "title": "Capitoli", "chaptersDotDotDot": "Capitoli...", - "chapters": "Capitoli", "selectedChapter": "Capitolo selezionato", "chapterTitle": "Titolo capitolo", "startTime": "Punto iniziale", @@ -2968,7 +2954,6 @@ "adjustTimes": "Regola i tempi", "shiftAllTimes": "Sposta tutti i tempi", "shiftAllTimesDescription": "Sposta ogni capitolo avanti o indietro della stessa quantità.", - "scaleTimes": "Modifica frequenza fotogrammi", "scaleTimesDescription": "Scala la durata di ogni capitolo da una frequenza fotogrammi all'altra.", "fromFrameRate": "Da", "toFrameRate": "A", @@ -3002,13 +2987,10 @@ "startOcr": "Avvia OCR", "extractingFramesX": "Estrazione fotogrammi immagine... {0}%", "analyzingFramesXY": "Analisi fotogrammi... {0}/{1}", - "runningOcrXY": "Esecuzione OCR... {0}/{1}", - "noLinesFoundTitle": "Nessun sottotitolo trovato", "noLinesFoundMessage": "Nell'area di scansione non è stato trovato testo: prova a regolare l'area di scansione, l'engine o la luminosità minima.", "ocrStoppedEarlyMessage": "Il motore OCR si è fermato prima che tutti i fotogrammi fossero letti, quindi le righe trovate fino a quel momento vengono mantenute e il resto risulta mancante.{0}{0}{1}", "linesFoundX": "trovate {0} righe", "refiningTimingXY": "Rifinitura tempi... {0}/{1}", - "fixOcrErrors": "Correggi errori OCR", "editLineX": "Modifica riga {0}", "downloadDictionaryHint": "Scarica un dizionario per il controllo ortografico...", "framesPerSecondHint": "Quanti fotogrammi al secondo vengono scansionati. 5 è l'ideale per i sottotitoli normali; un livello più alto cattura righe molto brevi e restringe il raggruppamento, ma richiede più tempo per l'OCR.", @@ -3037,8 +3019,6 @@ "backgroundMusic": { "title": "Genera musica sottofondo", "generateBackgroundMusicDotDotDot": "Genera musica sottofondo...", - "preset": "Preimpostazione", - "prompt": "Prompt", "tempoBpm": "Tempo (BPM)", "generateSeconds": "Genera (secondi)", "seed": "Seed", @@ -3075,6 +3055,7 @@ "outputLengthTextToSpeech": "Segui il parlato generato: la musica si ripete ininterrottamente e si abbassa mentre qualcuno parla", "addBackgroundMusic": "Aggiungi musica sottofondo", "addBackgroundMusicHint": "Genera musica localmente con ACE-Step 1.5 e mixala con il parlato: la musica si abbassa mentre qualcuno parla", + "music": "Musica", "generatingBackgroundMusicDotDotDot": "Generazione musica sottofondo...", "mixingBackgroundMusicDotDotDot": "Mix musica sottofondo..." }, @@ -3128,7 +3109,47 @@ "remuxVideoVolumePercent": "% volume:", "remuxVideoVolumeX": "Volume {0}%", "remuxVideoFastStart": "Ottimizza per lo streaming web (sposta l'indice in primo piano - più lento)", + "remuxVideoSubtitlesAs": "Sottotitoli come:", + "remuxVideoSubtitlesAsTracks": "Tracce sottotitoli (mov_text)", + "remuxVideoSubtitlesAsCaptions": "CEA-608 nel video (SEI)", + "remuxVideoMp4CaptionsHint": "I sottotitoli vengono scritti nel video H.264/H.265 come dati utente ATSC A/53 SEI: il primo file sottotitoli è CC1, il secondo CC3.\r\nVideo e audio vengono copiati, non ricodificati.", + "remuxVideoMp4MaxTwoCaptionTracks": "I sottotitoli nel video contengono al massimo due tracce (CC1 e CC3): rimuovi i file dei sottotitoli in modo che ne rimangano uno o due.", + "remuxVideoMp4CaptionsNeedH264X": "I sottotitoli in un file .mp4 richiedono video H.264 o H.265 e il video è '{0}'.\r\nScegli invece le tracce sottotitoli o l'output .mpg.", + "remuxVideoCaptionOffset": "Offset didascalia:", + "remuxVideoCaptionOffsetHint": "Tolti i tempi delle didascalie.\r\nI file broadcast .scc/.mcc spesso iniziano a 01:00:00:00 mentre il video inizia da zero: l'offset viene suggerito quando la prima didascalia si trova oltre la fine del video.", "embedSubtitlesDotDotDot": "Aggiungi/rimuovi sottotitoli incorporati...", + "syncAudioTrackDotDotDot": "Sincronizza traccia audio...", + "syncAudioTrackTitle": "Sincronizza la traccia audio", + "syncAudioTrackAudioTrack": "Traccia audio", + "syncAudioTrackOffset": "Scostamento (ms)", + "syncAudioTrackHint": "I valori positivi riproducono l'audio più tardi, i valori negativi prima.\r\nIl video salvato è una copia con spostata solo questa traccia: nulla viene ricodificato.", + "syncAudioTrackPreviewNeedsMpv": "L'anteprima dal vivo dell'offset necessita del lettore video mpv (Opzioni > Impostazioni > Video): il video salvato ottiene comunque l'offset.", + "syncAudioTrackNoAudio": "Il video non ha traccia audio.", + "syncAudioTrackSaveVideoDotDotDot": "Salva video...", + "syncAudioTrackSaving": "Salvataggio video...", + "syncToAudioDotDotDot": "Sincronizza sottotitoli con l'audio...", + "syncToAudioTitle": "Sincronizza sottotitoli con l'audio", + "syncToAudioReference": "Sincronizza contro", + "syncToAudioReferenceAudio": "Discorso nell'audio del video (dalla forma d'onda)", + "syncToAudioReferenceSubtitle": "Un altro file sottotitoli già sincronizzato", + "syncToAudioPickReferenceSubtitle": "Scegli il file sottotitoli con cui sincronizzare", + "syncToAudioNoReferenceSubtitle": "Nessun file scelto", + "syncToAudioMaxOffsetSeconds": "Offset massimo (secondi)", + "syncToAudioTryFrameRates": "Prova anche le conversioni delal frequenza fotogrammi (23.976, 24, 25, 29.97, 30)", + "syncToAudioResult": "Risultato", + "syncToAudioAnalyze": "Analizza", + "syncToAudioAnalyzing": "Analizzi...", + "syncToAudioResultOffsetX": "Scostamento: {0}", + "syncToAudioResultFrameRateXToY": "Frequenza fotogrammi: {0} → {1}", + "syncToAudioResultFrameRateUnchanged": "Frequenza fotogrammi: invariata", + "syncToAudioResultConfidenceX": "Accuratezza: {0}%", + "syncToAudioLowConfidence": "Scarsa accuratezza: il sottotitolo potrebbe appartenere a un altro taglio del video oppure l'audio potrebbe contenere principalmente musica.\r\nPrima di salvare controlla alcune righe.", + "syncToAudioNoWaveform": "L'audio può essere usato solo una volta generata la forma d'onda: sincronizzalo con un altro file sottotitoli o genera prima la forma d'onda.", + "syncToAudioNoLines": "Il sottotitolo non ha righe.", + "syncToAudioNoSpeechFound": "Nell'audio non è stato possibile distinguere alcun discorso dal silenzio.", + "syncToAudioReferenceSubtitleEmpty": "Il sottotitolo di riferimento non ha righe.", + "syncToAudioHint": "Trova un offset per l'intero sottotitolo (e una mancata corrispondenza della frequenza fotogrammi, se presente) confrontando il momento in cui viene visualizzato il testo con quando qualcuno parla.\r\nUn sottotitolo che non è sincronizzato in modo diverso nelle diverse parti necessita invece di 'Sincronizzazione accurata' o 'Migliora timecode'.", + "syncToAudioAppliedX": "Sincronizza sottotitoli con l'audio: {0}", "generateTimeCodes": "Genera time code", "checkeredImage": "Immagine a scacchi", "promptForFfmpegParamsAndGenerate": "Richiedi parametri ffmpeg e genera", @@ -3165,11 +3186,13 @@ "cutVideoTransitionRadial": "Radiale", "cutVideoTransitionPixelize": "Pixelizza", "cutVideoTransitionBlur": "Sfoca", + "cutVideoSegments": "Segmenti", + "cutVideoTransitions": "Transizioni", + "cutVideoCutType": "Tipo di taglio", "importCurrentSubtitle": "Importa sottotitolo attuale", "addRemoveEmbeddedSubtitlesTitle": "Aggiungi/rimuovi sottotitoli incorporati", "addRemoveEmbeddedSubtitlesMp4Title": "Aggiungi/rimuovi sottotitoli incorporati (MP4)", "addCurrentSubtitle": "Aggiungi sottotitolo attuale", - "embeddedTrackNoSubtitlesFoundTitle": "Nessun sottotitolo trovato", "embeddedTrackNoSubtitlesFoundMessage": "Il file sottotitoli selezionato non contiene sottotitoli.", "embeddedTrackNoTracksTitle": "Nessuna traccia aggiunta", "embeddedTrackNoTracksMessage": "Aggiungi una o più tracce sottotitoli o carica un video che ha già i sottotitoli incorporati.", @@ -3181,6 +3204,7 @@ "embeddedTrackGeneratingVideo": "Generazione video...", "mp4FilesFilter": "File MP4", "embeddedTracksPickVideoHint": "Per vedere le tracce sottotitoli scegli un file video o rilasciane uno qui.", + "embeddedTracks": "Tracce", "viewMatroskaTrackX": "Visualizza traccia Matroska - {0}", "resolutionSeparator": "x", "openFromUrlTitle": "Apri file video dall'URL", @@ -3194,6 +3218,10 @@ "openFromUrlDownloadSubtitles": "Scarica anche i sottotitoli", "openFromUrlDownloadVideo": "Download video", "openFromUrlIncludeAutoGeneratedSubtitles": "Includi sottotitoli generati automaticamente (lingua originale)", + "openFromUrlCookiesFromBrowser": "Cookie dal browser", + "openFromUrlCookiesFromBrowserHint": "Invia i cookie del browser da cui hai effettuato l'accesso a YouTube, necessari quando YouTube chiede di confermare che non sei un bot.", + "openFromUrlBotCheckHint": "YouTube vuole uan conferma che non sei un bot.\r\nAccedi a YouTube nel browser, quindi seleziona il browser in questione in 'Cookie dal browser' nella finestra 'Apri file video da URL' e riprova (Firefox funziona meglio).\r\nPuò essere d’aiuto disattivare la VPN.", + "openFromUrlBotCheckHintWithBrowserX": "YouTube vuole ancora una conferma che non sei un bot.\r\nAssicurati di aver effettuato l'accesso a YouTube in '{0}'.\r\nChrome e Edge su Windows spesso bloccano la lettura dei cookie: prima chiudi il browser o prova Firefox.", "assaStyleWillBeUsed": "Verrà usato l'attuale stile ASSA\n\nSe vuoi impostare gli stili qui\ncambia il formato dei sottotitoli", "pickOnlineSubtitleTitle": "Scegli i sottotitoli da scaricare", "outputSettingsTitle": "Impostazioni output", @@ -3327,7 +3355,6 @@ "blockCopyGetFromClipboard": "Ottieni testo dagli appunti (dal traduttore/IA)", "reCopyTextToClipboard": "Ricopia il testo negli appunti (per traduttore/IA)", "blockXOfY": "Blocco {0} / {1}", - "noTextInClipboard": "Nessun testo negli appunti", "textInClipboardIsSameAsSourceText": "Il testo negli appunti è lo stesso del testo sorgente, riprova.", "lineMerge": "Unione riga", "delayInSecondsBetweenRequests": "Ritardo tra le richieste (secondi)", @@ -3361,7 +3388,6 @@ "keepLineBreaks": "Mantieni interruzioni di riga", "samplingParameters": "Parametri campionamento", "minusOneMeansDefault": "-1 = modello/server predefinito", - "temperature": "Temperatura", "topP": "Top-p", "topK": "Top-k", "repeatPenalty": "Penalità ripetizione", @@ -3375,7 +3401,6 @@ "customPromptHint": "Istruzioni personalizzate ({0} = lingua di origine, {1} = lingua destinazione); campo vuoto = prompt integrato", "editTranslationHint": "Fai clic sulla traduzione della riga selezionata per modificarla (Ins=salva, Esc=annulla)", "llamaCppDownloadEngineAndModelPrompt": "Per usare llama.cpp è necessario scaricare il motore llama-server e un modello traduzione. Vuoi scaricarli?", - "llamaCppDownloadEnginePrompt": "Per usare llama.cpp è necessario scaricare il motore llama-server. Vuoi scaricarlo?", "llamaCppDownloadModelPrompt": "Per usare llama.cpp è necessario scaricare il modello traduzione selezionato. Vuoi scaricarlo?" }, "options": { @@ -3397,6 +3422,7 @@ "continuationStyleLeadingTrailingDash": "Trattino", "continuationStyleLeadingTrailingDashDots": "Trattino, ma punti per le pause", "cpsLineLengthStyle": "Cps/lunghezza riga", + "cpsLineLengthIgnoreTagsRegex": "Ignora tag personalizzati (regex)", "cpsLineLengthStyleCalcAll": "Conta tutti i caratteri", "cpsLineLengthStyleCalcNoSpaceCpsOnly": "Conta tutto tranne gli spazi, solo CPS", "cpsLineLengthStyleCalcNoSpace": "Conta tutto tranne gli spazi", @@ -3437,7 +3463,6 @@ "gridAlternatingRowColor": "Colore riga alternato (tema chiaro)", "gridAlternatingRowColorDark": "Colore riga alternato (tema scuro)", "uiFont": "Font interfaccia utente", - "theme": "Tema", "iconTheme": "Tema icona", "darkThemeForegroundColor": "Colore di primo piano tema scuro", "darkThemeBackgroundColor": "Colore di sfondo tema scuro", @@ -3447,7 +3472,6 @@ "spellCheckHighlightColor": "Colore evidenziazione controllo ortografico", "singleLineMaxLength": "Lunghezza massima riga singola", "optimalCharsPerSec": "Caratteri/sec ottimali", - "maxCharsPerSec": "Caratteri/sec massimi", "maxWordsPerMin": "Numero massimo parole/min", "minDurationMs": "Durata minima (ms)", "maxDurationMs": "Durata massima (ms)", @@ -3458,7 +3482,6 @@ "minGapCalculateFrames": "Divario minimo fotogrammi", "minGapCalculateXFramesAtYGivesZMs": "{0} fotogrammi ad una frequenza fotogrammi di {1} danno {2} millisecondi.", "minGapCalculateUseXAsNewGap": "Vuoi usare '{0}' millisecondi come nuovo intervallo minimo?", - "maxLines": "Numero massimo di righe", "unbreakSubtitlesShortThan": "Non interrompere sottotitoli più brevi di", "autoBreakLineEndingEarly": "Interruzione automatica anticipata per la fine della frase (.!?)", "autoBreakCommaBreakEarly": "Interruzione automatica anticipata per la virgola", @@ -3499,6 +3522,15 @@ "colorOverlap": "Colore sovrapposizione codice temporale", "colorGapTooShort": "Colora se il gap è troppo breve", "errorBackgroundColor": "Colore sfondo errore", + "customErrorRules": "Regole errore personalizzate", + "customErrorRulesHighlight": "Righe colore che corrispondono a una regola di errore personalizzata", + "customErrorRulesHint": "Una riga il cui testo corrisponde all'espressione regolare di una regola abilitata viene colorata con il colore della regola e conta come un errore (Elenca errori, Vai all'errore successivo).\r\nLa prima regola di corrispondenza decide il colore.", + "customErrorRuleSelected": "Regola selezionata", + "customErrorRuleTest": "Test", + "customErrorRuleTestMatch": "La regola selezionata corrisponde al testo di esempio", + "customErrorRuleTestNoMatch": "La regola selezionata non corrisponde al testo di esempio", + "customErrorRulesOn": "Regole di errore personalizzate: attivate", + "customErrorRulesOff": "Regole di errore personalizzate: disattivate", "waveformDrawGridLines": "Disegna righe griglia", "waveformUseSkiaRenderer": "Usa renderer veloce sperimentale", "waveformCenterVideoPositionAlsoWhenPaused": "Centra posizione video anche durante la pausa", @@ -3556,6 +3588,8 @@ "videoControlsPositionText": "Posizione/durata testo", "videoControlsVideoFileName": "Nome file video", "videoControlsPlayerName": "Nome lettore video", + "videoControlsPreviousSubtitle": "Sottotitolo precedente", + "videoControlsNextSubtitle": "Sottotitolo successivo", "fullscreenHideControls": "Nascondi controlli video a schermo intero", "showSecondarySubtitleDialog": "Quando si apre un secondo file sottotitoli visualizza la finestra di dialogo stile", "rememberSecondarySubtitleFile": "Ricorda secondo file sottotitoli", @@ -3612,6 +3646,7 @@ "timeCodeModeFrameNumbers": "Numeri fotogrammi", "frameModeSnapToVideoFrames": "Memorizza i tempi sui fotogrammi del video (non sui fotogrammi di ogni secondo)", "textBoxLimitNewLines": "Limita il numero di righe nella casella testo sottotitoli", + "textBoxAutoWrapWhileTyping": "A capo automatico durante la digitazione", "mpvOpenGl": "libmpv - OpenGL", "mpvSoftwareRendering": "libmpv - rendering software (lento)", "mpvMetalRendering": "libmpv - Metal (sperimentale, necessita di riavvio)", @@ -3636,6 +3671,10 @@ "subtitleDoubleClickAction": "Griglia sottotitoli - azione doppio clic", "subtitleGridCenterSelectedRow": "Griglia sottotitoli, centra quando si seleziona la riga precedente/successiva", "saveAsBehavior": "Comportamento 'Salva con nome'", + "fileChangedOutsideAction": "File sottotitoli modificato da un altro programma", + "fileChangedOutsideReloadWhenUnmodified": "Ricarica se non è stato salvato nulla, altrimenti chiedi", + "fileChangedOutsideAlwaysAsk": "Chiedi sempre", + "fileChangedOutsideNever": "Non fare nulla", "defaultSaveLocation": "Percorso salvataggio predefinito", "defaultSaveLocationSourceFileFolder": "Cartella sottotitoli/file video", "defaultSaveLocationLastUsedFolder": "Ultima cartella usata", @@ -3696,6 +3735,7 @@ "textBoxButtonShowColor": "Casella testo: visualizza pulsante 'Colore'", "textBoxButtonShowRemoveFormatting": "Casella testo: visualizza pulsante 'Rimuovi formattazione'", "textBoxButtonShowAiAssistant": "Casella di testo: visualizza il pulsante 'Assistente IA'", + "textBoxButtonShowRecordSpeech": "Casella di testo: visualizza il pulsante registrazione vocale (dal microfono al testo)", "waveformSingleClickAction": "Azione clic singolo sulla forma d'onda", "waveformDoubleClickAction": "Azione doppio clic sulla forma d'onda (dopo l'azione con clic singolo)", "allSettings": "Tutte le impostazioni", @@ -3760,7 +3800,6 @@ "customShortcutPickCommand": "Seleziona un comando da eseguire.", "customShortcutEnterText": "Digita il testo da inserire.", "customShortcutEnterFind": "Inserisci il testo da cercare.", - "customShortcutInvalidRegexX": "Espressione regolare non valida: {0}", "customShortcutStepsHint": "I passaggi vanno dall'alto verso il basso.\r\nI passaggi di testo funzionano nelle righe selezionate;\r\ni comandi che aprono una finestra continuano dopo la sua chiusura.", "activeIn": "Attivo", "activeInEverywhere": "Ovunque", @@ -3782,6 +3821,7 @@ "alsoSetVideoPosition": "Imposta anche posizione video", "generalGoToVideoPosition": "Vai alla posizione video", "generalToggleItalic": "Attiva/disattiva corsivo", + "generalRecordSpeechToTextBox": "Registra la voce nella casella di testo (avvio/arresto)", "generalToggleBold": "Attiva/disattiva grassetto", "generalToggleUnderline": "Attiva/disattiva sottolineato", "generalToggleBox": "Riquadro ON/OFF (EBU STL)", @@ -3845,6 +3885,12 @@ "moveTextFromCursorToNextAndGoToNext": "Sposta il testo dopo la posizione del cursore al sottotitolo successivo e vai al successivo", "moveTextFromCursorToNextAndGoToNextAndPlay": "Sposta il testo dopo la posizione del cursore al sottotitolo successivo, vai al successivo e riproduci", "moveTextFromCursorToNextAndGoToNextAndPlayAndPause": "Sposta il testo dopo la posizione del cursore al sottotitolo successivo, vai al successivo, riproducilo e alla fine metti in pausa", + "moveTextBeforeCursorToPrevious": "Sposta il testo prima della posizione del cursore al sottotitolo precedente", + "showCustomErrorRules": "Visualizza regole di errore personalizzate", + "toggleCustomErrorRules": "Attiva/disattiva colorazione regole di errore personalizzate", + "splitAtVideoPositionAndPlay": "Dividi la riga nella posizione del video, vai alla seconda parte e riproduci", + "splitAtTextBoxCursorPositionAndPlay": "Dividi la riga nella posizione del cursore della casella di testo, vai alla seconda parte e riproduci", + "splitAtVideoAndTextBoxPositionAndPlay": "Dividi la riga nella posizione del video e della casella di testo, vai alla seconda parte e riproduci", "breakAtFirstSpaceFromCursor": "Interrompi al primo spazio dalla posizione del cursore", "breakAtFirstSpaceFromCursorAndGoToNext": "Interrompi al primo spazio dalla posizione del cursore e passa al successivo", "toggleFocusGridAndWaveform": "Alterna evidenziazione griglia sottotitoli e forma d'onda/spettrogramma", @@ -4013,6 +4059,8 @@ "duplicatesFound": "Sono state trovate i seguenti tasti rapidi duplicati:", "copyTextToClipboard": "Copia testo negli appunti (righe selezionate)", "copyTextFromOriginalToClipboard": "Copia il testo dall'originale negli appunti (righe selezionate)", + "copyPlainTextToClipboard": "Copia testo normale negli appunti (righe selezionate)", + "copyPlainTextFromOriginalToClipboard": "Copia testo semplice dall'originale negli appunti (righe selezionate)", "assaDraw": "Disegno ASSA", "assaGenerateProgressBar": "Genera barra avanzamento ASSA", "assaGenerateBackgroundBox": "Genera riquadro di sfondo ASSA", @@ -4056,6 +4104,12 @@ "moveSelectedLinesAndForwardXMsForward": "Sposta le righe selezionate e tutte le successive avanti di X ms (X definito in Impostazioni)", "moveAllLinesXMsBack": "Sposta tutte le righe indietro di X ms (X impostato in Impostazioni)", "moveAllLinesXMsForward": "Sposta tutte le righe avanti di X ms (X impostato in Impostazioni)", + "moveSelectedLinesOneFrameBack": "Sposta le righe selezionate indietro di un fotogramma", + "moveSelectedLinesOneFrameForward": "Sposta le righe selezionate in avanti di un fotogramma", + "moveSelectedLinesAndForwardOneFrameBack": "Sposta le righe selezionate e tutte quelle successive indietro di un fotogramma", + "moveSelectedLinesAndForwardOneFrameForward": "Sposta le righe selezionate e tutte quelle successive in avanti di un fotogramma", + "moveAllLinesOneFrameBack": "Sposta tutte le righe indietro di un fotogramma", + "moveAllLinesOneFrameForward": "Sposta tutte le righe in avanti di un fotogramma", "moveSelectedLinesCustomMsBack": "Sposta le righe selezionate indietro, millisecondi personalizzati ({0:#,###,##0}), {1}", "moveSelectedLinesCustomMsForward": "Sposta le righe selezionate, millisecondi personalizzati ({0:#,###,##0}) avanti, {1}", "moveSelectedLinesAndForwardCustomMsBack": "Sposta le righe selezionate e tutte le successive, millisecondi personalizzati ({0:#,###,##0}) indietro, {1}", @@ -4110,7 +4164,13 @@ "doNotAskAgain": "Non chiedere più (modifica in Gestisci plugin)", "applyPluginsTo": "Applica plugin a", "askEachTime": "Chiedi ogni volta", - "allLines": "Tutte le righe" + "allLines": "Tutte le righe", + "runPlugin": "Esegui plug-in", + "plugin": "Plugin", + "configurePlugin": "Configura...", + "noBatchPluginsInstalled": "Nessun plugin installato supporta la conversione batch.", + "batchPluginInfo": "Il plugin viene eseguito per ciascun file senza visualizzare la sua finestra, usando le impostazioni salvate.", + "pluginXNotAvailable": "Il plugin '{0}' non è installato, è disabilitato o non supporta la conversione batch." }, "help": { "aboutSubtitleEdit": "Info su Subtitile Edit", @@ -4119,7 +4179,10 @@ "checkForUpdatesUpToDate": "Questa versione è aggiornata.", "checkForUpdatesNewVersionAvailable": "Disponibile nuova versione: {0}", "checkForUpdatesUnableToCheck": "Impossibile controllare gli aggiornamenti.", - "checkForUpdatesDownloadNewVersion": "Download nuova versione" + "checkForUpdatesDownloadNewVersion": "Download nuova versione", + "checkForUpdatesInstallAndRestart": "Installa e riavvia", + "checkForUpdatesInstallFailed": "Aggiornamento automatico non riuscito: usa invece \"Download nuova versione\".", + "checkForUpdatesInstallsWhenClosed": "L'aggiornamento verrà installato alla chiusura di Subtitle Edit." }, "ocr": { "linesToDraw": "Righe da disegnare", @@ -4140,7 +4203,6 @@ "numberOfPixelsIsSpace": "Il numero di pixel è lo spazio", "inspectImageMatches": "Controlla corrispondenze immagini", "resolutionXYAndTopmarginZ": "Risoluzione {0}x{1}, margine superiore {2}", - "runningOcrDotDotDotXY": "Esecuzione OCR... {0}/{1}", "runningOcrDotDotDot": "Esecuzione OCR...", "autoSubmitFirstCharacter": "Invio automatico del primo carattere", "editNOcrDatabase": "Modifica database nOCR", @@ -4156,12 +4218,10 @@ "addNewCharcter": "Aggiungi nuovo carattere", "addNOcrCharacterTitle": "Aggiungi carattere nOCR per la riga {0}, carattere {1} di {2} usando il database '{3}'", "addBinaryOcrCharacterTitle": "Aggiungi carattere OCR binario per la riga {0}, carattere {1} di {2} usando il database '{3}'", - "lineIndexX": "Riga {0}", "inspectNOcrAdditions": "Controlla nuove aggiunte nOCR", "ocrSelectedLines": "Righe selezionate OCR", "fillSelectedLinesWithClipboard": "Riempi righe selezionate con testo degli appunti", "showImage": "Visualizza immagine", - "fixOcrErrors": "Correggi errori OCR", "promptForUknownWords": "Richiedi conferma parole sconosciute", "tryToGuessUnknownWords": "Prova a indovinare parole sconosciute", "autoBreakIfMoreThanXLines": "Interruzione automatica se più di {0} righe", @@ -4180,7 +4240,6 @@ "clearBackground": "Sfondo chiaro", "clearForeground": "Primo piano chiaro", "nOcrDrawHelp": "Suggerimenti per il disegno\r\n────────────────────\r\n• Per continuare la riga tieni premuto 'Ctrl'\r\n• Ctrl+z=annulla, Ctrl+y=ripeti", - "editWholeText": "Modifica intero testo", "editWordOnly": "Modifica solo la parola", "imagePreProcessing": "Pre-elaborazione immagini...", "preProcessingTitle": "Pre-elaborazione", @@ -4225,7 +4284,6 @@ "htmlExportNoMatches": "Nessuna riga corrisponde al filtro", "htmlExportImageBackground": "Sfondo immagine", "htmlExportCheckerboard": "Scacchiera", - "htmlExportTheme": "Tema", "htmlExportGeneratedBySubtitleEdit": "Generato da Subtitle Edit", "vobSubColors": "Colori VobSub/DVD...", "vobSubColorsTitle": "Colori VobSub/DVD", @@ -4256,7 +4314,6 @@ "llamaCppNotDownloaded": "Motore/modello llama.cpp non scaricato: scarica tramite impostazioni conversione batch", "llamaCppReturnedNoText": "llama.cpp non ha restituito testo: controlla il server e il modello", "llamaCppDownloadEngineAndModelPrompt": "llama.cpp richiede che vengano scaricati il motore llama-server e il modello OCR selezionato. Vuoi scaricarli?", - "llamaCppDownloadEnginePrompt": "llama.cpp richiede che venga scaricato il motore llama-server. Vuoi scaricarlo?", "llamaCppDownloadModelPrompt": "llama.cpp richiede che venga scaricato il modello OCR selezionato. Vuoi scaricarlo?", "crispEmbedNotDownloaded": "Motore/modello CrispEmbed non scaricato: scaricalo tramite le impostazioni di conversione batch.", "crispEmbedReturnedNoText": "CrispEmbed non ha restituito alcun testo: verifica il modello", @@ -4292,7 +4349,11 @@ "alsoTrainItalic": "Addestra anche con il corsivo", "trainingXLearnedYSkipped": "Addestramento '{0}' - {1:#,##0} caratteri appresi, {2:#,##0} saltati", "trainingDoneXLearned": "Addestramento completato: {0:#,##0} caratteri appresi", - "selectAtLeastOneFont": "Seleziona almeno un carattere" + "selectAtLeastOneFont": "Seleziona almeno un carattere", + "nOcrZoomIn": "Zoom -", + "nOcrZoomOut": "Zoom +", + "nOcrLineAlgorithm": "Algoritmo", + "nOcrLineAlgorithmHint": "Algoritmo usato da disegno automatico per generare righe di primo piano/sfondo" }, "assa": { "assaDraw": "Disegno ASSA", @@ -4333,8 +4394,6 @@ "drawImportSvg": "Importa immagine SVG...", "drawRotateClockwise": "Ruota di 90° in senso orario", "drawConvertToCurve": "Converti segmento in curva", - "drawBackground": "Sfondo", - "drawPosition": "Posizione", "drawZoom": "Zoom", "drawShapeTool": "Strumento forma", "drawColorPickerTool": "Scegli il colore da una forma o dallo sfondo", @@ -4347,6 +4406,112 @@ "drawCategoryArrows": "Frecce", "drawCategoryBasicShapes": "Forme basilari", "drawCategorySymbols": "Simboli", + "drawCategoryClipArt": "ClipArt", + "drawClipArtRedHeart": "Cuore rosso", + "drawClipArtAngerMark": "Marchio rabbia", + "drawClipArtSweatDrop": "Goccia di sudore", + "drawClipArtSparkles": "Scintille", + "drawClipArtBlush": "Arrossamento", + "drawClipArtSurpriseLines": "Righe a sorpresa", + "drawClipArtBamBurst": "Bam! Scoppia", + "drawClipArtZzz": "Zzz", + "drawClipArtSun": "Sole", + "drawClipArtFlame": "Fiamma", + "drawClipArtLightBulb": "Lampadina", + "drawClipArtLightningBolt": "Fulmine", + "drawClipArtColoredMusicNotes": "Note musicali colorate", + "drawClipArtPhone": "Telefono", + "drawClipArtQuestionMark": "Punto interrogativo", + "drawClipArtExclamationMark": "Punto esclamativo", + "drawClipArtGreenCheck": "Segno di spunta verde", + "drawClipArtRedCross": "Croce rossa", + "drawClipArtThumbsUp": "Pollice su", + "drawClipArtGoldStar": "Stella d'oro", + "drawClipArtBrokenHeart": "Cuore spezzato", + "drawClipArtGhost": "Fantasma", + "drawClipArtSkull": "Teschio", + "drawClipArtSnowflake": "Fiocco di neve", + "drawClipArtMoonAndStars": "Luna e stelle", + "drawClipArtRainCloud": "Nuvola di pioggia", + "drawClipArtBomb": "Bomba", + "drawClipArtGift": "Regalo", + "drawClipArtCrown": "Corona", + "drawClipArtClock": "Orologio", + "drawClipArtClapperboard": "Ciak", + "drawClipArtMicrophone": "Microfono", + "drawClipArtWarningSign": "Segnale di avvertimento", + "drawClipArtNoEntry": "Vietato l'accesso", + "drawClipArtPopcorn": "Popcorn", + "drawClipArtCoffeeCup": "Tazza di caffè", + "drawClipArtRainbow": "Arcobaleno", + "drawClipArtTrophy": "Trofeo", + "drawClipArtRocket": "Razzo", + "drawClipArtBalloon": "Palloncino", + "drawClipArtCamera": "Telecamera", + "drawClipArtPartyPopper": "Festa scoppiettante", + "drawClipArtPizzaSlice": "Trancio di pizza", + "drawClipArtIceCream": "Gelato", + "drawClipArtUmbrella": "Ombrello", + "drawClipArtCatFace": "Faccia da gatto", + "drawClipArtKey": "Chiave", + "drawClipArtChristmasTree": "Albero di Natale", + "drawClipArtYouTube": "YouTube", + "drawClipArtInstagram": "Instagram", + "drawClipArtLinkedIn": "LinkedIn", + "drawClipArtFacebook": "Facebook", + "drawClipArtX": "X", + "drawClipArtTikTok": "TikTok", + "drawClipArtClosedCaptions": "Sottotitoli chiusi", + "drawClipArtSpeakerLabel": "Altoparlante", + "drawClipArtTranslation": "Traduzione", + "drawClipArtSignLanguage": "Linguaggio dei segni", + "drawClipArtAi": "IA", + "drawClipArtHeadphones": "Cuffie", + "drawClipArtYummy": "Delizioso", + "drawClipArtDrooling": "Sbavando", + "drawClipArtDisgusted": "Disgustato", + "drawClipArtTooSpicy": "Troppo piccante", + "drawClipArtChefsKiss": "Il bacio dello chef", + "drawClipArtLoveIt": "Lo adoro", + "drawClipArtStarRating": "{0} su 5 stelle", + "drawAnimation": "Animazione", + "drawAnimationIn": "Ingresso", + "drawAnimationOut": "Uscita", + "drawAnimationFade": "Dissolvenza", + "drawAnimationPop": "Pop", + "drawAnimationZoom": "Zoom", + "drawAnimationSpin": "Rotazione", + "drawAnimationFromLeft": "Da sinistra", + "drawAnimationFromRight": "Da destra", + "drawAnimationFromTop": "Dall'alto", + "drawAnimationFromBottom": "Dal basso", + "drawAnimationToLeft": "A sinistra", + "drawAnimationToRight": "A destra", + "drawAnimationToTop": "In alto", + "drawAnimationToBottom": "In basso", + "drawAnimationPlay": "Riproduci animazione", + "drawAnimationStop": "Stop animazione", + "drawAnimationLayerHint": "Si applica all'intero livello (una riga di sottotitolo); un gruppo si muove all'unisono.", + "drawAnimationLoop": "Ciclo continuo", + "drawAnimationPulse": "Impulso", + "drawAnimationBlink": "Lampeggio", + "drawAnimationBounce": "Rimbalzo", + "drawAnimationShake": "Scuoti", + "drawAnimationSwing": "Oscilla", + "drawAnimationFlip": "Capovolgi", + "drawAnimationLoopBlocked": "Questo ciclo continuo gira attorno a un altro punto rispetto all'effetto in/out, quindi viene escluso.", + "drawClipArtLoadingSpinner": "Caricamento spinner", + "drawClipArtRec": "REG", + "drawClipArtRingingPhone": "Telefono che squilla", + "drawClipArtBeatingHeart": "Cuore pulsante", + "drawClipArtBouncingArrow": "Freccia che rimbalza", + "drawClipArtRingingBell": "Suona il campanello", + "drawClipArtPointingHand": "Mano che indica", + "drawClipArtWiFi": "Wifi", + "drawClipArtTwinklingStar": "Stella scintillante", + "drawClipArtAlarmClock": "Sveglia", + "drawClipArtHourglass": "Clessidra", + "drawClipArtIdea": "Idea", "drawCategoryMyShapes": "Le mie forme", "drawShapeSpeechBubble": "Nuvoletta", "drawShapeSpeechBubbleRight": "Nuvoletta, coda a destra", @@ -4356,10 +4521,16 @@ "drawShapeThoughtBubble": "Nuvoletta pensiero", "drawShapeShoutBubble": "Nuvoletta urlo", "drawShapeCaptionBox": "Casella didascalia", + "drawShapeSpeechBubbleSide": "Nuvoletta, coda lato sinistro", + "drawShapeRoundBubble": "Bolla rotonda", + "drawShapePillBubble": "Bolla pillola", + "drawShapeScreamBubble": "Bolla urlo", "drawShapeArrow": "Freccia", "drawShapeDoubleArrow": "Doppia freccia", "drawShapeChevron": "Freccia a V", "drawShapeCurvedArrow": "Freccia curva", + "drawShapeRepeatArrow": "Ripeti freccia", + "drawShapeUTurnArrow": "Freccia inversione di marcia", "drawShapeRoundedRectangle": "Rettangolo arrotondato", "drawShapeTriangle": "Triangolo", "drawShapeDiamond": "Diamante", @@ -4370,6 +4541,8 @@ "drawShapeRing": "Anello", "drawShapePlus": "Più", "drawShapeBanner": "Striscione", + "drawShapeHalfCircle": "Semicerchio", + "drawShapeTeardrop": "Lacrima", "drawShapeHeart": "Cuore", "drawShapeCheck": "Segno di spunta", "drawShapeMusicNote": "Nota musicale", @@ -4378,8 +4551,10 @@ "drawShapeCloud": "Nuvola", "drawShapeMoon": "Luna", "drawShapeBadge": "Badge", - "drawSize": "Dimensione", - "drawLayer": "Strato", + "drawShapeSpeaker": "Altoparlante", + "drawShapeCross": "Incrocio", + "drawShapeSmiley": "Faccina", + "drawShapeSadSmiley": "Faccina triste", "drawBackgroundVideoFrame": "Fotogramma video (posizione attuale)", "drawBackgroundVideoFrameAt": "Fotogramma video a...", "drawBackgroundImage": "File immagine...", @@ -4387,13 +4562,30 @@ "drawBackgroundStretch": "Allunga l'immagine alla tela", "drawBackgroundOpacity": "Opacità sfondo", "drawBackgroundVideoFrameFailed": "Impossibile ottenere un fotogramma video a {0}.", - "drawImages": "Immagini", "drawConvertToLine": "Converti segmento in linea", "drawConvertShapeToCurves": "Converti tutte le linee in curve", "drawConvertShapeToLines": "Converti tutte le curve in linee", "drawRotateCounterClockwise": "Ruota di 90° in senso antiorario", "drawSvgImages": "Immagini SVG", "drawSvgNoShapes": "Nessuna forma piena o tratto trovato nel file SVG.", + "drawTextTool": "Strumento testo", + "drawDefaultText": "Testo", + "drawShrinkToFit": "Riduci per adattarlo", + "drawTextBox": "Casella di testo", + "drawDiscardChanges": "Il disegno è stato modificato.\r\nVuoi eliminare le modifiche?", + "drawColorPickerToolName": "Selettore colore", + "drawOpenDrawing": "Apri disegno ASSA", + "drawSaveDrawing": "Salva disegno ASSA", + "drawDrawingFiles": "File disegno ASSA", + "drawAssFiles": "File ASS", + "drawPositionXY": "Posizione: {0}, {1}", + "drawXShapes": "{0} forme", + "drawXPoints": "{0} punti", + "drawShapeXDraw": "Forma {0} (disegno)", + "drawShapeXErase": "Forma {0} (gomma)", + "drawGroup": "Raggruppa (Ctrl+G)", + "drawUngroup": "Separa (Ctrl+Maiusc+G)", + "drawGroupX": "Gruppo {0}", "progressBarTitle": "Barra avanzamento ASSA", "progressBarSettings": "Barra avanzamento", "mouseOverColor": "Colore al passaggio del mouse", @@ -4401,17 +4593,34 @@ "progressBarForeColor": "Colore primo piano", "progressBarSquareCorners": "Angoli squadrati", "progressBarRoundedCorners": "Angoli arrotondati", - "progressBarChapters": "Capitoli", "progressBarSplitterWidth": "Larghezza divisione", "progressBarSplitterHeight": "Altezza divisione", "progressBarXAdjustment": "Regolazione X", "progressBarYAdjustment": "Regolazione Y", "progressBarTextAlignment": "Allineamento testo", + "progressBarStyleCutCorners": "Angoli taglio", + "progressBarStyleArrow": "Freccia", + "progressBarStyleSlanted": "Inclinato", + "progressBarStyleSegments": "Segmenti", + "progressBarStyleRoundedSegments": "Segmenti arrotondati", + "progressBarStyleDots": "Punti", + "progressBarStyleDiamonds": "Diamanti", + "progressBarStyleChevrons": "Spina di pesce", + "progressBarStyleStripes": "Strisce", + "progressBarStyleWave": "Onda", + "progressBarStyleZigzag": "Zigzag", + "progressBarStyleTapered": "Rastremato", + "progressBarStyleRuler": "Righello", + "progressBarStyleDoubleLine": "Doppia linea", + "progressBarStyleEqualizer": "Equalizzatore", + "progressBarStyleStars": "Stelle", + "progressBarPlayhead": "Testina di riproduzione", + "progressBarEdgeMargin": "Margine bordo", + "progressBarSplitter": "Divisore", "fontCollectorTitle": "Raccoglitore font", "fontCollectorUsedIn": "Usato in", "fontCollectorFontFiles": "File font", "fontCollectorStyleX": "Stile: {0}", - "fontCollectorInlineLineX": "Riga {0}", "fontCollectorFound": "Trovato", "fontCollectorNotFound": "Non trovato", "fontCollectorEmbedded": "Incorporato", @@ -4465,8 +4674,26 @@ "backgroundBoxTornPaperDouble": "Carta strappata (in alto e in basso)", "backgroundBoxStarburst": "Esplosione di stelle", "backgroundBoxScroll": "Scorrimento/pergamena", + "backgroundBoxPill": "Pillola", + "backgroundBoxCutCorners": "Angoli di taglio", + "backgroundBoxSlanted": "Inclinato", + "backgroundBoxTicket": "Biglietto", + "backgroundBoxSpeechBubble": "Nuvoletta", + "backgroundBoxRibbon": "Nastro", + "backgroundBoxBrushStroke": "Tratto pennello", + "backgroundBoxCornerBrackets": "Staffe angolari", + "backgroundBoxUnderline": "Barra di sottolineatura", + "backgroundBoxFrame": "Fotogramma", + "backgroundBoxArrowTag": "Etichetta freccia", + "backgroundBoxSideBar": "Barra laterale", "stylesInFile": "Stili nel file", "stylesSaved": "Stili salvati", + "stylesSummary": "{0} stili nel file · {1} salvati", + "stylesTransform": "Trasforma", + "stylesEditing": "Modifica", + "stylesPreviewSampleTextHint": "Testo di esempio - \\N per una nuova riga", + "stylesScale": "Scala %", + "stylesPreviewChecker": "Controllo", "allCategories": "[Tutte]", "newCategory": "Nuova categoria...", "renameCategory": "Rinomina categoria...", @@ -4501,6 +4728,8 @@ "takeUsagesFromDotDotDot": "Prendi usi da...", "replaceStyleWithDotDotDot": "Sostituisci stile con...", "noAttachmentsFound": "Nessun allegato trovato nel file ASSA selezionato.", + "attachmentsDropHint": "Trascina qui i file dei font o l'immagine per allegarli", + "attachmentsSummary": "{0} font · {1} grafiche · {2}", "deleteStyleQuestion": "Vuoi eliminare lo stile?", "deleteStylesQuestion": "Vuoi eliminare gli stili?", "deleteXStylesQuestion": "Vuoi eliminare '{0}' stili?", @@ -4526,13 +4755,32 @@ "spinIn": "Gira dentro", "setPosition": "Imposta posizione", "rotation": "Rotazione", + "setPositionTopLeft": "In alto a sinistra", + "setPositionTopCenter": "In alto al centro", + "setPositionTopRight": "In alto a destra", + "setPositionMiddleLeft": "Al centro a sinistra", + "setPositionMiddleRight": "Al centro a destra", + "setPositionBottomLeft": "In basso a sinistra", + "setPositionBottomCenter": "In basso al centro", + "setPositionBottomRight": "In basso a destra", + "setPositionCustom": "Personalizzato", + "setPositionReset": "Ripristina la posizione della riga", + "setPositionResetRotation": "Ripristina rotazione", + "setPositionGuides": "Guide", + "setPositionSafeArea": "Zona sicura", + "setPositionCenterLines": "Linee centrali", + "setPositionSnapToGuides": "Aggancia alle guide", + "setPositionNudgeHint": "I tasti freccia spostano di 1 px, Maiusc di 10 px", "scaleX": "Scala X", "scaleY": "Scala Y", "spacing": "Spaziatura", "angle": "Angolo", "imageColorPicker": "Selettore colore immagine", "copyColorAsHextoClipboard": "Copia colore come esadecimale negli appunti", - "generatingBackgroundBoxXOfY": "Generazione casella di sfondo {0} / {1}...", + "imageColorPickerHint": "Fai clic sul fotgramma per scegliere un colore", + "copyColorAsAssaToClipboard": "Copia il colore come ASSA (&HBBGGRR&) negli appunti", + "copyColorAsAssaTagToClipboard": "Copia il colore come tag ASSA \\c negli appunti", + "generatingBackgroundBoxXOfY": "Generazione casella sfondo '{0}' di '{1}'...", "applyAdvancedEffectTitle": "Applica effetto avanzato", "chooseEffect": "Scegli effetto", "advancedEffectTypewriter": "Macchina da scrivere", @@ -4613,6 +4861,34 @@ "advancedEffectLowerThird": "Sottopancia", "advancedEffectLowerThirdDescription": "Banner in basso a sinistra per nome e ruolo con linea grafica di enfasi animata a scorrimento - stile intervista/documentario (prima riga = nome, righe successive = ruolo)", "advancedEffectLowerThirdAccentColor": "Colore enfasi", + "advancedEffectCategoryWords": "Parole e karaoke", + "advancedEffectCategoryInOut": "Ingresso e uscita", + "advancedEffectCategoryTextFx": "Effetti testo", + "advancedEffectCategoryScenes": "Scene", + "advancedEffectCategoryTitles": "Titoli e ringraziamenti", + "advancedEffectSearchWatermark": "Ricerca effetti...", + "advancedEffectNoMatches": "Nessun effetto corrisponde alla ricerca", + "advancedEffectCustomize": "Personalizza", + "advancedEffectNoSettings": "Questo effetto non ha impostazioni: funziona così com'è.", + "advancedEffectAmount": "Quantità", + "advancedEffectPreviewNoVideo": "Anteprima esempio: per vedere il rendering reale apri un video", + "advancedEffectHighlightBox": "Casella evidenziazione", + "advancedEffectHighlightBoxDescription": "Una casella colorata salta da una parola all'altra mentre la riga viene pronunciata: didascalie in stile CapCut", + "advancedEffectSingleWordPop": "Pop di una sola parola", + "advancedEffectSingleWordPopDescription": "Visualizza una parola grossa alla volta, emergendo mentre viene pronunciata: stile didascalia video breve", + "advancedEffectBoldCaptions": "Didascalie in grassetto", + "advancedEffectBoldCaptionsDescription": "Didascalie in grassetto con contorni spessi: la parola pronunciata si illumina di colore e cresce", + "advancedEffectHighlightColor": "Colore evidenziazione", + "advancedEffectUpperCase": "Maiuscolo", + "advancedEffectTextSizePercent": "Dimensione testo (%)", + "advancedEffectSmoothFill": "Riempimento liscio", + "advancedEffectSmoothFillDescription": "Un colroe evidenziazione attraversa uniformemente ogni parola mentre viene pronunciata", + "advancedEffectBlurInWords": "Parole sfocate", + "advancedEffectBlurInWordsDescription": "Le parole appaiono una per una mentre vengono pronunciate: ciascuna si affina da una sfocatura e si alza al suo posto", + "advancedEffectRollingCaptions": "Didascalie a scorrimento", + "advancedEffectRollingCaptionsDescription": "Ogni riga scorre in posizione e spinge verso l'alto la riga precedente, come una chat dal vivo o un teleprompter", + "advancedEffectFillColor": "Colore riempimento", + "advancedEffectRise": "Aumento dalla riga di base", "overrideTagsHistory": "Sostituisci cronologia tag" }, "about": { @@ -4635,12 +4911,10 @@ "all": "Tutti", "tooManyLines": "Troppe righe", "charactersPerSecond": "Velocità lettura", - "durationTooShort": "Troppo corta", - "durationTooLong": "Troppo lunga", "lineTooLong": "Riga troppo lunga", "lineTooWide": "Riga troppo larga", - "overlap": "Sovrapposizione", "gapTooShort": "Gap troppo breve", + "customRule": "Regola personalizzata", "tooManyLinesHint": "Più righe rispetto al numero massimo di righe", "charactersPerSecondHint": "Caratteri al secondo superiori al massimo", "durationTooShortHint": "Tempo visualizzazione inferiore al minimo", @@ -4649,13 +4923,13 @@ "lineTooWideHint": "Una riga è più larga (in pixel) della larghezza massima", "overlapHint": "Si sovrappone alla riga precedente o successiva", "gapTooShortHint": "Lo spazio tra la riga precedente o successiva è inferiore al minimo", + "customRuleHint": "Il testo corrisponde a una delle regole di errore personalizzate", "detailXGreaterThanY": "{0} > {1}", "detailXLessThanY": "{0} < {1}", "detailOverlapFromPrevious": "dal precedente: {0} ms", "detailOverlapToNext": "al successivo: {0} ms", "detailGapToPrevious": "al precedente: {0} < {1} ms", "detailGapToNext": "al successivo: {0} < {1} ms", - "detail": "Dettagli", "tip": "Fai doppio clic su una riga (o premi 'Invio') per accedervi. I controlli da eseguire e i relativi limiti vengono impostati in Impostazioni > Generali.", "exportAsText": "File testo (.txt)...", "exportAsExcel": "File Excel (.xlsx)...", From dac1bbf16a6a783bda0084de2ebea027e27207f0 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 20:46:35 +0200 Subject: [PATCH 14/82] Language auto-detect: tell Macedonian and Serbian from Bulgarian/Russian by their letters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Natural Macedonian text was reported as Bulgarian (or Russian): the South Slavic languages share their short words (не, да, за, нас, може, така, беше, ...), so the Bulgarian and Russian word lists won, and the Macedonian list was mostly words from one space film plus OCR misreads with Latin letters. The spelling is unambiguous: ј/љ/њ/џ are written only in Macedonian and Serbian, ќ/ѓ/ѕ only in Macedonian, ћ/ђ only in Serbian, and none of them has ъ/ы/э/ё/й/щ/ь/ю/я. One letter scan per detection now decides "mk" or "sr" in the Russian and Bulgarian blocks and in the letter-statistics fallback (which answered "ru" for every Cyrillic text). Bulgarian/Russian keep winning when their letters outnumber the misread ones in an OCR file. The Macedonian word list is rebuilt from everyday words that Bulgarian and Serbian spell differently. Co-Authored-By: Claude Fable 5.1 --- src/libse/Common/LanguageAutoDetect.cs | 123 +++++++++++++++--- .../Core/LanguageAutoDetectCyrillicTest.cs | 109 ++++++++++++++++ .../Core/LanguageAutoDetectShortFileTest.cs | 3 + 3 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 tests/libse/Core/LanguageAutoDetectCyrillicTest.cs diff --git a/src/libse/Common/LanguageAutoDetect.cs b/src/libse/Common/LanguageAutoDetect.cs index 47c627d61ea..db9cb225c15 100644 --- a/src/libse/Common/LanguageAutoDetect.cs +++ b/src/libse/Common/LanguageAutoDetect.cs @@ -118,6 +118,64 @@ private static int GetCount(string text, string[] words) return CountWords(text).Get(words); } + /// + /// Tells Macedonian or Serbian from the other Cyrillic languages by spelling: ј, љ, њ + /// and џ are written only in those two, and neither of them has ъ, ы, э, ё, й, щ, ь, ю + /// or я (which Bulgarian, Russian and Ukrainian use all the time). Within the pair, ќ, + /// ѓ and ѕ exist only in Macedonian and ћ and ђ only in Serbian. This beats the word + /// counts, where the short words all South Slavic languages share (не, да, за, нас, може, + /// така, беше, ...) let the Bulgarian and Russian lists win on Macedonian text. + /// + /// The text to look at. + /// Word counts of the same text, for a tie between the pair; null to count on demand. + /// "mk", "sr", or null when the letters do not say Macedonian or Serbian. + private static string GetMacedonianOrSerbianByLetters(string text, WordListCounts? counts) + { + var macedonianOrSerbian = 0; // ј љ њ џ ќ ѓ ѕ ћ ђ + var bulgarianOrRussian = 0; // ъ ы э ё й щ ь ю я + var macedonianOnly = 0; // ќ ѓ ѕ + var serbianOnly = 0; // ћ ђ + foreach (var c in text) + { + switch (c) + { + case 'ј': case 'Ј': case 'љ': case 'Љ': case 'њ': case 'Њ': case 'џ': case 'Џ': + macedonianOrSerbian++; + break; + case 'ќ': case 'Ќ': case 'ѓ': case 'Ѓ': case 'ѕ': case 'Ѕ': + macedonianOrSerbian++; + macedonianOnly++; + break; + case 'ћ': case 'Ћ': case 'ђ': case 'Ђ': + macedonianOrSerbian++; + serbianOnly++; + break; + case 'ъ': case 'Ъ': case 'ы': case 'Ы': case 'э': case 'Э': case 'ё': case 'Ё': case 'й': case 'Й': + case 'щ': case 'Щ': case 'ь': case 'Ь': case 'ю': case 'Ю': case 'я': case 'Я': + bulgarianOrRussian++; + break; + } + } + + // Strictly more, so that a few misread letters in an OCR'ed file cannot flip a + // Bulgarian or Russian text, where ъ/й/я/ю are everywhere. + if (macedonianOrSerbian == 0 || macedonianOrSerbian <= bulgarianOrRussian) + { + return null; + } + + if (macedonianOnly != serbianOnly) + { + return macedonianOnly > serbianOnly ? "mk" : "sr"; + } + + // Neither ќ/ѓ/ѕ nor ћ/ђ, or as many of each (an OCR tie): the word lists decide. + var c2 = counts ?? CountWords(text); + var macedonianCount = c2.Get(AutoDetectWordsMacedonian); + var serbianCount = c2.Get(AutoDetectWordsSerbianCyrillic) + c2.Get(AutoDetectWordsSerbianCyrillicOnly); + return macedonianCount >= serbianCount ? "mk" : "sr"; + } + private static int GetCountContains(string text, params string[] words) { var count = 0; @@ -430,12 +488,15 @@ public static string AutoDetectGoogleLanguage(Encoding encoding) "있어요", "보여줘", "괜찮아요", "목소릴", "기억이", "저주를", "좋아요" }; + // Everyday Macedonian words, preferring the ones Bulgarian and Serbian spell + // differently (ќе/ще/ће, јас/аз/ја, тоа/това/то, зошто/защо/зашто, ...). The old list + // was mostly words from one space film plus OCR misreads with Latin letters in them. private static readonly string[] AutoDetectWordsMacedonian = { - "господине", "Нема", "господине", "работа", "вселената", "Може", "треба", "Треба", "слетување", "капсулата", "време", "Френдшип", "Прием", "Добро", "пресметки", "Благодарам", "нешто", "Благодарам", "орбитата", "инженер", - "Харисон", "Фала", "тоалет", "орбита", "знаеме", "Супервизор", "жени", "Добра", "требаат", "што", "дeкa", "eшe", "кучe", "Руиз", "кучeто", "кучињa", "Бјути", "имa", "многу", "кучињaтa", "AДЗЖ", "Животни", "моЖe", "мaчe", - "мecто", "имaмe", "мaчињa", "пpвото", "пpaвaт", "нeшто", "колку", - "што", "тоа", "дека", "Што", "Дали", "треба", "беше", "таму" + "ќе", "што", "тоа", "дека", "дали", "јас", "тој", "таа", "тие", "сум", "треба", "требаат", "нема", "може", "можеби", "мора", + "многу", "нешто", "ништо", "некој", "никој", "сите", "колку", "каде", "зошто", "затоа", "тука", "таму", "уште", "веќе", + "сега", "денес", "секогаш", "никогаш", "сакам", "мислам", "знаеме", "одиме", "правиш", "знаеш", "имаш", "беше", "време", + "работа", "луѓе", "жени", "добро", "добра", "благодарам", "фала", "господине", "тоалет", "инженер", "вселената", "орбитата", }; private static readonly string[] AutoDetectWordsFinnish = @@ -613,6 +674,8 @@ private static string AutoDetectGoogleLanguage(string text, int bestCount, out i var best = string.Empty; var bestScore = -1; var counts = CountWords(text); + var macedonianOrSerbianChecked = false; + string macedonianOrSerbianByLetters = null; void Consider(string languageCode, int score) { @@ -623,6 +686,18 @@ void Consider(string languageCode, int score) } } + // "mk", "sr" or null, scanning the letters once no matter how many blocks ask. + string MacedonianOrSerbian() + { + if (!macedonianOrSerbianChecked) + { + macedonianOrSerbianByLetters = GetMacedonianOrSerbianByLetters(text, counts); + macedonianOrSerbianChecked = true; + } + + return macedonianOrSerbianByLetters; + } + var count = counts.Get(AutoDetectWordsEnglish); if (count > bestCount) { @@ -766,7 +841,12 @@ void Consider(string languageCode, int score) var bulgarianCount = counts.Get(AutoDetectWordsBulgarian); var ukrainianCount = counts.Get(AutoDetectWordsUkrainian); var macedonianCount = counts.Get(AutoDetectWordsMacedonian); - if (bulgarianCount > count) + var macedonianOrSerbian = MacedonianOrSerbian(); + if (macedonianOrSerbian != null) + { + Consider(macedonianOrSerbian, count); + } + else if (bulgarianCount > count) { if (ukrainianCount > bulgarianCount && ukrainianCount > macedonianCount) { @@ -814,7 +894,12 @@ void Consider(string languageCode, int score) if (count > bestCount) { var macedonianCount = counts.Get(AutoDetectWordsMacedonian); - if (macedonianCount > count) + var macedonianOrSerbian = MacedonianOrSerbian(); + if (macedonianOrSerbian != null) + { + Consider(macedonianOrSerbian, count); + } + else if (macedonianCount > count) { Consider("mk", count); } @@ -2520,24 +2605,26 @@ public static string GetEncodingViaLetter(string text) } } - if (topCount == 1) + var result = LanguageCodes[firstTop]; + for (var p = 0; topCount > 1 && p < PriorityOrder.Length; p++) { - return LanguageCodes[firstTop]; + // Priority order for tie-breaking + var i = Array.IndexOf(LanguageCodes, PriorityOrder[p]); + if (i >= 0 && languageScores[i] == maxScore) + { + result = PriorityOrder[p]; + break; + } } - // Priority order for tie-breaking - foreach (var lang in PriorityOrder) + if (result == "ru") { - for (var i = 0; i < languageScores.Length; i++) - { - if (languageScores[i] == maxScore && LanguageCodes[i] == lang) - { - return lang; - } - } + // The Cyrillic letter set is Russian's, so every Cyrillic text lands here; + // Macedonian and Serbian have letters of their own that say otherwise. + return GetMacedonianOrSerbianByLetters(text, null) ?? "ru"; } - return LanguageCodes[firstTop]; + return result; } public static bool IsLanguageWithoutPeriods(string language) diff --git a/tests/libse/Core/LanguageAutoDetectCyrillicTest.cs b/tests/libse/Core/LanguageAutoDetectCyrillicTest.cs new file mode 100644 index 00000000000..ca8b424605a --- /dev/null +++ b/tests/libse/Core/LanguageAutoDetectCyrillicTest.cs @@ -0,0 +1,109 @@ +using Nikse.SubtitleEdit.Core.Common; + +namespace LibSETests.Core; + +/// +/// Macedonian, Serbian and Bulgarian share most of their short words (не, да, за, нас, може, +/// така, беше, ...), so natural Macedonian or Serbian text used to be reported as Bulgarian or +/// Russian (user mail, 2026-10-19: "the language shown is Bulgarian instead of Macedonian"). +/// The spelling tells them apart for sure: ј/љ/њ/џ are Macedonian and Serbian only, ќ/ѓ/ѕ +/// Macedonian only, ћ/ђ Serbian only, and ъ/ы/э/ё/й/щ/ь/ю/я never occur in either. +/// +public class LanguageAutoDetectCyrillicTest +{ + // Plot summaries of the same film, in the three languages. + private const string Macedonian = @"„Пред дождот“ е македонски воен филм од 1994 година, во режија на Милчо Манчевски. Се состои од три поврзани дела: „Зборови“, „Лица“ и „Слики“. +Во првиот дел, во еден манастир, младиот монах Кирил крие Замира, девојка од албанско семејство, која бега од селска потера. Кога селаните ја откриваат, монахот заминува од манастирот заедно со неа, но нивната заедничка иднина завршува трагично. +Вториот дел се случува во Лондон. Фотографот Александар Кирков, Македонец и добитник на Пулицерова награда, има љубовна врска со Ен. Нивната врска е прекината со трагедија во кафеана, каде загинува нејзиниот сопруг. +Третиот дел се одвива во Македонија, каде Александар се враќа во родниот крај, кој е поделен меѓу два народа и полн со недоверба. Тој се обидува да ја спаси Замира од обвинение за убиство, но на крајот загинува од рака на роднина."; + + private const string Bulgarian = @"Преди дъжда е драматичен филм от 1994 г. на режисьора и сценариста Милчо Манчевски, копродукция между Република Македония и Великобритания. В главните роли са Катрин Картлидж, Раде Шербеджия, Грегоар Колин и Лабина Митевска. Музиката е на македонската група „Анастасия“. Филмът е номиниран за „Оскар“ и печели „Златен лъв“ във Венеция през 1994 г. +Историята е разделена на три части, които разказват за невъзможни любови: +Първата част следва млад монах, Кирил, който е положил обет за мълчание, но го нарушава заради албанско момиче, Замира. +Втората част се развива в Лондон и проследява Ан, редактор на снимки, разкъсана между съпруга си Ник и военния фотограф Александър. +Третата част обединява предишните две истории. Александър се връща в Северна Македония, където се среща с Хана, в която е бил влюбен преди заминаването си. Той тръгва да търси Замира, дъщерята на Хана, но на хоризонта се надига буря и филмът започва отначало."; + + private const string Serbian = @"Пре кише је македонски драмски филм из 1994. године, у режији и по сценарију Милча Манчевског. Подељен је у три дела: „Зборови”, „Лица” и „Слике”, а сваки има у средишту трагичну и несрећну љубав. +У делу „Зборови” монах Ћирило, који се заветовао на ћутање у манастиру у македонским планинама, устаје да брани Замиру, младу Албанку која бежи од руље оптужене за убиство. Заједно напуштају манастир и беже кроз планину, али њихова прича се нагло и брутално завршава. +У делу „Лица” радња се одвија у Лондону, где Ана живи растрзана између љубави према супругу Нику и привлачности према разочараном македонском ратном фотографу Александру. Она запада у низ трагичних догађаја након пуцњаве у оближњем ресторану. +У делу „Слике” Александар се враћа у родно село, које је рат поделио по националности и у којем се комшије Албанци гледају као непријатељи. Тамо га стара љубав Хана моли да сачува њену ћерку Замиру. Док Александар тражи Замиру, на хоризонту се ствара олуја, и филм се враћа на почетак."; + + private static readonly string[] MacedonianDialogue = + { + "Што правиш тука?", "Не знам, благодарам.", "Ќе видиме што ќе се случи.", "Јас сум тука веќе два часа.", + "Дали си добро?", "Да, се разбира.", "Тој рече дека ќе дојде утре.", "Не можам да верувам.", + "Ајде, одиме!", "Каде беше вчера?", "Сакам да ти кажам нешто.", "Многу ми е жал.", + "Ништо не е во ред.", "Треба да одиме сега.", "Зошто го направи тоа?", "Таа е мојата сестра.", + "Ние сме пријатели.", "Добро утро, господине.", "Колку чини ова?", "Никогаш не сум бил таму.", + "Те молам, слушај ме.", "Луѓето зборуваат.", "Ова е многу важно.", "Знаеш ли кој е тој?", + "Еве, земи го.", "Нема проблем.", "Чекај малку!", "Што се случува овде?", "Тие не се тука.", + "Можеби имаш право.", "Сега разбирам.", "Ќе ти се јавам подоцна.", "Денес е убав ден.", + "Имам само едно прашање.", "Секогаш си бил добар човек.", "Не го познавам.", "Види, не е така.", + "Мора да бидеме внимателни.", "Кога ќе дојдеш?", "Во ред, ќе почекам.", + }; + + private static Subtitle Make(IEnumerable lines) + { + var sub = new Subtitle(); + var start = 0; + foreach (var line in lines) + { + sub.Paragraphs.Add(new Paragraph(line, start, start + 2000)); + start += 2500; + } + + return sub; + } + + private static IEnumerable Sentences(string text) + { + return text.Replace("\r", string.Empty).Split(new[] { ". ", ".\n", ": " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()); + } + + [Fact] + public void MacedonianProseIsMacedonian() + { + Assert.Equal("mk", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(Sentences(Macedonian)))); + } + + [Fact] + public void SerbianProseIsSerbian() + { + Assert.Equal("sr", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(Sentences(Serbian)))); + } + + [Fact] + public void BulgarianProseStaysBulgarian() + { + Assert.Equal("bg", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(Sentences(Bulgarian)))); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(20)] + [InlineData(40)] + public void MacedonianDialogueOfAnyLengthIsMacedonian(int lines) + { + Assert.Equal("mk", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(MacedonianDialogue.Take(lines)))); + } + + [Fact] + public void MacedonianWithoutListWordsFallsBackToLettersNotRussian() + { + // No word of any Cyrillic list, so the word pass finds nothing and the letter + // statistics used to answer "ru" for every Cyrillic text. + Assert.Equal("mk", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(new[] { "Ѕвездите светат.", "Меѓутоа, пријателот замина." }))); + Assert.Equal("sr", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(new[] { "Ђаци певају.", "Међутим, пријатељ оде." }))); + } + + [Fact] + public void FewMisreadLettersDoNotFlipBulgarian() + { + // An OCR'ed Bulgarian file with a stray ј and ќ keeps its ъ/й/я majority. + var lines = Sentences(Bulgarian).ToList(); + lines[0] = lines[0].Replace("Милчо", "Милчо ј ќ"); + Assert.Equal("bg", LanguageAutoDetect.AutoDetectGoogleLanguageOrNull(Make(lines))); + } +} diff --git a/tests/libse/Core/LanguageAutoDetectShortFileTest.cs b/tests/libse/Core/LanguageAutoDetectShortFileTest.cs index ee9a463b8b7..e762e05d297 100644 --- a/tests/libse/Core/LanguageAutoDetectShortFileTest.cs +++ b/tests/libse/Core/LanguageAutoDetectShortFileTest.cs @@ -156,6 +156,9 @@ public void SingleLineFrench() // Cyrillic [InlineData("bg", "Какво правиш тук?", "Не знам, благодаря.")] [InlineData("sr", "Шта радиш овде?", "Не знам, хвала.")] + [InlineData("mk", "Што правиш тука?", "Не знам, благодарам.")] + [InlineData("mk", "Ќе дојдам утре.", "Не можам да верувам.")] // only Bulgarian list words (не, да) - the ќ decides + [InlineData("sr", "Доћи ћу сутра.", "Не могу да верујем.")] // ћ decides // Greek [InlineData("el", "Τι κάνεις εδώ;", "Δεν ξέρω, ευχαριστώ πολύ.")] // Middle East From 07e3d34cd0a6fc41f7ea81eb94fce4013175c33d Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 20:55:47 +0200 Subject: [PATCH 15/82] Italian: fix swapped nOCR zoom tooltips, typos and wording in new strings Zoom in/out tooltips were inverted, "Colora le righe" for the custom error rules toggle, "Elenco elenco", "uan", "delal", "fotgramma", "colroe", "Analisi in corso...", double spaces, Disinstalla, "un tuo modello", Cornice for the Frame background box, and the Sync to audio hint now names the real menu items. Co-Authored-By: Claude Fable 5.1 --- src/ui/Assets/Languages/Italian.json | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ui/Assets/Languages/Italian.json b/src/ui/Assets/Languages/Italian.json index a5db6075bf8..c8d97b53b3a 100644 --- a/src/ui/Assets/Languages/Italian.json +++ b/src/ui/Assets/Languages/Italian.json @@ -718,7 +718,7 @@ "undelete": "Annulla eliminazione", "underline": "Sottolineato", "undo": "Annulla operazione", - "uninstall": "Disinstallare", + "uninstall": "Disinstalla", "uninstalled": "Disinstallato", "unknown": "Sconosciuto", "unknownNoInstallRecord": "Sconosciuto (nessuna registrazione installazione)", @@ -1424,10 +1424,10 @@ "exportPacSecondaryCodePage": "Pagina codici secondaria", "customTextFormatsDotDotDot": "Formati testo personali_zzati...", "plainTextDotDotDot": "_Testo semplice...", - "dialogueListDotDotDot": "Elenco elenco dialoghi Excel (.xlsx)...", + "dialogueListDotDotDot": "Elenco dialoghi Excel (.xlsx)...", "titleExportDialogueList": "Esporta elenco dialoghi Excel", "dialogueListLineBreaksInCell": "Mantieni le interruzioni di riga all'interno della cella (disattivato: unisce le righe con ' | ')", - "dialogueListHeaderRow": "Riga intestazione", + "dialogueListHeaderRow": "Riga intestazione", "dialogueListFramesFormat": "oo:mm:ss:ff (fotogrammi)", "dialogueListNoColumns": "Scegli almeno una colonna.", "dialogueListEnd": "Fine", @@ -2649,7 +2649,7 @@ "selectModel": "Seleziona modello", "addCustomModelDotDotDot": "Aggiungi modello personalizzato...", "customModelHelp": "Puoi usare un modello: scegli il file del modello (whisper.cpp ggml '.bin') o la cartella del modello (cartella con file 'model.bin' all'interno).\r\nVerrà copiato nella cartella dei modelli e aggiunto all'elenco qui sopra.", - "customModelHelpGguf": "Puoi usare il modello: scegli un file qwen3-asr.cpp '.gguf' (un file che contiene sia il codificatore audio che il modello linguistico, ad esempio da OpenVoiceOS o FlippyDora in Hugging Face).\r\nViene copiato nella cartella dei modelli e aggiunto all'elenco sopra.\r\nI file GGUF creati per llama.cpp, transcribe.cpp o CapsWriter usano un layout diverso e non possono essere selezionati.", + "customModelHelpGguf": "Puoi usare un tuo modello: scegli un file qwen3-asr.cpp '.gguf' (un file che contiene sia il codificatore audio che il modello linguistico, ad esempio da OpenVoiceOS o FlippyDora in Hugging Face).\r\nViene copiato nella cartella dei modelli e aggiunto all'elenco sopra.\r\nI file GGUF creati per llama.cpp, transcribe.cpp o CapsWriter usano un layout diverso e non possono essere selezionati.", "viewToolsLogFile": "Visualizza file registro Whisper", "updateXTitle": "Vuoi aggiornare {0}?", "updateXMessage": "È disponibile una nuova versione di {0}.{1}{1}Vuoi scaricare e installare l'aggiornamento?", @@ -3135,10 +3135,10 @@ "syncToAudioPickReferenceSubtitle": "Scegli il file sottotitoli con cui sincronizzare", "syncToAudioNoReferenceSubtitle": "Nessun file scelto", "syncToAudioMaxOffsetSeconds": "Offset massimo (secondi)", - "syncToAudioTryFrameRates": "Prova anche le conversioni delal frequenza fotogrammi (23.976, 24, 25, 29.97, 30)", + "syncToAudioTryFrameRates": "Prova anche le conversioni della frequenza fotogrammi (23.976, 24, 25, 29.97, 30)", "syncToAudioResult": "Risultato", "syncToAudioAnalyze": "Analizza", - "syncToAudioAnalyzing": "Analizzi...", + "syncToAudioAnalyzing": "Analisi in corso...", "syncToAudioResultOffsetX": "Scostamento: {0}", "syncToAudioResultFrameRateXToY": "Frequenza fotogrammi: {0} → {1}", "syncToAudioResultFrameRateUnchanged": "Frequenza fotogrammi: invariata", @@ -3148,7 +3148,7 @@ "syncToAudioNoLines": "Il sottotitolo non ha righe.", "syncToAudioNoSpeechFound": "Nell'audio non è stato possibile distinguere alcun discorso dal silenzio.", "syncToAudioReferenceSubtitleEmpty": "Il sottotitolo di riferimento non ha righe.", - "syncToAudioHint": "Trova un offset per l'intero sottotitolo (e una mancata corrispondenza della frequenza fotogrammi, se presente) confrontando il momento in cui viene visualizzato il testo con quando qualcuno parla.\r\nUn sottotitolo che non è sincronizzato in modo diverso nelle diverse parti necessita invece di 'Sincronizzazione accurata' o 'Migliora timecode'.", + "syncToAudioHint": "Trova un offset per l'intero sottotitolo (e una mancata corrispondenza della frequenza fotogrammi, se presente) confrontando il momento in cui viene visualizzato il testo con quando qualcuno parla.\r\nUn sottotitolo che non è sincronizzato in modo diverso nelle diverse parti necessita invece di 'Sincronizzazione punti' o 'Migliora time code'.", "syncToAudioAppliedX": "Sincronizza sottotitoli con l'audio: {0}", "generateTimeCodes": "Genera time code", "checkeredImage": "Immagine a scacchi", @@ -3220,7 +3220,7 @@ "openFromUrlIncludeAutoGeneratedSubtitles": "Includi sottotitoli generati automaticamente (lingua originale)", "openFromUrlCookiesFromBrowser": "Cookie dal browser", "openFromUrlCookiesFromBrowserHint": "Invia i cookie del browser da cui hai effettuato l'accesso a YouTube, necessari quando YouTube chiede di confermare che non sei un bot.", - "openFromUrlBotCheckHint": "YouTube vuole uan conferma che non sei un bot.\r\nAccedi a YouTube nel browser, quindi seleziona il browser in questione in 'Cookie dal browser' nella finestra 'Apri file video da URL' e riprova (Firefox funziona meglio).\r\nPuò essere d’aiuto disattivare la VPN.", + "openFromUrlBotCheckHint": "YouTube vuole una conferma che non sei un bot.\r\nAccedi a YouTube nel browser, quindi seleziona il browser in questione in 'Cookie dal browser' nella finestra 'Apri file video da URL' e riprova (Firefox funziona meglio).\r\nPuò essere d'aiuto disattivare la VPN.", "openFromUrlBotCheckHintWithBrowserX": "YouTube vuole ancora una conferma che non sei un bot.\r\nAssicurati di aver effettuato l'accesso a YouTube in '{0}'.\r\nChrome e Edge su Windows spesso bloccano la lettura dei cookie: prima chiudi il browser o prova Firefox.", "assaStyleWillBeUsed": "Verrà usato l'attuale stile ASSA\n\nSe vuoi impostare gli stili qui\ncambia il formato dei sottotitoli", "pickOnlineSubtitleTitle": "Scegli i sottotitoli da scaricare", @@ -3523,7 +3523,7 @@ "colorGapTooShort": "Colora se il gap è troppo breve", "errorBackgroundColor": "Colore sfondo errore", "customErrorRules": "Regole errore personalizzate", - "customErrorRulesHighlight": "Righe colore che corrispondono a una regola di errore personalizzata", + "customErrorRulesHighlight": "Colora le righe che corrispondono a una regola di errore personalizzata", "customErrorRulesHint": "Una riga il cui testo corrisponde all'espressione regolare di una regola abilitata viene colorata con il colore della regola e conta come un errore (Elenca errori, Vai all'errore successivo).\r\nLa prima regola di corrispondenza decide il colore.", "customErrorRuleSelected": "Regola selezionata", "customErrorRuleTest": "Test", @@ -4350,8 +4350,8 @@ "trainingXLearnedYSkipped": "Addestramento '{0}' - {1:#,##0} caratteri appresi, {2:#,##0} saltati", "trainingDoneXLearned": "Addestramento completato: {0:#,##0} caratteri appresi", "selectAtLeastOneFont": "Seleziona almeno un carattere", - "nOcrZoomIn": "Zoom -", - "nOcrZoomOut": "Zoom +", + "nOcrZoomIn": "Zoom +", + "nOcrZoomOut": "Zoom -", "nOcrLineAlgorithm": "Algoritmo", "nOcrLineAlgorithmHint": "Algoritmo usato da disegno automatico per generare righe di primo piano/sfondo" }, @@ -4408,7 +4408,7 @@ "drawCategorySymbols": "Simboli", "drawCategoryClipArt": "ClipArt", "drawClipArtRedHeart": "Cuore rosso", - "drawClipArtAngerMark": "Marchio rabbia", + "drawClipArtAngerMark": "Marchio rabbia", "drawClipArtSweatDrop": "Goccia di sudore", "drawClipArtSparkles": "Scintille", "drawClipArtBlush": "Arrossamento", @@ -4683,7 +4683,7 @@ "backgroundBoxBrushStroke": "Tratto pennello", "backgroundBoxCornerBrackets": "Staffe angolari", "backgroundBoxUnderline": "Barra di sottolineatura", - "backgroundBoxFrame": "Fotogramma", + "backgroundBoxFrame": "Cornice", "backgroundBoxArrowTag": "Etichetta freccia", "backgroundBoxSideBar": "Barra laterale", "stylesInFile": "Stili nel file", @@ -4777,7 +4777,7 @@ "angle": "Angolo", "imageColorPicker": "Selettore colore immagine", "copyColorAsHextoClipboard": "Copia colore come esadecimale negli appunti", - "imageColorPickerHint": "Fai clic sul fotgramma per scegliere un colore", + "imageColorPickerHint": "Fai clic sul fotogramma per scegliere un colore", "copyColorAsAssaToClipboard": "Copia il colore come ASSA (&HBBGGRR&) negli appunti", "copyColorAsAssaTagToClipboard": "Copia il colore come tag ASSA \\c negli appunti", "generatingBackgroundBoxXOfY": "Generazione casella sfondo '{0}' di '{1}'...", @@ -4882,7 +4882,7 @@ "advancedEffectUpperCase": "Maiuscolo", "advancedEffectTextSizePercent": "Dimensione testo (%)", "advancedEffectSmoothFill": "Riempimento liscio", - "advancedEffectSmoothFillDescription": "Un colroe evidenziazione attraversa uniformemente ogni parola mentre viene pronunciata", + "advancedEffectSmoothFillDescription": "Un colore di evidenziazione attraversa uniformemente ogni parola mentre viene pronunciata", "advancedEffectBlurInWords": "Parole sfocate", "advancedEffectBlurInWordsDescription": "Le parole appaiono una per una mentre vengono pronunciate: ciascuna si affina da una sfocatura e si alza al suo posto", "advancedEffectRollingCaptions": "Didascalie a scorrimento", From 911d4f05952bec3a037af526df7bfe4155342b90 Mon Sep 17 00:00:00 2001 From: TOKUZO Date: Tue, 20 Oct 2026 03:58:55 +0900 Subject: [PATCH 16/82] Update Japanese.json for 5.4 Beta3 Updated the Japanese translation for Subtitle Edit 5.4.0 Beta2, in preparation for Beta3. --- src/ui/Assets/Languages/Japanese.json | 32 ++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/ui/Assets/Languages/Japanese.json b/src/ui/Assets/Languages/Japanese.json index c163e640fd4..e88755e9c8e 100644 --- a/src/ui/Assets/Languages/Japanese.json +++ b/src/ui/Assets/Languages/Japanese.json @@ -1,6 +1,6 @@ { "title": "Subtitle Edit", - "version": "v5.4.0-beta2", + "version": "v5.4.0-beta3", "translatedBy": "hisui3393", "cultureName": "jp-JP", "general": { @@ -660,6 +660,9 @@ "styles": "スタイル", "subtitleFile": "字幕ファイル", "subtitleFileChangedOutsideX": "ファイル「{0}」がSubtitle Edit以外で変更されました。\n\n再読み込みしますか?(現在の変更内容は失われます)", + "subtitleFileChangedOutsideReloadX": "ファイル「{0}」がSubtitle Edit以外で変更されました。\n\n再読み込みしますか?", + "subtitleFileChangedOutsideOverwriteX": "ファイル「{0}」は読み込み後にSubtitle Edit以外で変更されました。\n\nSubtitle Editで編集中の字幕で上書きしますか?", + "subtitleFileChangedOutsideAutoSaveSkippedX": "自動保存をスキップしました:{0}はSubtitle Edit以外で変更されました", "subtitleFileName": "字幕ファイル名", "subtitleFileSaved": "字幕ファイルが保存されました", "subtitleFileSavedToX": "字幕ファイルが{0}に保存されました", @@ -1093,6 +1096,7 @@ "seekAmountHint": "移動量 {0}", "seekVideo": "動画シーク(<< >>)", "configureToolbarItems": "ツールバー項目の設定...", + "ruleProfileHint": "ルールプロファイル {0}", "textPreviousHint": "前の字幕を再生し、終端で停止 {0}", "textPlayHint": "現在の字幕を再生し、終端で停止 {0}", "textPauseHint": "再生を一時停止 {0}", @@ -1157,6 +1161,11 @@ "copyTextFromOriginalToCurrent": "並べた別字幕から作業字幕へテキストをコピーします", "insertedXTextsFromSubtitleY": "字幕ファイル{1}から{0}個のテキストを挿入しました", "italicHint": "選択した行/文字を斜体に", + "recordSpeechHint": "マイクから音声を録音してテキストボックスに入力します(もう一度クリックすると停止)", + "recordingSpeechClickToStop": "録音中... 停止するにはマイクボタンをもう一度クリックしてください", + "recordingFailedX": "録音に失敗しました:{0}", + "noMicrophoneFound": "マイクが見つかりません", + "noSpeechRecognized": "音声を認識できませんでした", "joinedSubtitleLoaded": "結合された字幕が読み込まれました", "lineXTextAndTimingChanged": "行{0}: テキストとタイミングが変更されました", "lineXTextChangedFromYToZ": "行{0}: テキストが{1}から{2}に変更されました", @@ -1436,6 +1445,7 @@ "addNewLineAfterTimeCode": "表示タイミングの後に改行を追加", "addNewLineAfterText": "テキストの後に改行を追加", "addLineBetweenSubtitles": "字幕間に空行を追加", + "mergeAllLines": "すべての字幕行を結合", "titleExportDCinemaInteropPng": "D-Cinema(Interop/png)で書き出し", "titleExportDCinemaSmpte2014Png": "D-Cinema(SMPTE 2014/png)で書き出し", "titleExportDCinemaSmpte2014Mxf": "D-Cinema(SMPTE 2014/MXF)で書き出し", @@ -1763,7 +1773,13 @@ "findWhat": "検索文字列", "descriptionOptional": "説明(任意)", "regularExpressionTooSlowX": "正規表現が{0}秒でタイムアウトしました - このルールはスキップされます。より単純なパターンを試してください", - "moveToCategory": "カテゴリーに移動" + "moveToCategory": "カテゴリーに移動", + "addRules": "ルールを追加", + "addRulesDotDotDot": "ルールを追加...", + "addRulesToX": "「{0}」にルールを追加", + "xRules": "{0:#,##0} 件のルール", + "discardXRules": "入力した{0}件のルールを破棄しますか?", + "enterAddsRule": "Enterキーでルールをリストに追加し、OKでリストをカテゴリに追加します(Ctrl+Enter)" }, "find": { "searchTextWatermark": "検索中...", @@ -2633,6 +2649,7 @@ "selectModel": "モデルを選択", "addCustomModelDotDotDot": "カスタムモデルを追加...", "customModelHelp": "独自のモデルを使用できます。モデルファイル(whisper.cpp の ggml「.bin」ファイル)またはモデルフォルダー(「model.bin」を含む faster-whisper フォルダー)を選択してください。\n\n選択したモデルは models フォルダーにコピーされ、上の一覧に追加されます", + "customModelHelpGguf": "独自のモデルを使用できます。qwen3-asr.cpp用の「.gguf」ファイルを選択してください(音声エンコーダーと言語モデルの両方を1つにまとめたファイル。Hugging Face上のOpenVoiceOSやFlippyDoraなどから入手できます)。ファイルはモデルフォルダーにコピーされ、上のリストに追加されます。llama.cpp、transcribe.cpp、CapsWriter用に作成されたGGUFファイルは構成が異なるため、使用できません。", "viewToolsLogFile": "Whisperログファイルを表示", "updateXTitle": "{0}を更新しますか?", "updateXMessage": "{0}の新しいバージョンが利用可能です。{1}{1}今すぐアップデートをダウンロードしてインストールしますか?", @@ -3098,6 +3115,8 @@ "remuxVideoMp4CaptionsHint": "字幕はATSC A/53のSEIユーザーデータとしてH.264/H.265動画に格納されます。1つ目の字幕ファイルはCC1、2つ目はCC3になります。動画と音声は再エンコードせず、そのままコピーされます。", "remuxVideoMp4MaxTwoCaptionTracks": "動画内に格納できるクローズドキャプションは最大2トラック(CC1とCC3)です。字幕ファイルを削除し、1つまたは2つだけ残してください。", "remuxVideoMp4CaptionsNeedH264X": "MP4ファイル内のクローズドキャプションにはH.264またはH.265の動画が必要ですが、現在の動画は{0}です。代わりに字幕トラックを選択するか、出力形式に.mpgを指定してください。", + "remuxVideoCaptionOffset": "字幕のズレ:", + "remuxVideoCaptionOffsetHint": "字幕のタイムコードから差し引く値です。放送用の.scc/.mccファイルは、映像がゼロから始まるのに対し、字幕が01:00:00:00から始まることがよくあります。最初の字幕の時刻が映像の終了時刻を超えている場合、ズレの値が提案されます。", "embedSubtitlesDotDotDot": "埋め込み字幕の追加/削除", "syncAudioTrackDotDotDot": "音声トラックの同期...", "syncAudioTrackTitle": "音声トラックの同期", @@ -3403,6 +3422,7 @@ "continuationStyleLeadingTrailingDash": "「―」", "continuationStyleLeadingTrailingDashDots": "続きは「—」 間は「…」", "cpsLineLengthStyle": "読みやすさに合わせた行の 長さ調整", + "cpsLineLengthIgnoreTagsRegex": "カスタムタグを無視(正規表現)", "cpsLineLengthStyleCalcAll": "全ての文字をカウント", "cpsLineLengthStyleCalcNoSpaceCpsOnly": "スペースを除くすべての文字をカウント (CPSのみ)", "cpsLineLengthStyleCalcNoSpace": "スペースを除くすべての文字をカウント", @@ -3625,7 +3645,7 @@ "timeCodeModeFrames": "フレーム(hh:mm:ss:ff)", "timeCodeModeFrameNumbers": "フレーム番号", "frameModeSnapToVideoFrames": "時間を動画のフレームに合わせて保存(各秒内のフレームではなく)", - "textBoxLimitNewLines": "テキストボックスの行数を制限します", + "textBoxLimitNewLines": "テキストボックスの行数を制限", "textBoxAutoWrapWhileTyping": "入力中に自動で折り返す", "mpvOpenGl": "GPU (OpenGL):高速,滑らか", "mpvSoftwareRendering": "CPUのみ:互換性重視(低速)", @@ -3651,6 +3671,10 @@ "subtitleDoubleClickAction": "字幕一覧で ダブルクリックした時の動作", "subtitleGridCenterSelectedRow": "字幕一覧で前/次行選択時に中央表示", "saveAsBehavior": "\"名前を付けて保存\"の動作", + "fileChangedOutsideAction": "別のプログラムによる字幕ファイルの変更", + "fileChangedOutsideReloadWhenUnmodified": "未保存の変更がなければ再読み込みし、ある場合は確認する", + "fileChangedOutsideAlwaysAsk": "常に確認する", + "fileChangedOutsideNever": "何もしない", "defaultSaveLocation": "既定の保存先", "defaultSaveLocationSourceFileFolder": "字幕/動画ファイルのフォルダー", "defaultSaveLocationLastUsedFolder": "前回使用したフォルダー", @@ -3711,6 +3735,7 @@ "textBoxButtonShowColor": "テキスト欄: カラーボタンを表示", "textBoxButtonShowRemoveFormatting": "テキスト欄: 書式設定削除ボタンを表示", "textBoxButtonShowAiAssistant": "テキストボックス: AIアシスタントボタンを表示", + "textBoxButtonShowRecordSpeech": "テキストボックス:音声入力ボタンを表示(マイク入力をテキスト化)", "waveformSingleClickAction": "波形シングルクリックアクション", "waveformDoubleClickAction": "波形ダブルクリックアクション(シングルクリックアクション後)", "allSettings": "すべての設定", @@ -3796,6 +3821,7 @@ "alsoSetVideoPosition": "ビデオ位置も設定", "generalGoToVideoPosition": "ビデオの位置へ移動", "generalToggleItalic": "斜体のオン/オフ", + "generalRecordSpeechToTextBox": "テキストボックスに音声入力(開始/停止)", "generalToggleBold": "太字のオン/オフ", "generalToggleUnderline": "下線のオン/オフ", "generalToggleBox": "ボックスの切り替え(EBU STL)", From a8cd5c9113a345eff4b19a8321f665fff9c1f321 Mon Sep 17 00:00:00 2001 From: niksedk Date: Mon, 19 Oct 2026 21:38:30 +0200 Subject: [PATCH 17/82] iTunes Timed Text: export presets with a template-file header source "Save as iTunes Timed Text" produced a different header for every source format: an ITT source kept its own styling (e.g. tts:color="#EBEBEB"), a TTML source kept its header, and an SRT got the built-in one with white styles, a without a default region and region="bottom" on every line. There was no way to get the same header for every file (#16257). File > Properties for iTunes Timed Text now has: - Presets: a named copy of the whole setup (combo + save/delete like batch convert), stored in Settings.Formats.TimedTextItunesPresets. - Header source: "Current file" (as before) or "Template file" - an .itt/.xml whose head (styles, regions, metadata) and body attributes are used for every saved file; the window lists the template's styles and regions and shows a summary or why the file is rejected. The UI reads the file and hands libse the text (TimedTextItunesHeaderTemplate). - Default color: the color of the built-in header's styles; in current file mode OK recolors the styles that carried the old default color. - Default style/region are persisted and put on the built-in . libse ItunesTimedText.ToText: a template header replaces the source header (frame rate/drop mode follow the current frame rate, the language setting wins when set, unknown paragraph styles are dropped), and a line only names its region when it differs from the body's - with only top-aligned lines get region="top". Co-Authored-By: Claude Fable 5.1 --- src/libse/Settings/SubtitleSettings.cs | 20 + src/libse/SubtitleFormats/ItunesTimedText.cs | 368 +++++++++--- src/ui/Assets/Languages/English.json | 14 +- .../ItunesTimedTextPropertiesViewModel.cs | 544 +++++++++++++++--- .../ItunesTimedTextPropertiesWindow.cs | 53 +- .../Config/Language/File/LanguageFile.cs | 24 + src/ui/Logic/Config/Se.cs | 4 + src/ui/Logic/Config/SeFormats.cs | 23 +- .../Logic/Config/SeItunesTimedTextPreset.cs | 61 ++ .../Files/ItunesTimedTextPropertiesTests.cs | 233 ++++++++ .../ItunesTimedTextPresetTest.cs | 288 ++++++++++ 11 files changed, 1465 insertions(+), 167 deletions(-) create mode 100644 src/ui/Logic/Config/SeItunesTimedTextPreset.cs create mode 100644 tests/UI/Features/Files/ItunesTimedTextPropertiesTests.cs create mode 100644 tests/libse/SubtitleFormats/ItunesTimedTextPresetTest.cs diff --git a/src/libse/Settings/SubtitleSettings.cs b/src/libse/Settings/SubtitleSettings.cs index 833ef99d792..0d872f064fd 100644 --- a/src/libse/Settings/SubtitleSettings.cs +++ b/src/libse/Settings/SubtitleSettings.cs @@ -77,6 +77,22 @@ public class SubtitleSettings public string TimedTextItunesTimeCodeFormat { get; set; } public string TimedTextItunesStyleAttribute { get; set; } public string TimedTextItunesLanguage { get; set; } + + /// tts:color for the styles of the built-in iTunes Timed Text header (when no header is carried over). + public string TimedTextItunesDefaultColor { get; set; } + + /// Style id written on <body> (empty = none). With a template header it overrides the template's own. + public string TimedTextItunesDefaultStyle { get; set; } + + /// Region id written on <body> (empty = none). With a template header it overrides the template's own. + public string TimedTextItunesDefaultRegion { get; set; } + + /// + /// A complete TTML document whose head (styling, layout, metadata) and body attributes are + /// used for every saved iTunes Timed Text file instead of the source file's header. Empty = + /// keep the source header (or the built-in one). The UI loads it from the user's template file. + /// + public string TimedTextItunesHeaderTemplate { get; set; } public string TimedTextImsc11TimeCodeFormat { get; set; } public string TimedTextImsc11FileExtension { get; set; } @@ -166,6 +182,10 @@ public SubtitleSettings() TimedTextItunesBottomExtent = "100% 15%"; TimedTextItunesTimeCodeFormat = "Frames"; TimedTextItunesStyleAttribute = "tts:fontStyle"; + TimedTextItunesDefaultColor = "white"; + TimedTextItunesDefaultStyle = string.Empty; + TimedTextItunesDefaultRegion = string.Empty; + TimedTextItunesHeaderTemplate = string.Empty; TimedTextImsc11TimeCodeFormat = "hh:mm:ss.ms"; TimedTextImsc11FileExtension = ".xml"; diff --git a/src/libse/SubtitleFormats/ItunesTimedText.cs b/src/libse/SubtitleFormats/ItunesTimedText.cs index b6f3dbc4434..b66f36212dd 100644 --- a/src/libse/SubtitleFormats/ItunesTimedText.cs +++ b/src/libse/SubtitleFormats/ItunesTimedText.cs @@ -1,4 +1,5 @@ using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.Settings; using System; using System.Collections.Generic; using System.Linq; @@ -35,9 +36,17 @@ public override bool IsMine(List lines, string fileName) public override string ToText(Subtitle subtitle, string title) { + var settings = Configuration.Settings.SubtitleSettings; + + // A template header (File > Properties > "Header source: template file") replaces whatever + // header the source file carries, so every saved .itt gets the same styling/layout (#16257). + var templateXml = LoadHeaderTemplate(settings.TimedTextItunesHeaderTemplate); + var useTemplate = templateXml != null; + var sourceIsSubStationAlpha = subtitle.Header != null && (subtitle.Header.Contains("[V4+ Styles]") || subtitle.Header.Contains("[V4 Styles]")); + XmlNode styleHead = null; var convertedFromSubStationAlpha = false; - if (subtitle.Header != null) + if (!useTemplate && subtitle.Header != null) { try { @@ -91,11 +100,7 @@ public override string ToText(Subtitle subtitle, string title) } var xml = new XmlDocument { XmlResolver = null }; - var nsmgr = new XmlNamespaceManager(xml.NameTable); - nsmgr.AddNamespace("ttml", "http://www.w3.org/ns/ttml"); - nsmgr.AddNamespace("ttp", "http://www.w3.org/ns/10/ttml#parameter"); - nsmgr.AddNamespace("tts", "http://www.w3.org/ns/ttml#styling"); - nsmgr.AddNamespace("ttm", "http://www.w3.org/ns/10/ttml#metadata"); + var nsmgr = MakeNamespaceManager(xml); var frameRate = ((int)Math.Round(Configuration.Settings.General.CurrentFrameRate)).ToString(); var frameRateMultiplier = "999 1000"; @@ -110,30 +115,60 @@ public override string ToText(Subtitle subtitle, string title) } var language = LanguageAutoDetect.AutoDetectGoogleLanguage(subtitle); - if (!string.IsNullOrEmpty(Configuration.Settings.SubtitleSettings.TimedTextItunesLanguage)) + if (!string.IsNullOrEmpty(settings.TimedTextItunesLanguage)) + { + language = settings.TimedTextItunesLanguage; + } + + var defaultColor = XmlAttributeValueOrDefault(settings.TimedTextItunesDefaultColor, "white"); + var bodyAttributes = string.Empty; + if (BuiltInStyles.Contains(settings.TimedTextItunesDefaultStyle ?? string.Empty)) + { + bodyAttributes += " style=\"" + settings.TimedTextItunesDefaultStyle + "\""; + } + + if (BuiltInRegions.Contains(settings.TimedTextItunesDefaultRegion ?? string.Empty)) { - language = Configuration.Settings.SubtitleSettings.TimedTextItunesLanguage; + bodyAttributes += " region=\"" + settings.TimedTextItunesDefaultRegion + "\""; } var xmlStructure = "" + Environment.NewLine + "" + Environment.NewLine + " " + Environment.NewLine + " " + Environment.NewLine + - "