{"record":{"id":"06610c93e447e94a","repo":"nexu-io/open-design","slug":"cannot-parse-line-line","errorCode":null,"errorMessage":"Cannot parse line: {line}","messagePattern":"Cannot parse line: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":75,"sourceCode":"        line = raw_line.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        meta_match = re.match(r\"^(title|time|start|gap|hold)\\s*[:：]\\s*(.+)$\", line, flags=re.IGNORECASE)\n        if meta_match:\n            key = meta_match.group(1).strip().lower()\n            value = meta_match.group(2).strip()\n            metadata[key] = int(value) if key in {\"start\", \"gap\", \"hold\"} else value\n            continue\n        raw_messages.append(parse_message_line(line))\n    return {\"metadata\": metadata, \"messages\": raw_messages}\n\n\ndef parse_message_line(line: str) -> dict:\n    parts = [part.strip() for part in line.split(\"|\")] if \"|\" in line else []\n    if not parts:\n        match = re.match(r\"^([^:：]{1,30})[:：]\\s*(.+)$\", line)\n        if not match:\n            raise ValueError(f\"Cannot parse line: {line}\")\n        parts = [match.group(1).strip(), match.group(2).strip()]\n    result = {\"speaker\": parts[0], \"side\": None, \"avatar\": None, \"text\": \"\", \"highlight\": False}\n    if len(parts) == 2:\n        result[\"text\"] = parts[1]\n        return result\n    if len(parts) == 3:\n        if is_flag(parts[2]):\n            result[\"text\"] = parts[1]\n            result[\"highlight\"] = True\n            return result\n        if is_side(parts[1]):\n            result[\"side\"] = SIDE_MAP[parts[1]]\n            result[\"text\"] = parts[2]\n            return result\n        result[\"text\"] = parts[1]\n        result[\"avatar\"] = parts[2]\n        return result\n    if len(parts) >= 4 and is_side(parts[1]):","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L57-L93","documentation":"Raised by parse_message_line in build_chat_overlay_spec.py when a transcript line has no '|' separator AND does not match the speaker:text regex `^([^:：]{1,30})[:：]\\s*(.+)$`. The regex requires a speaker (1-30 chars, no colon) followed by an ASCII or fullwidth colon and non-empty body. Lines that are pure prose, contain only a colon, or have a speaker longer than 30 chars are rejected.","triggerScenarios":"Feeding a transcript file where a line is a stage direction, a paragraph of body text with no speaker, an empty/whitespace line that slipped past the blank-line filter, a speaker name longer than 30 characters, or a line whose only colon is part of a URL/time ('https://', '12:30').","commonSituations":"User pastes a raw chat export with system notices ('--- User joined ---'); non-Latin speaker names that include punctuation pushing length over 30; timestamps embedded as 'speaker 12:34: hi'; markdown bullets leaking through; the blank-line skip ('if not line') bypassed by whitespace-only lines that strip to empty only after the check.","solutions":["Reformat each offending line to `Speaker: text` or `Speaker | side | text` using an ASCII or fullwidth colon.","Prefix stage directions with '#' so they are treated as comments and skipped, or remove them from the transcript.","Shorten speaker names to <=30 characters; rename long handles.","Move embedded timestamps out of the speaker field (e.g. 'Alice: 12:30 hi' -> 'Alice: hi').","If a line legitimately has no speaker, decide whether to drop it or assign a speaker before re-running."],"exampleFix":"// before\nAlice said something really long with no colon anywhere\n--- Bob joined ---\n# -> ValueError: Cannot parse line: ...\n\n// after\nAlice: hello there\n# Bob joined (commented out)\nBob: hi Alice","handlingStrategy":"validation","validationCode":"import re\n\nSPEAKER_RE = re.compile(r\"^([^:：]{1,30})[:：]\\s*(.+)$\")\n\ndef looks_like_message(line: str) -> bool:\n    line = line.strip()\n    if not line or line.startswith(\"#\"):\n        return False\n    if \"|\" in line:\n        return len([p for p in line.split(\"|\") if p.strip()]) >= 2\n    return bool(SPEAKER_RE.match(line))\n\nbad = [ln for ln in transcript_lines if not looks_like_message(ln)]\nif bad:\n    raise SystemExit(f\"Unparseable transcript lines: {bad[:3]}\")","typeGuard":null,"tryCatchPattern":"try:\n    parsed = parse_transcript(Path(args.input))\nexcept ValueError as exc:\n    raise SystemExit(f\"Transcript parse error: {exc}\") from exc","preventionTips":["Document the accepted line shapes (speaker:text, speaker|text, speaker|side|text, speaker|side|avatar|text[|highlight]).","Strip comments (lines starting with '#') and blank lines before parsing.","Keep speaker names <=30 chars and free of colons.","Run a dry validator over transcripts before handing them to the renderer."],"tags":["parsing","validation","transcript","chat-overlay","regex"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}