init: workspace opencode sync
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user