{"record":{"id":"43b13e8b08678e86","repo":"hiyouga/LlamaFactory","slug":"more-segment-tags-than-provided-media-files","errorCode":null,"errorMessage":"More {segment} tags than provided media files.","messagePattern":"More (.+?) tags than provided media files\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llamafactory/v1/plugins/data_plugins/converter.py","lineNumber":111,"sourceCode":"\ndef _to_content_blocks(text: str, media_iters: dict[str, Any]) -> list[Content]:\n    \"\"\"Split ``text`` on inline media placeholders, interleaving media-url content blocks.\n\n    Each placeholder consumes the next path from its modality iterator (in document order). Plain\n    text with no placeholders yields a single text block (byte-identical to the legacy behavior).\n    Raises on an unmatched placeholder (more tags than media files).\n    \"\"\"\n    if not _TAG_PATTERN.search(text):\n        return [{\"type\": \"text\", \"value\": text}]\n\n    blocks: list[Content] = []\n    for segment in _TAG_PATTERN.split(text):\n        block_type = _TAG_TO_BLOCK.get(segment)\n        if block_type is not None:\n            try:\n                path = next(media_iters[segment])\n            except StopIteration:\n                raise ValueError(f\"More {segment} tags than provided media files.\") from None\n            blocks.append({\"type\": block_type, \"value\": path})\n        elif segment:\n            blocks.append({\"type\": \"text\", \"value\": segment})\n    return blocks\n\n\ndef _assert_media_consumed(media_iters: dict[str, Any]) -> None:\n    \"\"\"Ensure every media file was referenced by a tag (fewer tags than media -> error).\"\"\"\n    for tag, media_iter in media_iters.items():\n        unused = len(list(media_iter))\n        if unused:\n            raise ValueError(f\"Fewer {tag} tags than provided media files ({unused} unused).\")\n\n\nclass DataConverterPlugin(BasePlugin):\n    \"\"\"Plugin for data converters.\"\"\"\n\n    def __call__(self, raw_sample: dict[str, Any]) -> Sample:","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/v1/plugins/data_plugins/converter.py#L93-L129","documentation":"During multimodal sample conversion, each inline media placeholder tag (e.g. <image>, <video>, <audio>) in the text consumes one path from the sample's media column. This ValueError fires when the text contains more placeholder tags of a modality than the sample provides media files for that modality, so a tag has no path to bind.","triggerScenarios":"A dataset row whose instruction/output text contains N '<image>' tags but whose 'images' field has fewer than N entries (same for videos/audios). Raised from _to_content_blocks during DataConverterPlugin conversion (alpaca/sharegpt converters).","commonSituations":"Hand-edited JSONL where an image tag was added but the images array not updated; templates that inject an extra media placeholder; OCR/caption datasets with inconsistent tag counts per row.","solutions":["Fix the offending rows: make the number of media tags in the text exactly equal the length of the corresponding images/videos/audios list.","Audit the dataset with a small script counting tags vs list lengths per sample to find bad rows.","Remove stray placeholder tags from text if the media was never intended.","If a single sample uses one media file repeated, duplicate the path in the media list."],"exampleFix":"// before\n{\"instruction\": \"Describe <image> and <image>\", \"output\": \"...\", \"images\": [\"a.jpg\"]}\n\n// after\n{\"instruction\": \"Describe <image> and <image>\", \"output\": \"...\", \"images\": [\"a.jpg\", \"b.jpg\"]}","handlingStrategy":"validation","validationCode":"import json, re\nfrom llamafactory.extras.constants import IMAGE_PLACEHOLDER, VIDEO_PLACEHOLDER, AUDIO_PLACEHOLDER\n\nTAGS = {'images': IMAGE_PLACEHOLDER, 'videos': VIDEO_PLACEHOLDER, 'audios': AUDIO_PLACEHOLDER}\n\ndef sample_ok(row) -> bool:\n    text = ' '.join(str(row.get(k, '')) for k in ('instruction', 'input', 'output', 'conversations'))\n    for col, tag in TAGS.items():\n        media = row.get(col) or []\n        media = media if isinstance(media, list) else [media]\n        if len(re.findall(re.escape(tag), text)) > len(media):\n            return False\n    return True\n\nassert all(sample_ok(json.loads(l)) for l in open('train.jsonl'))","typeGuard":"def has_matching_tag_counts(row: dict) -> bool:\n    \"\"\"True when every media tag in text has a corresponding media file.\"\"\"\n    return sample_ok(row)","tryCatchPattern":"try:\n    sample = converter(raw)\nexcept ValueError as e:\n    if 'tags than provided media' in str(e):\n        logger.error('bad row skipped: %s', raw.get('id', '?'))\n        continue\n    raise","preventionTips":["Run a tag-count vs media-list-length validator over JSONL before training.","Generate placeholder tags programmatically from the media list instead of editing text by hand.","Add a dataset CI check for multimodal datasets."],"tags":["data","multimodal","dataset-conversion","validation"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}