{"record":{"id":"71541a531f7c82ff","repo":"SubtitleEdit/subtitleedit","slug":"kokoro-tts-label-download-failed-integrity-check","errorCode":null,"errorMessage":"Kokoro TTS {label} download failed integrity check (expected SHA-256 {expected}, got {actual}).","messagePattern":"Kokoro TTS (.+?) download failed integrity check \\(expected SHA-256 (.+?), got (.+?)\\)\\.","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"src/ui/Logic/Download/KokoroTtsCppDownloadService.cs","lineNumber":69,"sourceCode":"    {\n        if (string.IsNullOrEmpty(key) || stream.Length == 0)\n        {\n            return;\n        }\n\n        var expected = DownloadHashManager.GetLatestKnownHash(key);\n        if (string.IsNullOrEmpty(expected))\n        {\n            return;\n        }\n\n        stream.Position = 0;\n        var actual = await Sha256Util.ComputeSha256Async(stream, cancellationToken);\n        stream.Position = 0;\n\n        if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))\n        {\n            throw new IOException(\n                $\"Kokoro TTS {label} download failed integrity check (expected SHA-256 {expected}, got {actual}).\");\n        }\n    }\n\n    public async Task DownloadModels(string modelsFolder, IProgress<float>? progress, Action<string>? titleProgress, CancellationToken cancellationToken)\n    {\n        var ttsPath    = Path.Combine(modelsFolder, TtsModelFileName);\n        var voicesPath = Path.Combine(modelsFolder, VoicesModelFileName);\n        var needTts    = !File.Exists(ttsPath);\n        var needVoices = !File.Exists(voicesPath);\n        var total      = (needTts ? 1 : 0) + (needVoices ? 1 : 0);\n        var step       = 0;\n\n        if (needTts)\n        {\n            step++;\n            titleProgress?.Invoke($\"Downloading Kokoro TTS models ({step}/{total}): {TtsModelFileName}\");\n            await DownloadHelper.DownloadFileAsync(_httpClient, TtsModelUrl, ttsPath, progress, cancellationToken);","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Logic/Download/KokoroTtsCppDownloadService.cs#L51-L87","documentation":"Thrown by KokoroTtsCppDownloadService's integrity check. Unlike the CrispASR services, the expected hash is fetched dynamically via DownloadHashManager.GetLatestKnownHash(key); if empty, verification is skipped. When non-empty, the SHA-256 of the supplied stream is compared case-insensitively — a mismatch raises IOException. The stream Position is rewound to 0 both before and after hashing so the caller can continue using the stream.","triggerScenarios":"A non-empty latest-known hash exists for the Kokoro TTS download key, and the stream's actual SHA-256 differs: corrupt/truncated download, mirror returned wrong bytes, MITM, or DownloadHashManager's hash advanced to a new build while the local cache holds the old one.","commonSituations":"DownloadHashManager was updated (rolling hash) to track a newer Kokoro build, but the old bytes are still cached; partial download reported success; AV stripped bytes; proxy rewriting the body. Because hashes come from a manager, a stale manager cache pointing at the wrong build also trips this.","solutions":["Clear the cached download so the service re-fetches the bytes matching the manager's hash.","Confirm DownloadHashManager.GetLatestKnownHash(key) points at the hash of the artifact currently served upstream; refresh/correct the manager if a rolling update mis-fired.","Re-download over a clean network path to rule out proxy/AV corruption.","If rolling hashes are intentional and your local copy is valid-but-old, force a full re-download rather than skipping verification."],"exampleFix":"// before\nawait kokoroService.DownloadModels(folder, progress, titleProgress, ct);\n\n// after: purge cached tts+voices so hashes match the manager\nforeach (var f in new[]{ KokoroTtsCppDownloadService.TtsModelFileName, KokoroTtsCppDownloadService.VoicesModelFileName })\n{\n    var p = Path.Combine(folder, f);\n    if (File.Exists(p)) File.Delete(p);\n}\nawait kokoroService.DownloadModels(folder, progress, titleProgress, ct);","handlingStrategy":"try-catch","validationCode":"// Pre-check: skip if no known hash; otherwise the verify IS the check\nvar expected = DownloadHashManager.GetLatestKnownHash(key);\nif (string.IsNullOrEmpty(expected)) { /* verification skipped */ }","typeGuard":null,"tryCatchPattern":"try { await kokoroService.VerifyAsync(stream, label, key, ct); }\ncatch (IOException ex) when (ex.Message.Contains(\"failed integrity check\"))\n{ stream.SetLength(0); await kokoroService.DownloadModels(folder, progress, titleProgress, ct); }","preventionTips":["When DownloadHashManager rolls a hash forward, also purge the local cache.","Log the manager's current hash alongside the actual on mismatch.","Treat rolling-hash mismatches as 're-download required', not 'corruption'."],"tags":["integrity","sha256","kokoro","tts","download","corruption","rolling-hash"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}