#!/usr/bin/env python3 """Read an episode's creator documents into the structures the dashboard shows. The dashboard never parses Markdown in the browser. Everything here reads the same documents the checker reads, with the checker's own patterns wherever one exists, so the dashboard and ``creator_markdown_check.py`` cannot disagree about what a shot, a reference slot or a continuity lock is. Two documents belong to other stages: the screenplay grammar is owned by the write stage's index builder and the cut list by the edit tool. Skills install one at a time, so this module does not import them; it keeps a minimal reader of each and ``tests/test_dashboard_server.py`` holds both to the canonical parser on the same inputs. Every reader is tolerant: a section it cannot read comes back as ``None`` with one sentence the creator can act on, and the view falls back to the raw text. Nothing here reads files; the server hands in text it read safely. """ from __future__ import annotations import importlib.util import re import sys from pathlib import Path from types import ModuleType from typing import Any, Callable, Optional MINIMUM_PYTHON = (3, 9) if sys.version_info < MINIMUM_PYTHON: raise SystemExit( "short-drama needs Python {}.{} or newer; this interpreter is {}.{}".format( *MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor ) ) def _load_checker() -> ModuleType: path = Path(__file__).resolve().with_name("creator_markdown_check.py") spec = importlib.util.spec_from_file_location("creator_views_checker", path) if spec is None or spec.loader is None: raise RuntimeError(f"cannot load the creator document checker: {path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module check = _load_checker() SCREENPLAY = "剧本.md" SETTINGS = "视觉设定.md" STORYBOARD = "分镜.md" IMAGE_PROMPTS = "图片提示词.md" VIDEO_PROMPTS = "视频提示词.md" CUT_LIST = "剪辑单.md" CREATOR_DOCUMENTS = (SCREENPLAY, SETTINGS, STORYBOARD, IMAGE_PROMPTS, VIDEO_PROMPTS) # Where the edit stage writes the finished film, relative to the episode. FILM = "制作成果/成片/成片.mp4" # ------------------------------------------------------------------ shot size # Ordered far to close. 中近景 is its own rung between 中景 and 近景. SCALES = ("远景", "全景", "中景", "中近景", "近景", "特写", "大特写", "细节") # The rhythm profile's close_shot_share_min counts exactly these (develop's # episode-design reference): 近景、特写、大特写与细节镜头. 中近景 is not one. CLOSE_SCALES = frozenset({"近景", "特写", "大特写", "细节"}) _SCALE_RE = re.compile("大特写|中近景|远景|全景|中景|近景|特写|细节") def shot_scale(framing: str) -> str: """The first shot-size word in a 景别/机位 value, or "" when none is named. Longer words are tried first at each position, so 中近景 and 大特写 are read whole instead of as the 近景 or 特写 inside them. """ match = _SCALE_RE.search(framing or "") return match.group(0) if match else "" # ------------------------------------------------------------------ screenplay # The write stage's grammar (screenplay_index.py): one scene heading per scene, # one paragraph per block, production tags in half-width brackets. _EPISODE = r"EP(?:[0-9]{3}|[1-9][0-9]{3,})" # The same spelling project_tool.py's EPISODE_ID_RE enforces on 剧集//. EPISODE_ID_RE = re.compile(_EPISODE) TITLE_RE = re.compile(r"^# (?:" + _EPISODE + r")?\s*(?:·\s*)?(.*)$") # Same heading grammar as the writing stage's index, season prefix included # (`S01-EP001-SC001`), so a screenplay the index accepts never drops scenes here. SCENE_RE = re.compile( r"^## (?P(?:[A-Za-z0-9]+-)*?" + _EPISODE + r"-SC[0-9]{3}) " r"(?P内外|内|外) · (?P\S(?:.*\S)?) · (?P