#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ AutoShorts Transcript Editor ============================ Edit transcription results directly in the AutoShorts SQLite DB. The app reads transcripts from the DB on every generate/render, so edits are picked up without restarting or patching the app. Usage: python transcript_edit.py export [--project ] [-o out.txt] python transcript_edit.py import [--project ] python transcript_edit.py list python transcript_edit.py show [--project ] python transcript_edit.py fix "new text" [--project ] Edit flow (recommended): 1) export -> edit the text column in a text editor 2) import -> only changed lines are written back Only the segment *text* is changed; start/end/speaker are preserved. Lines in the edit file that are unchanged or blank are skipped. """ import argparse import json import os import sqlite3 import sys import re sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") DB = r"C:\Users\Admin\AppData\Roaming\com.autoshorts.desktop\autoshorts.sqlite" LINE_RE = re.compile( r"^\[(\d{4})\]\s+(\d{2}:\d{2}\.\d{2})->(\d{2}:\d{2}\.\d{2})\s+\[(S\d+)\]\s+(.*)$" ) def connect(): c = sqlite3.connect(DB) c.execute("PRAGMA busy_timeout=5000") return c def get_latest_project(c): row = c.execute( "SELECT project_id FROM transcripts ORDER BY created_at DESC LIMIT 1" ).fetchone() if not row: sys.exit("no transcripts found") return row[0] def load_transcript(c, pid): row = c.execute( "SELECT raw_json FROM transcripts WHERE project_id=? ORDER BY created_at DESC LIMIT 1", (pid,), ).fetchone() if not row: sys.exit(f"no transcript for project {pid}") return json.loads(row[0]) def save_transcript(c, pid, data): c.execute( "UPDATE transcripts SET raw_json=? WHERE project_id=?", (json.dumps(data, ensure_ascii=False), pid), ) c.commit() def ts(x): m = int(x // 60) s = x - m * 60 return f"{m:02d}:{s:06.2f}" def parse_ts(s): m, sec = s.split(":") return int(m) * 60 + float(sec) def cmd_list(c): rows = c.execute( """SELECT t.project_id, p.name, p.source_path, t.engine, t.language, length(t.raw_json), t.created_at FROM transcripts t LEFT JOIN projects p ON p.id = t.project_id ORDER BY t.created_at DESC""" ).fetchall() if not rows: print("(no transcripts)") return for pid, name, src, eng, lang, ln, ts_ in rows: print(f"{pid}\n name={name} engine={eng} lang={lang} json={ln} created={ts_}\n src={src}") def cmd_export(c, pid, out): data = load_transcript(c, pid) segs = data.get("segments", []) lines = [ f"# transcript project={pid} segments={len(segs)} duration={ts(data.get('duration', 0))}", "# edit the text after the bracket; do not change [idx]/timestamps/speaker", "# lines left unchanged are skipped on import", "#", ] for i, x in enumerate(segs): lines.append( f"[{i:04d}] {ts(x['start'])}->{ts(x['end'])} [{x.get('speaker', 'S?')}] {x['text']}" ) with open(out, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") print(f"exported {len(segs)} segments to {out}") def cmd_import(c, pid, path): data = load_transcript(c, pid) segs = data["segments"] with open(path, "r", encoding="utf-8") as f: raw_lines = f.read().splitlines() updated = 0 skipped = 0 for ln in raw_lines: s = ln.strip() if not s or s.startswith("#"): continue m = LINE_RE.match(s) if not m: print(f" ! skip (no match): {ln[:80]}") skipped += 1 continue idx = int(m.group(1)) new_text = m.group(5).strip() if idx >= len(segs): print(f" ! skip idx {idx}: out of range") skipped += 1 continue old = segs[idx].get("text", "") if old == new_text: continue segs[idx]["text"] = new_text updated += 1 if updated: save_transcript(c, pid, data) print(f"updated {updated} segment(s) in project {pid}") else: print("no changes") if skipped: print(f"warnings: {skipped} unparseable/skipped line(s)") def cmd_show(c, pid, idxs): data = load_transcript(c, pid) segs = data["segments"] for idx in idxs: if 0 <= idx < len(segs): x = segs[idx] print(f"[{idx:04d}] {ts(x['start'])}->{ts(x['end'])} [{x.get('speaker','?')}] {x['text']}") else: print(f"[{idx:04d}] out of range (0..{len(segs)-1})") def cmd_fix(c, pid, idx, new_text): data = load_transcript(c, pid) segs = data["segments"] if not (0 <= idx < len(segs)): sys.exit(f"idx {idx} out of range (0..{len(segs)-1})") old = segs[idx].get("text", "") segs[idx]["text"] = new_text save_transcript(c, pid, data) print(f"[{idx:04d}] {old!r}") print(f" -> {new_text!r}") def main(): ap = argparse.ArgumentParser(description="AutoShorts transcript editor") sub = ap.add_subparsers(dest="cmd", required=True) p = sub.add_parser("list") p.set_defaults(fn=cmd_list) p = sub.add_parser("export") p.add_argument("--project", default=None) p.add_argument("-o", "--out", default="transcript.txt") p.set_defaults(fn=cmd_export) p = sub.add_parser("import") p.add_argument("path") p.add_argument("--project", default=None) p.set_defaults(fn=cmd_import) p = sub.add_parser("show") p.add_argument("idxs", nargs="+", type=int) p.add_argument("--project", default=None) p.set_defaults(fn=cmd_show) p = sub.add_parser("fix") p.add_argument("idx", type=int) p.add_argument("text") p.add_argument("--project", default=None) p.set_defaults(fn=cmd_fix) args = ap.parse_args() c = connect() pid = getattr(args, "project", None) or get_latest_project(c) if args.cmd == "list": cmd_list(c) elif args.cmd == "export": cmd_export(c, pid, args.out) elif args.cmd == "import": cmd_import(c, pid, args.path) elif args.cmd == "show": cmd_show(c, pid, args.idxs) elif args.cmd == "fix": cmd_fix(c, pid, args.idx, args.text) c.close() if __name__ == "__main__": main()