Files
2026-07-13 13:13:17 +08:00

118 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""
topic_doc_sources.py — gather source material for writing a ui: topic's embedded help.
Prints the bundle the write-topic-docs skill distills into a topic's doc: the transcript passages
where the topic is explained (found via the ui: deep-link index, ranked in-depth > explained >
passing, recent first), the implementing class(es) to fact-check against, and any existing .help/
page. Read-only, in-repo.
python topic_doc_sources.py ui:Timeline # one topic
python topic_doc_sources.py # every topic whose doc is still empty
"""
import json
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
HELP = HERE.parent
IDX = HELP / "references" / "indices"
SRTS = HELP / ".tmp" / "video-transcripts"
DOCS = HELP / "docs"
DEPTH_RANK = {"in-depth": 0, "explained": 1, "passing": 2}
WIN_BEFORE, WIN_AFTER = 75, 90 # seconds of transcript to pull around each deep-link
TOP = 5
CUE = re.compile(r"(\d\d):(\d\d):(\d\d),\d+\s*-->")
def date_key(d):
return int(d.replace("-", "")) if d else 0
def srt_cues(path):
"""[(start_seconds, text)] from a final .srt."""
out = []
if not path.exists():
return out
for block in re.split(r"\n\s*\n", path.read_text(encoding="utf-8", errors="replace").strip()):
lines = [l for l in block.splitlines() if l.strip()]
ti = next((i for i, l in enumerate(lines) if "-->" in l), None)
if ti is None:
continue
m = CUE.search(lines[ti])
if m:
s = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
out.append((s, " ".join(lines[ti + 1:]).strip()))
return out
def span_text(vid, t):
cues = srt_cues(SRTS / f"{vid}.srt")
picked = [tx for (s, tx) in cues if t - WIN_BEFORE <= s <= t + WIN_AFTER and tx]
return " ".join(picked)
def time_label(seconds):
seconds = int(seconds or 0)
h, rem = divmod(seconds, 3600)
m, s = divmod(rem, 60)
return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
def main():
# Windows console defaults to cp1252, which can't encode arrows / em-dashes in notes.
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError):
pass
topics = json.loads((IDX / "topics.json").read_text(encoding="utf-8"))["topics"]
mentions = json.loads((IDX / "mentions.json").read_text(encoding="utf-8"))
videos = {v["id"]: v for v in json.loads((IDX / "videos.json").read_text(encoding="utf-8"))["videos"]}
arg = sys.argv[1] if len(sys.argv) > 1 else None
if arg:
key = arg if arg.startswith("ui:") else f"ui:{arg}"
if key not in topics:
sys.exit(f"Unknown topic '{key}'. See ids in {IDX / 'topics.json'}.")
targets = [key]
else:
targets = [k for k, v in topics.items() if not v.get("doc")]
print(f"# {len(targets)} topic(s) with an empty doc\n")
for key in targets:
t = topics[key]
print("=" * 88)
head = f"{key} term: {t['term']} classes: {', '.join(t['classes']) or '(none)'}"
if t.get("parent"):
head += f" parent: {t['parent']}"
print(head)
hits = sorted(p.relative_to(HELP).as_posix() for p in DOCS.rglob("*.md")
if t["term"].replace(" ", "").lower() in p.stem.replace(" ", "").lower())
if hits:
print("existing .help pages:", ", ".join(hits))
ms = list(mentions.get(key, []))
ms.sort(key=lambda m: (DEPTH_RANK.get(m.get("depth"), 3),
-date_key(videos.get(m["video"], {}).get("date"))))
if not ms:
print("video explanations: (none yet — run /analyze-videos with the topic vocabulary first)")
for m in ms[:TOP]:
v = videos.get(m["video"], {})
start = m.get("startSecond", 0)
txt = span_text(m["video"], start)
print(f"\n--- [{m.get('depth')}] {v.get('date', '?')} "
f"\"{(v.get('title') or m['video'])[:60]}\" @ {time_label(start)} {m.get('url', '')}")
if m.get("note"):
print(f" note: {m['note']}")
print(" " + (txt[:1400] if txt else "(transcript span empty)"))
print()
if __name__ == "__main__":
main()