SubtitleEdit/subtitleedit · error · IOException

Kokoro TTS {label} download failed integrity check (expected

Error message

Kokoro TTS {label} download failed integrity check (expected SHA-256 {expected}, got {actual}).

What it means

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.

Source

Thrown at src/ui/Logic/Download/KokoroTtsCppDownloadService.cs:69

    {
        if (string.IsNullOrEmpty(key) || stream.Length == 0)
        {
            return;
        }

        var expected = DownloadHashManager.GetLatestKnownHash(key);
        if (string.IsNullOrEmpty(expected))
        {
            return;
        }

        stream.Position = 0;
        var actual = await Sha256Util.ComputeSha256Async(stream, cancellationToken);
        stream.Position = 0;

        if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
        {
            throw new IOException(
                $"Kokoro TTS {label} download failed integrity check (expected SHA-256 {expected}, got {actual}).");
        }
    }

    public async Task DownloadModels(string modelsFolder, IProgress<float>? progress, Action<string>? titleProgress, CancellationToken cancellationToken)
    {
        var ttsPath    = Path.Combine(modelsFolder, TtsModelFileName);
        var voicesPath = Path.Combine(modelsFolder, VoicesModelFileName);
        var needTts    = !File.Exists(ttsPath);
        var needVoices = !File.Exists(voicesPath);
        var total      = (needTts ? 1 : 0) + (needVoices ? 1 : 0);
        var step       = 0;

        if (needTts)
        {
            step++;
            titleProgress?.Invoke($"Downloading Kokoro TTS models ({step}/{total}): {TtsModelFileName}");
            await DownloadHelper.DownloadFileAsync(_httpClient, TtsModelUrl, ttsPath, progress, cancellationToken);

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Clear the cached download so the service re-fetches the bytes matching the manager's hash.
  2. Confirm DownloadHashManager.GetLatestKnownHash(key) points at the hash of the artifact currently served upstream; refresh/correct the manager if a rolling update mis-fired.
  3. Re-download over a clean network path to rule out proxy/AV corruption.
  4. If rolling hashes are intentional and your local copy is valid-but-old, force a full re-download rather than skipping verification.

Example fix

// before
await kokoroService.DownloadModels(folder, progress, titleProgress, ct);

// after: purge cached tts+voices so hashes match the manager
foreach (var f in new[]{ KokoroTtsCppDownloadService.TtsModelFileName, KokoroTtsCppDownloadService.VoicesModelFileName })
{
    var p = Path.Combine(folder, f);
    if (File.Exists(p)) File.Delete(p);
}
await kokoroService.DownloadModels(folder, progress, titleProgress, ct);
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check: skip if no known hash; otherwise the verify IS the check
var expected = DownloadHashManager.GetLatestKnownHash(key);
if (string.IsNullOrEmpty(expected)) { /* verification skipped */ }

Try / catch

try { await kokoroService.VerifyAsync(stream, label, key, ct); }
catch (IOException ex) when (ex.Message.Contains("failed integrity check"))
{ stream.SetLength(0); await kokoroService.DownloadModels(folder, progress, titleProgress, ct); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13). Data as JSON: /api/errors/71541a531f7c82ff. Report an issue: GitHub.