118 lines
4.2 KiB
Python
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()
|