nexu-io/open-design · error · ValueError
Cannot parse line: {line}
Error message
Cannot parse line: {line} What it means
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.
Source
Thrown at skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py:75
line = raw_line.strip()
if not line or line.startswith("#"):
continue
meta_match = re.match(r"^(title|time|start|gap|hold)\s*[::]\s*(.+)$", line, flags=re.IGNORECASE)
if meta_match:
key = meta_match.group(1).strip().lower()
value = meta_match.group(2).strip()
metadata[key] = int(value) if key in {"start", "gap", "hold"} else value
continue
raw_messages.append(parse_message_line(line))
return {"metadata": metadata, "messages": raw_messages}
def parse_message_line(line: str) -> dict:
parts = [part.strip() for part in line.split("|")] if "|" in line else []
if not parts:
match = re.match(r"^([^::]{1,30})[::]\s*(.+)$", line)
if not match:
raise ValueError(f"Cannot parse line: {line}")
parts = [match.group(1).strip(), match.group(2).strip()]
result = {"speaker": parts[0], "side": None, "avatar": None, "text": "", "highlight": False}
if len(parts) == 2:
result["text"] = parts[1]
return result
if len(parts) == 3:
if is_flag(parts[2]):
result["text"] = parts[1]
result["highlight"] = True
return result
if is_side(parts[1]):
result["side"] = SIDE_MAP[parts[1]]
result["text"] = parts[2]
return result
result["text"] = parts[1]
result["avatar"] = parts[2]
return result
if len(parts) >= 4 and is_side(parts[1]):View on GitHub (pinned to 5be4028344)
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.
Example fix
// before Alice said something really long with no colon anywhere --- Bob joined --- # -> ValueError: Cannot parse line: ... // after Alice: hello there # Bob joined (commented out) Bob: hi Alice
Defensive patterns
Strategy: validation
Validate before calling
import re
SPEAKER_RE = re.compile(r"^([^::]{1,30})[::]\s*(.+)$")
def looks_like_message(line: str) -> bool:
line = line.strip()
if not line or line.startswith("#"):
return False
if "|" in line:
return len([p for p in line.split("|") if p.strip()]) >= 2
return bool(SPEAKER_RE.match(line))
bad = [ln for ln in transcript_lines if not looks_like_message(ln)]
if bad:
raise SystemExit(f"Unparseable transcript lines: {bad[:3]}") Try / catch
try:
parsed = parse_transcript(Path(args.input))
except ValueError as exc:
raise SystemExit(f"Transcript parse error: {exc}") from exc Prevention
- 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.
When it happens
Trigger: 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').
Common situations: 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.
Related errors
- Unsupported message line shape: {line}
- invalid zip central directory entry
- EMPTY_TRANSCRIPT
- invalid template binding path: ${binding}
- Unsupported container: {config['container']}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/06610c93e447e94a.
Report an issue: GitHub.