render.py
Language Python3.2 KB88 lines
"""Writers: .txt (one timestamped line per sentence), .md for the vault,.json with everything, and .srt/.vtt subtitles."""from __future__ import annotationsimport jsonfrom .shape import turnsFORMATS = ("txt", "md", "json", "srt", "vtt")def _pad(n: int, w: int = 2) -> str:return str(n).zfill(w)def stamp(seconds: float) -> str:"""[HH:MM:SS], floored."""total = max(0, int(seconds))return f"[{_pad(total // 3600)}:{_pad(total // 60 % 60)}:{_pad(total % 60)}]"def _clock(seconds: float, sep: str) -> str:ms_total = max(0, round(seconds * 1000))ms, s = ms_total % 1000, ms_total // 1000return f"{_pad(s // 3600)}:{_pad(s // 60 % 60)}:{_pad(s % 60)}{sep}{_pad(ms, 3)}"def to_txt(sentences: list[dict], timestamps: bool = True) -> str:"""One sentence per line: '[00:00:02] Speaker 1: text' (label omitted when unknown)."""lines = []for s in sentences:parts = []if timestamps:parts.append(stamp(s["start"]))parts.append(f"{s['speaker']}: {s['text']}" if s.get("speaker") else s["text"])lines.append(" ".join(parts))return "\n".join(lines) + ("\n" if lines else "")def _yaml_str(v: str) -> str:return json.dumps(v, ensure_ascii=False)def to_markdown(sentences: list[dict], meta: dict) -> str:"""A vault note: frontmatter, then speaker turns as paragraphs."""fm = ["---", "type: transcript",f"title: {_yaml_str(meta['title'])}",f"recorded: {meta['recorded']}",f"transcribed: {meta['transcribed']}",f"duration: {_yaml_str(meta['duration'])}",f"engine: {meta['engine']}",f"language: {meta.get('language') or 'unknown'}",f"source: {_yaml_str(meta['source'])}"]if meta.get("speakers"):fm.append("speakers:")fm += [f" - {_yaml_str(sp)}" for sp in meta["speakers"]]fm += ["tags:", " - transcript", "---", ""]body = [f"# {meta['title']}", ""]for t in turns(sentences):head = f"**{t['speaker']}** {stamp(t['start'])}" if t["speaker"] else stamp(t["start"])body += [head, " ".join(s["text"] for s in t["sentences"]), ""]return "\n".join(fm + body).rstrip() + "\n"def to_json(sentences: list[dict], result: dict, meta: dict) -> str:data = {"meta": meta, "sentences": sentences, "segments": result.get("segments", [])}return json.dumps(data, indent=2, ensure_ascii=False, default=str) + "\n"def _cues(sentences: list[dict]):for i, s in enumerate(sentences):end = s["end"]if end <= s["start"]:nxt = sentences[i + 1]["start"] if i + 1 < len(sentences) else s["start"] + 2.0end = max(nxt, s["start"] + 0.5)text = f"{s['speaker']}: {s['text']}" if s.get("speaker") else s["text"]yield s["start"], end, textdef to_srt(sentences: list[dict]) -> str:return "\n".join(f"{i}\n{_clock(a, ',')} --> {_clock(b, ',')}\n{t}\n"for i, (a, b, t) in enumerate(_cues(sentences), 1))def to_vtt(sentences: list[dict]) -> str:return "WEBVTT\n\n" + "\n".join(f"{_clock(a, '.')} --> {_clock(b, '.')}\n{t}\n"for a, b, t in _cues(sentences))