{"record":{"id":"55cec5ea07630d03","repo":"unslothai/unsloth","slug":"no-captioned-images-found-provide-a-metadata-json","errorCode":null,"errorMessage":"No captioned images found. Provide a metadata.jsonl / captions.jsonl, per-image .txt captions, or an instance prompt.","messagePattern":"No captioned images found\\. Provide a metadata\\.jsonl / captions\\.jsonl, per-image \\.txt captions, or an instance prompt\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"studio/backend/core/training/diffusion_train_common.py","lineNumber":1451,"sourceCode":"        # 3. dreambooth instance prompt for any image still without a caption.\n        if not caption and instance_prompt:\n            caption = instance_prompt\n        if caption:\n            if verify_images:\n                # Reject a corrupt/truncated image now via a cheap PIL header probe: otherwise it passes filename-only discovery, the start route frees the GPU models, and the trainer crashes in Image.open.\n                try:\n                    from PIL import Image\n                    with Image.open(img) as _probe:\n                        _probe.verify()\n                except Exception as e:  # noqa: BLE001 -- corrupt/zero-byte/truncated file\n                    raise ValueError(\n                        f\"Image cannot be decoded: {img.name} ({e}). Remove or replace the \"\n                        f\"corrupt or zero-byte file before training.\"\n                    ) from e\n            pairs.append((str(img), caption))\n\n    if not pairs:\n        raise ValueError(\n            \"No captioned images found. Provide a metadata.jsonl / captions.jsonl, per-image \"\n            \".txt captions, or an instance prompt.\"\n        )\n    return pairs\n\n\n# Families whose trainer has no checkpoint/resume support yet. The shared DiffusionLoraConfig\n# carries save_steps / resume_from_checkpoint for every family, so a loop that implements\n# neither has to say so rather than ignore them.\nCHECKPOINTLESS_FAMILIES: frozenset[str] = frozenset({\"minimax-h3\"})\n\n# Families whose forward covers ONE packed sequence, so the batch axis is a pure replication\n# axis and a second clip cannot join it: the layout, the rotary grid and the row timesteps are\n# set by that clip's geometry and its caption's length. Kept beside the refusal it explains.\nSINGLE_SEQUENCE_FAMILIES: frozenset[str] = frozenset({\"minimax-h3\"})\n\n# Families whose trainer loads its base through ``ModularPipeline.from_pretrained``. Their local\n# layout is ``modular_model_index.json`` and no ``model_index.json``, so the conventional shape","sourceCodeStart":1433,"sourceCodeEnd":1469,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/training/diffusion_train_common.py#L1433-L1469","documentation":"After scanning metadata.jsonl/captions.jsonl, per-image .txt sidecars, and the dreambooth instance_prompt fallback, zero image/caption pairs were produced. Every image lacked a caption from every source, or the directory had no recognized image files at all, so the trainer refuses to start with an empty dataset.","triggerScenarios":"A data_dir with images but no metadata.jsonl, no sidecar .txt files, and no instance_prompt; or a data_dir whose files use an extension outside _IMAGE_EXTS; or a metadata.jsonl whose keys don't match the images (bad-upload records are skipped, not fatal).","commonSituations":"User uploads bare images expecting automatic captioning; sidecar files named image-caption.txt instead of image.txt; metadata.jsonl file_name fields with wrong paths/extensions; forgetting to set instance_prompt for dreambooth-style datasets.","solutions":["Add a metadata.jsonl (or captions.jsonl) with {\"file_name\": \"img.png\", \"prompt\": \"...\"} records.","Or add a per-image caption sidecar: img.png + img.txt in the same directory.","Or set an instance_prompt on the run so every uncaptioned image falls back to it.","Verify image extensions are within the recognized set and metadata file_name values exactly match the image filenames."],"exampleFix":"# before: data_dir has only images\n# after: add captions.jsonl in data_dir\n{\"file_name\": \"img_0001.png\", \"prompt\": \"a photo of sks dog\"}\n{\"file_name\": \"img_0002.png\", \"prompt\": \"a photo of sks dog at the beach\"}","handlingStrategy":"validation","validationCode":"IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp'}\ndef dataset_is_captionable(root: Path, instance_prompt=None) -> bool:\n    imgs = [p for p in root.iterdir() if p.is_file() and p.suffix.lower() in IMAGE_EXTS]\n    if not imgs:\n        return False\n    has_meta = (root / 'metadata.jsonl').is_file() or (root / 'captions.jsonl').is_file()\n    has_sidecars = any(p.with_suffix('.txt').is_file() for p in imgs)\n    return bool(has_meta or has_sidecars or instance_prompt)","typeGuard":null,"tryCatchPattern":"try:\n    pairs = build_caption_pairs(...)\nexcept ValueError as e:\n    if 'No captioned images' in str(e):\n        prompt_user_for_captions_or_instance_prompt()","preventionTips":["Ship a dataset linter that reports caption coverage per image before training.","Standardize on metadata.jsonl with file_name fields that exactly match filenames (case and extension)."],"tags":["training","dataset","captions","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}