228 lines
6.4 KiB
Python
228 lines
6.4 KiB
Python
#!/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 <id>] [-o out.txt]
|
|
python transcript_edit.py import <edited.txt> [--project <id>]
|
|
python transcript_edit.py list
|
|
python transcript_edit.py show <idx> [--project <id>]
|
|
python transcript_edit.py fix <idx> "new text" [--project <id>]
|
|
|
|
Edit flow (recommended):
|
|
1) export -> edit the text column in a text editor
|
|
2) import <file> -> 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()
|