Files
2026-08-15 00:28:02 +07:00

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()