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

  1. Reformat each offending line to `Speaker: text` or `Speaker | side | text` using an ASCII or fullwidth colon.
  2. Prefix stage directions with '#' so they are treated as comments and skipped, or remove them from the transcript.
  3. Shorten speaker names to <=30 characters; rename long handles.
  4. Move embedded timestamps out of the speaker field (e.g. 'Alice: 12:30 hi' -> 'Alice: hi').
  5. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/06610c93e447e94a. Report an issue: GitHub.