Summary
If a TTSKit model download is interrupted partway (app backgrounded/killed, network drop), the partially-downloaded folder permanently poisons the cache: every subsequent setupModels/TTSKit.download call short-circuits on a local-cache check that accepts any non-empty directory as complete, so the missing files are never fetched and loadModels() fails with Core ML compile errors forever. The only way out is manually deleting the model folder.
Environment
- argmax-oss-swift 1.0.0 (
25c6299), product TTSKit
- iPhone 17 Pro, iOS 27 beta
- Default config (
.qwen3TTS_0_6b, repo argmaxinc/ttskit-coreml), foreground download session
Repro
- Start a fresh model download (
setupModels(download: true) or first generate()).
- Kill or background the app mid-download (foreground
URLSession, so transfers stop).
- Relaunch and load again.
Expected: the missing files are detected and re-downloaded (the per-file etag/existence checks in HubApi.snapshot handle exactly this).
Actual: resolveRepo returns the partial folder as "cached", modelState proceeds to loading, and loadModels() fails:
Failed to open file: …/qwen3_tts/code_decoder/12hz-0.6b-customvoice/W8A16-stateful/CodeDecoder.mlmodelc/coremldata.bin. It is not a valid .mlmodelc file.
E5RT: Input model path not found: …/qwen3_tts/text_projector/12hz-0.6b-customvoice/W8A16/TextProjector.mlmodelc/model.mil (1)
Failed to parse ML Program. It is likely an invalid or broken model.
On-device contents of TextProjector.mlmodelc after the interrupted download — the small files landed, model.mil and weights/ never did:
analytics/coremldata.bin 243 B
coremldata.bin 380 B
metadata.json 2 KB
(model.mil missing — 6.5 KB in repo)
(weights/weight.bin missing — 317 MB in repo)
Root cause
ModelDownloader.resolveRepo short-circuits before HubApi.snapshot ever runs:
if patternsExistLocally(patterns, in: localRoot) {
Logging.debug("[ModelDownloader] All models found in local cache at \(localRoot.path)")
return localRoot
}
and patternsExistLocally only checks that each pattern's concrete directory prefix exists and is non-empty:
guard FileManager.default.fileExists(atPath: dir.path) else { return false }
let contents = (try? FileManager.default.contentsOfDirectory(atPath: dir.path)) ?? []
return !contents.isEmpty
A folder holding one stray metadata.json passes. Since HubApi.snapshot is never reached, its per-file downloaded/etag verification — which would repair the gaps — never runs.
Suggested fix
Any of these would resolve it:
- Drop the short-circuit and always call
snapshot when download == true. When the cache is genuinely complete, snapshot is cheap — every file skips via the existing metadata/etag check — so the fast path is mostly preserved at the cost of one tree-listing request (and it picks up upstream revisions as a bonus).
- Keep the offline fast path but make
patternsExistLocally verify actual completeness (compare against the expected component file set, e.g. model.mil + coremldata.bin + weights/ per .mlmodelc, or persist the matched file list from the last successful snapshot and check it).
- At minimum: on
loadModels() failure due to missing/invalid component files, invalidate the cached folder so the next setupModels re-downloads, instead of failing identically forever.
Workaround
Delete the model folder (Documents/huggingface/models/argmaxinc/ttskit-coreml) and re-download; we now also wrap loadModels() and wipe + refetch on failure in our integration.
Happy to PR option 1 or 2 if you have a preference.
Summary
If a TTSKit model download is interrupted partway (app backgrounded/killed, network drop), the partially-downloaded folder permanently poisons the cache: every subsequent
setupModels/TTSKit.downloadcall short-circuits on a local-cache check that accepts any non-empty directory as complete, so the missing files are never fetched andloadModels()fails with Core ML compile errors forever. The only way out is manually deleting the model folder.Environment
25c6299), productTTSKit.qwen3TTS_0_6b, repoargmaxinc/ttskit-coreml), foreground download sessionRepro
setupModels(download: true)or firstgenerate()).URLSession, so transfers stop).Expected: the missing files are detected and re-downloaded (the per-file etag/existence checks in
HubApi.snapshothandle exactly this).Actual:
resolveReporeturns the partial folder as "cached",modelStateproceeds to loading, andloadModels()fails:On-device contents of
TextProjector.mlmodelcafter the interrupted download — the small files landed,model.milandweights/never did:Root cause
ModelDownloader.resolveReposhort-circuits beforeHubApi.snapshotever runs:and
patternsExistLocallyonly checks that each pattern's concrete directory prefix exists and is non-empty:A folder holding one stray
metadata.jsonpasses. SinceHubApi.snapshotis never reached, its per-filedownloaded/etag verification — which would repair the gaps — never runs.Suggested fix
Any of these would resolve it:
snapshotwhendownload == true. When the cache is genuinely complete, snapshot is cheap — every file skips via the existing metadata/etag check — so the fast path is mostly preserved at the cost of one tree-listing request (and it picks up upstream revisions as a bonus).patternsExistLocallyverify actual completeness (compare against the expected component file set, e.g.model.mil+coremldata.bin+weights/per.mlmodelc, or persist the matched file list from the last successful snapshot and check it).loadModels()failure due to missing/invalid component files, invalidate the cached folder so the nextsetupModelsre-downloads, instead of failing identically forever.Workaround
Delete the model folder (
Documents/huggingface/models/argmaxinc/ttskit-coreml) and re-download; we now also wraploadModels()and wipe + refetch on failure in our integration.Happy to PR option 1 or 2 if you have a preference.