247 lines
8.2 KiB
Python
247 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import time
|
|
from typing import Any
|
|
|
|
from asrpipe.config import CFG, bootstrap_default_settings, get_setting, load_db_settings, set_settings
|
|
from .config import DB, ensure_dirs
|
|
|
|
STATUSES = {
|
|
"uploading", "queued", "transcribing", "refining", "review",
|
|
"publishing", "complete", "failed",
|
|
}
|
|
|
|
_BOOTSTRAPPED_DB: str = ""
|
|
|
|
|
|
def connect() -> sqlite3.Connection:
|
|
global _BOOTSTRAPPED_DB
|
|
ensure_dirs()
|
|
con = sqlite3.connect(DB, timeout=30)
|
|
con.row_factory = sqlite3.Row
|
|
con.execute("PRAGMA journal_mode=WAL")
|
|
con.execute("PRAGMA foreign_keys=ON")
|
|
con.executescript("""
|
|
CREATE TABLE IF NOT EXISTS intake_job(
|
|
id TEXT PRIMARY KEY, original_name TEXT NOT NULL, stored_name TEXT NOT NULL,
|
|
title TEXT NOT NULL DEFAULT '', context TEXT NOT NULL DEFAULT '',
|
|
size INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL,
|
|
detail TEXT NOT NULL DEFAULT '', transcript TEXT NOT NULL DEFAULT '',
|
|
draft TEXT NOT NULL DEFAULT '', result TEXT NOT NULL DEFAULT '',
|
|
source TEXT NOT NULL DEFAULT 'web', sha256 TEXT NOT NULL DEFAULT '',
|
|
attempts INTEGER NOT NULL DEFAULT 0, error_code TEXT NOT NULL DEFAULT '',
|
|
asr_provider TEXT NOT NULL DEFAULT 'qwen3cam',
|
|
asr_model TEXT NOT NULL DEFAULT '',
|
|
created_at REAL NOT NULL, updated_at REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS job_event(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
job_id TEXT NOT NULL REFERENCES intake_job(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL, detail TEXT NOT NULL DEFAULT '', created_at REAL NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_job_event_job ON job_event(job_id,id);
|
|
CREATE TABLE IF NOT EXISTS app_setting(
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL DEFAULT '',
|
|
updated_at REAL NOT NULL
|
|
);
|
|
""")
|
|
cols = {r[1] for r in con.execute("PRAGMA table_info(intake_job)")}
|
|
default_prov = CFG.asr_provider or "qwen3cam"
|
|
if default_prov not in {"qwen3cam", "soniox", "gemini", "openai", "grok"}:
|
|
default_prov = "qwen3cam"
|
|
additions = {
|
|
"draft": "TEXT NOT NULL DEFAULT ''",
|
|
"result": "TEXT NOT NULL DEFAULT ''",
|
|
"source": "TEXT NOT NULL DEFAULT 'web'",
|
|
"sha256": "TEXT NOT NULL DEFAULT ''",
|
|
"attempts": "INTEGER NOT NULL DEFAULT 0",
|
|
"error_code": "TEXT NOT NULL DEFAULT ''",
|
|
"asr_provider": f"TEXT NOT NULL DEFAULT '{default_prov}'",
|
|
"asr_model": "TEXT NOT NULL DEFAULT ''",
|
|
}
|
|
for name, ddl in additions.items():
|
|
if name not in cols:
|
|
con.execute(f"ALTER TABLE intake_job ADD COLUMN {name} {ddl}")
|
|
con.commit()
|
|
if _BOOTSTRAPPED_DB != str(DB):
|
|
_BOOTSTRAPPED_DB = str(DB)
|
|
try:
|
|
bootstrap_default_settings()
|
|
except Exception:
|
|
pass
|
|
return con
|
|
|
|
|
|
def create_job(
|
|
job_id: str,
|
|
original_name: str,
|
|
stored_name: str,
|
|
*,
|
|
title: str = "",
|
|
context: str = "",
|
|
size: int = 0,
|
|
source: str = "web",
|
|
asr_provider: str = "",
|
|
asr_model: str = "",
|
|
) -> dict[str, Any]:
|
|
now = time.time()
|
|
prov = (asr_provider or CFG.asr_provider or "qwen3cam").strip().lower()
|
|
with connect() as con:
|
|
con.execute(
|
|
"INSERT INTO intake_job(id,original_name,stored_name,title,context,size,"
|
|
"status,source,asr_provider,asr_model,created_at,updated_at) "
|
|
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(job_id, original_name, stored_name, title, context, size,
|
|
"queued", source, prov, asr_model, now, now))
|
|
con.execute(
|
|
"INSERT INTO job_event(job_id,status,detail,created_at) VALUES(?,?,?,?)",
|
|
(job_id, "queued", f"录音已进入队列 (ASR: {prov})", now))
|
|
return get_job(job_id) or {}
|
|
|
|
|
|
def update_job(job_id: str, status: str, detail: str = "", **fields: Any) -> bool:
|
|
if status not in STATUSES:
|
|
raise ValueError(f"invalid status: {status}")
|
|
allowed = {
|
|
"transcript", "draft", "result", "sha256", "attempts",
|
|
"error_code", "asr_provider", "asr_model", "title", "context", "stored_name",
|
|
}
|
|
changes = {k: v for k, v in fields.items() if k in allowed and v is not None}
|
|
sets = ["status=?", "updated_at=?", "detail=?"]
|
|
values: list[Any] = [status, time.time(), detail[:1000]]
|
|
for key, value in changes.items():
|
|
sets.append(f"{key}=?")
|
|
values.append(value)
|
|
values.append(job_id)
|
|
with connect() as con:
|
|
cur = con.execute(f"UPDATE intake_job SET {','.join(sets)} WHERE id=?", values)
|
|
if cur.rowcount:
|
|
con.execute(
|
|
"INSERT INTO job_event(job_id,status,detail,created_at) VALUES(?,?,?,?)",
|
|
(job_id, status, detail[:1000], time.time()))
|
|
return bool(cur.rowcount)
|
|
|
|
|
|
def update_job_meta(
|
|
job_id: str,
|
|
*,
|
|
title: str | None = None,
|
|
context: str | None = None,
|
|
asr_provider: str | None = None,
|
|
asr_model: str | None = None,
|
|
) -> bool:
|
|
sets = ["updated_at=?"]
|
|
values: list[Any] = [time.time()]
|
|
if title is not None:
|
|
sets.append("title=?")
|
|
values.append(title[:120])
|
|
if context is not None:
|
|
sets.append("context=?")
|
|
values.append(context[:2000])
|
|
if asr_provider is not None:
|
|
sets.append("asr_provider=?")
|
|
values.append(asr_provider[:40])
|
|
if asr_model is not None:
|
|
sets.append("asr_model=?")
|
|
values.append(asr_model[:80])
|
|
values.append(job_id)
|
|
with connect() as con:
|
|
cur = con.execute(f"UPDATE intake_job SET {','.join(sets)} WHERE id=?", values)
|
|
return bool(cur.rowcount)
|
|
|
|
|
|
def delete_job(job_id: str) -> dict[str, Any] | None:
|
|
job = get_job(job_id)
|
|
if not job:
|
|
return None
|
|
with connect() as con:
|
|
con.execute("DELETE FROM job_event WHERE job_id=?", (job_id,))
|
|
con.execute("DELETE FROM intake_job WHERE id=?", (job_id,))
|
|
return job
|
|
|
|
|
|
def get_job(job_id: str) -> dict[str, Any] | None:
|
|
with connect() as con:
|
|
row = con.execute("SELECT * FROM intake_job WHERE id=?", (job_id,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def find_by_stored(stored_name: str) -> dict[str, Any] | None:
|
|
with connect() as con:
|
|
row = con.execute(
|
|
"SELECT * FROM intake_job WHERE stored_name=? ORDER BY created_at DESC LIMIT 1",
|
|
(stored_name,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def list_jobs(
|
|
limit: int = 30,
|
|
offset: int = 0,
|
|
status: str = "",
|
|
q: str = "",
|
|
provider: str = "",
|
|
) -> list[dict[str, Any]]:
|
|
limit = min(max(limit, 1), 200)
|
|
offset = max(offset, 0)
|
|
sql = "SELECT * FROM intake_job WHERE 1=1"
|
|
args: list[Any] = []
|
|
if status:
|
|
sql += " AND status=?"
|
|
args.append(status)
|
|
if provider:
|
|
sql += " AND asr_provider=?"
|
|
args.append(provider)
|
|
if q.strip():
|
|
pat = f"%{q.strip()}%"
|
|
sql += " AND (title LIKE ? OR original_name LIKE ? OR context LIKE ? OR detail LIKE ?)"
|
|
args.extend([pat, pat, pat, pat])
|
|
sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
args.extend((limit, offset))
|
|
with connect() as con:
|
|
return [dict(r) for r in con.execute(sql, args).fetchall()]
|
|
|
|
|
|
def events(job_id: str) -> list[dict[str, Any]]:
|
|
with connect() as con:
|
|
return [dict(r) for r in con.execute(
|
|
"SELECT status,detail,created_at FROM job_event WHERE job_id=? ORDER BY id",
|
|
(job_id,)).fetchall()]
|
|
|
|
|
|
def pending_recovery() -> list[dict[str, Any]]:
|
|
with connect() as con:
|
|
return [dict(r) for r in con.execute(
|
|
"SELECT * FROM intake_job WHERE status IN ('queued','transcribing','refining','publishing') "
|
|
"ORDER BY created_at").fetchall()]
|
|
|
|
|
|
def get_all_settings() -> dict[str, str]:
|
|
connect().close()
|
|
return load_db_settings(force=True)
|
|
|
|
|
|
def save_settings(updates: dict[str, Any]) -> dict[str, str]:
|
|
connect().close()
|
|
set_settings(updates)
|
|
return load_db_settings(force=True)
|
|
|
|
|
|
__all__ = [
|
|
"STATUSES",
|
|
"connect",
|
|
"create_job",
|
|
"update_job",
|
|
"update_job_meta",
|
|
"delete_job",
|
|
"get_job",
|
|
"find_by_stored",
|
|
"list_jobs",
|
|
"events",
|
|
"pending_recovery",
|
|
"get_all_settings",
|
|
"save_settings",
|
|
"get_setting",
|
|
]
|