feat(tools-pve/asr-pipeline): multi-backend ASR web app with LiteLLM routing
This commit is contained in:
@@ -1 +1 @@
|
||||
"""Standalone ASR service for web, Agent API, and SSH drop-folder intake."""
|
||||
"""Standalone WebUI, REST API, and background worker service for Meeting ASR."""
|
||||
|
||||
@@ -3,23 +3,27 @@ from __future__ import annotations
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from asrpipe.config import CFG
|
||||
|
||||
DATA = Path(os.getenv("ASR_DATA_DIR", "/data"))
|
||||
INBOX = DATA / "inbox"
|
||||
PROCESSING = DATA / "processing"
|
||||
PROCESSED = DATA / "processed"
|
||||
FAILED = DATA / "failed"
|
||||
TRANSCRIPTS = DATA / "transcripts"
|
||||
STATE = Path(os.getenv("ASR_STATE_DIR", "/state"))
|
||||
DRAFTS = DATA / "artifacts" / "drafts"
|
||||
RESULTS = DATA / "artifacts" / "results"
|
||||
STATE = Path(os.getenv("ASR_STATE_DIR", str(CFG.state_dir)))
|
||||
DRAFTS = Path(os.getenv("ASR_DRAFTS_DIR", str(DATA / "artifacts" / "drafts")))
|
||||
RESULTS = Path(os.getenv("ASR_RESULTS_DIR", str(DATA / "artifacts" / "results")))
|
||||
LOGS = DATA / "logs"
|
||||
META = STATE / "uploads"
|
||||
REVISION_QUEUE = STATE / "revision-queue"
|
||||
REVISION_QUEUE = Path(os.getenv("ASR_REVISION_QUEUE_DIR", str(STATE / "revision-queue")))
|
||||
DB = STATE / "intake.db"
|
||||
SUPPORTED = {".m4a", ".mp3", ".wav", ".mp4", ".aac", ".ogg", ".opus", ".webm", ".flac"}
|
||||
PUBLIC_URL = os.getenv("ASR_PUBLIC_URL", "http://192.168.50.49:8788").rstrip("/")
|
||||
AGENT_TOKEN = os.getenv("ASR_AGENT_TOKEN", "")
|
||||
DIARIZE_URL = os.getenv("ASR_DIARIZE_URL", "http://192.168.50.100:10099/asr")
|
||||
WEB_PASSWORD = os.getenv("ASR_WEB_PASSWORD", "")
|
||||
DIARIZE_URL = os.getenv("ASR_DIARIZE_URL", CFG.diarize_url)
|
||||
DEFAULT_PROVIDER = os.getenv("ASR_PROVIDER", CFG.asr_provider)
|
||||
MAX_UPLOAD = int(os.getenv("ASR_UPLOAD_MAX_BYTES", str(4 * 1024**3)))
|
||||
POLL_SECONDS = max(1, int(os.getenv("ASR_POLL_SECONDS", "5")))
|
||||
STABLE_SECONDS = max(1, int(os.getenv("ASR_STABLE_SECONDS", "10")))
|
||||
@@ -27,8 +31,13 @@ MAX_ATTEMPTS = max(1, int(os.getenv("ASR_MAX_ATTEMPTS", "3")))
|
||||
|
||||
|
||||
def ensure_dirs() -> None:
|
||||
for path in (INBOX, PROCESSING, PROCESSED, FAILED, TRANSCRIPTS, DRAFTS,
|
||||
RESULTS, STATE, LOGS, META, REVISION_QUEUE, DATA / "cache",
|
||||
DATA / "artifacts" / "raw", DATA / "vault" / "minutes",
|
||||
DATA / "vault" / "People", DATA / "vault" / "Companies"):
|
||||
# Keep asrpipe CFG aligned with container/service paths when running under asrservice
|
||||
CFG.state_dir
|
||||
for path in (
|
||||
INBOX, PROCESSING, PROCESSED, FAILED, TRANSCRIPTS, DRAFTS,
|
||||
RESULTS, STATE, LOGS, META, REVISION_QUEUE, DATA / "cache",
|
||||
DATA / "artifacts" / "raw", DATA / "artifacts" / "results",
|
||||
DATA / "vault" / "minutes", DATA / "vault" / "People",
|
||||
DATA / "vault" / "Companies", DATA / "vault" / "Projects",
|
||||
):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .config import DB, ensure_dirs
|
||||
from .config import DB, DEFAULT_PROVIDER, ensure_dirs
|
||||
|
||||
STATUSES = {"uploading", "queued", "transcribing", "refining", "review",
|
||||
"publishing", "complete", "failed"}
|
||||
STATUSES = {
|
||||
"uploading", "queued", "transcribing", "refining", "review",
|
||||
"publishing", "complete", "failed",
|
||||
}
|
||||
|
||||
|
||||
def connect() -> sqlite3.Connection:
|
||||
@@ -26,6 +27,8 @@ def connect() -> sqlite3.Connection:
|
||||
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(
|
||||
@@ -37,9 +40,14 @@ def connect() -> sqlite3.Connection:
|
||||
""")
|
||||
cols = {r[1] for r in con.execute("PRAGMA table_info(intake_job)")}
|
||||
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 ''",
|
||||
"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_PROVIDER}'",
|
||||
"asr_model": "TEXT NOT NULL DEFAULT ''",
|
||||
}
|
||||
for name, ddl in additions.items():
|
||||
if name not in cols:
|
||||
@@ -48,23 +56,40 @@ def connect() -> sqlite3.Connection:
|
||||
return con
|
||||
|
||||
|
||||
def create_job(job_id: str, original_name: str, stored_name: str, *, title: str = "",
|
||||
context: str = "", size: int = 0, source: str = "web") -> dict[str, Any]:
|
||||
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 DEFAULT_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,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
||||
(job_id, original_name, stored_name, title, context, size,
|
||||
"queued", source, now, now))
|
||||
con.execute("INSERT INTO job_event(job_id,status,detail,created_at) VALUES(?,?,?,?)",
|
||||
(job_id, "queued", "录音已进入队列", now))
|
||||
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"}
|
||||
allowed = {
|
||||
"transcript", "draft", "result", "sha256", "attempts",
|
||||
"error_code", "asr_provider", "asr_model",
|
||||
}
|
||||
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]]
|
||||
@@ -75,8 +100,9 @@ def update_job(job_id: str, status: str, detail: str = "", **fields: Any) -> boo
|
||||
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()))
|
||||
con.execute(
|
||||
"INSERT INTO job_event(job_id,status,detail,created_at) VALUES(?,?,?,?)",
|
||||
(job_id, status, detail[:1000], time.time()))
|
||||
return bool(cur.rowcount)
|
||||
|
||||
|
||||
@@ -88,8 +114,9 @@ def get_job(job_id: str) -> dict[str, Any] | 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()
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -2,4 +2,6 @@ import time
|
||||
from .config import STATE
|
||||
|
||||
heartbeat = STATE / "worker.heartbeat"
|
||||
raise SystemExit(0 if heartbeat.exists() and time.time() - heartbeat.stat().st_mtime < 90 else 1)
|
||||
raise SystemExit(
|
||||
0 if heartbeat.exists() and time.time() - heartbeat.stat().st_mtime < 90 else 1
|
||||
)
|
||||
|
||||
@@ -15,21 +15,28 @@ def send(status: str, filename: str = "", detail: str = "") -> bool:
|
||||
chat = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||
if not token or not chat:
|
||||
return False
|
||||
body = parse.urlencode({"chat_id": chat,
|
||||
"text": "\n".join(x for x in (status, filename, detail) if x)}).encode()
|
||||
req = request.Request(f"https://api.telegram.org/bot{token}/sendMessage",
|
||||
data=body, method="POST")
|
||||
body = parse.urlencode({
|
||||
"chat_id": chat,
|
||||
"text": "\n".join(x for x in (status, filename, detail) if x),
|
||||
}).encode()
|
||||
req = request.Request(
|
||||
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||
data=body, method="POST")
|
||||
elif backend == "hermes":
|
||||
url = os.getenv("HERMES_WEBHOOK_URL", "")
|
||||
secret = os.getenv("HERMES_WEBHOOK_SECRET", "")
|
||||
if not url or not secret:
|
||||
return False
|
||||
body = json.dumps({"status": status, "filename": filename, "detail": detail},
|
||||
ensure_ascii=False).encode()
|
||||
body = json.dumps(
|
||||
{"status": status, "filename": filename, "detail": detail},
|
||||
ensure_ascii=False).encode()
|
||||
sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
req = request.Request(url, data=body, method="POST",
|
||||
headers={"Content-Type": "application/json",
|
||||
"X-Webhook-Signature": sig})
|
||||
req = request.Request(
|
||||
url, data=body, method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Webhook-Signature": sig,
|
||||
})
|
||||
else:
|
||||
return False
|
||||
with request.urlopen(req, timeout=10) as response:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,17 +6,17 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from asrpipe import refine, review, revise, vault
|
||||
from asrpipe import graph, lexicon, providers, refine, review, revise, vault
|
||||
from . import db
|
||||
from .config import (DIARIZE_URL, DRAFTS, FAILED, INBOX, MAX_ATTEMPTS, META,
|
||||
POLL_SECONDS, PROCESSED, PROCESSING, PUBLIC_URL,
|
||||
REVISION_QUEUE, STABLE_SECONDS, STATE, SUPPORTED, TRANSCRIPTS, LOGS,
|
||||
ensure_dirs)
|
||||
from .config import (
|
||||
DATA, DEFAULT_PROVIDER, DRAFTS, FAILED, INBOX, LOGS, MAX_ATTEMPTS,
|
||||
POLL_SECONDS, PROCESSED, PROCESSING, PUBLIC_URL, REVISION_QUEUE,
|
||||
STABLE_SECONDS, STATE, SUPPORTED, TRANSCRIPTS, ensure_dirs,
|
||||
)
|
||||
from .notify import send
|
||||
|
||||
|
||||
@@ -35,69 +35,83 @@ def sha256(path: Path) -> str:
|
||||
|
||||
|
||||
def transcript_from_response(payload: dict) -> str:
|
||||
segments = payload.get("segments")
|
||||
if not isinstance(segments, list) or not segments:
|
||||
raise RuntimeError("ASR response contains no segments")
|
||||
vp = payload.get("voiceprint") or {}
|
||||
sid = str(vp.get("session_id") or "")
|
||||
matched = [f"{m.get('speaker_local')}={m.get('person_name')}" for m in vp.get("matches", [])
|
||||
if m.get("status") == "matched"]
|
||||
speakers = {str(s.get("speaker")) for s in segments if s.get("speaker") is not None}
|
||||
lines = [f"<!-- voiceprint_session: {sid} clusters: {len(speakers)} -->",
|
||||
"# 转写(Qwen3-ASR 1.7B + CAM++ 分角色 + 声纹)"
|
||||
+ (" | 声纹命名: " + ", ".join(matched) if matched else ""), ""]
|
||||
turns: list[list[str]] = []
|
||||
for segment in segments:
|
||||
speaker, text = str(segment.get("speaker", "?")), str(segment.get("text") or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
if turns and turns[-1][0] == speaker:
|
||||
turns[-1][1] += text
|
||||
else:
|
||||
turns.append([speaker, text])
|
||||
lines.extend(f"【{speaker}】{text}\n" for speaker, text in turns)
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
"""Backward-compatible helper for qwen3cam JSON responses."""
|
||||
return providers.transcript_from_qwen3cam_response(payload)
|
||||
|
||||
|
||||
def transcribe(audio: Path, job_id: str) -> Path:
|
||||
def transcribe(
|
||||
audio: Path,
|
||||
job_id: str,
|
||||
*,
|
||||
provider: str = "",
|
||||
model: str = "",
|
||||
title: str = "",
|
||||
user_context: str = "",
|
||||
) -> tuple[Path, providers.TranscribeResult]:
|
||||
res = providers.transcribe_audio(
|
||||
audio,
|
||||
provider=provider or DEFAULT_PROVIDER,
|
||||
model=model,
|
||||
title=title,
|
||||
user_context=user_context,
|
||||
)
|
||||
response_path = TRANSCRIPTS / f"{job_id}.asr.json"
|
||||
result = subprocess.run([
|
||||
"curl", "-sS", "--fail-with-body", "--max-time", "900",
|
||||
"-F", f"file=@{audio}", "-F", "language=Chinese",
|
||||
"-F", "save_voiceprint_session=true", DIARIZE_URL,
|
||||
], capture_output=True, timeout=930)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"ASR HTTP failed ({result.returncode})")
|
||||
response_path.write_bytes(result.stdout)
|
||||
try:
|
||||
payload = json.loads(result.stdout)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError("ASR returned invalid JSON") from exc
|
||||
response_path.write_text(
|
||||
json.dumps(res.raw_response, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
transcript = TRANSCRIPTS / f"{job_id}.txt"
|
||||
transcript.write_text(transcript_from_response(payload), encoding="utf-8")
|
||||
return transcript
|
||||
transcript.write_text(res.transcript_text, encoding="utf-8")
|
||||
return transcript, res
|
||||
|
||||
|
||||
def process_audio(source: Path, job: dict) -> None:
|
||||
job_id = job["id"]
|
||||
attempts = int(job.get("attempts") or 0) + 1
|
||||
prov = providers.normalize_provider_name(job.get("asr_provider") or DEFAULT_PROVIDER)
|
||||
model = str(job.get("asr_model") or "")
|
||||
digest = sha256(source)
|
||||
db.update_job(job_id, "transcribing", "正在进行分角色转写", sha256=digest,
|
||||
attempts=attempts, error_code="")
|
||||
transcript = transcribe(source, job_id)
|
||||
intake = {"job_id": job_id, "title": job.get("title", ""),
|
||||
"context": job.get("context", ""), "source": job.get("source", "")}
|
||||
prov_label = providers.PROVIDERS.get(prov, {}).get("label", prov)
|
||||
db.update_job(
|
||||
job_id, "transcribing", f"正在转写 ({prov_label})",
|
||||
sha256=digest, attempts=attempts, error_code="",
|
||||
asr_provider=prov, asr_model=model,
|
||||
)
|
||||
transcript, res = transcribe(
|
||||
source, job_id,
|
||||
provider=prov, model=model,
|
||||
title=str(job.get("title") or ""),
|
||||
user_context=str(job.get("context") or ""),
|
||||
)
|
||||
intake = {
|
||||
"job_id": job_id,
|
||||
"title": job.get("title", ""),
|
||||
"context": job.get("context", ""),
|
||||
"source": job.get("source", ""),
|
||||
"asr_provider": res.provider,
|
||||
"asr_model": res.model,
|
||||
}
|
||||
raw = transcript.read_text(encoding="utf-8")
|
||||
raw = f"<!-- intake: {json.dumps(intake, ensure_ascii=False)} -->\n" + raw
|
||||
transcript.write_text(raw, encoding="utf-8")
|
||||
db.update_job(job_id, "refining", "转写完成,正在生成整理稿", transcript=str(transcript))
|
||||
result = refine.refine(raw, date=time.strftime("%Y-%m-%d"),
|
||||
transcript_path=str(transcript), queue_review=True)
|
||||
db.update_job(
|
||||
job_id, "refining", f"转写完成 ({res.provider}:{res.model}),正在生成整理稿",
|
||||
transcript=str(transcript), asr_provider=res.provider, asr_model=res.model,
|
||||
)
|
||||
result = refine.refine(
|
||||
raw, date=time.strftime("%Y-%m-%d"),
|
||||
transcript_path=str(transcript), queue_review=True,
|
||||
)
|
||||
draft = DRAFTS / f"{job_id}.md"
|
||||
vault.write_draft_file(draft, result.draft)
|
||||
questions = review.enqueue_draft(str(transcript), draft)
|
||||
db.update_job(job_id, "review", f"整理稿已生成,{questions} 个问题待确认",
|
||||
transcript=str(transcript), draft=str(draft))
|
||||
db.update_job(
|
||||
job_id, "review", f"整理稿已生成,{questions} 个问题待确认",
|
||||
transcript=str(transcript), draft=str(draft),
|
||||
)
|
||||
target = PROCESSED / source.name
|
||||
if target.exists():
|
||||
target = PROCESSED / f"{source.stem}-{job_id[:8]}{source.suffix}"
|
||||
@@ -110,8 +124,11 @@ def claim(path: Path) -> tuple[Path, dict]:
|
||||
job = db.find_by_stored(path.name)
|
||||
if not job:
|
||||
job_id = uuid.uuid4().hex[:24]
|
||||
job = db.create_job(job_id, path.name, path.name, title=path.stem,
|
||||
size=path.stat().st_size, source="ssh")
|
||||
job = db.create_job(
|
||||
job_id, path.name, path.name, title=path.stem,
|
||||
size=path.stat().st_size, source="ssh",
|
||||
asr_provider=DEFAULT_PROVIDER,
|
||||
)
|
||||
target = PROCESSING / path.name
|
||||
if target.exists():
|
||||
target = PROCESSING / f"{path.stem}-{job['id'][:8]}{path.suffix}"
|
||||
@@ -132,10 +149,12 @@ def scan_inbox() -> None:
|
||||
except Exception as exc:
|
||||
attempts = int((db.get_job(job["id"]) or {}).get("attempts") or 0)
|
||||
retry = attempts < MAX_ATTEMPTS
|
||||
db.update_job(job["id"], "queued" if retry else "failed",
|
||||
"处理失败,等待重试" if retry else "处理失败",
|
||||
error_code=type(exc).__name__)
|
||||
log(f"job={job['id']} failed type={type(exc).__name__}")
|
||||
db.update_job(
|
||||
job["id"], "queued" if retry else "failed",
|
||||
f"处理失败,等待重试 ({exc})" if retry else f"处理失败:{exc}",
|
||||
error_code=type(exc).__name__,
|
||||
)
|
||||
log(f"job={job['id']} failed type={type(exc).__name__} err={exc}")
|
||||
target = INBOX / source.name if retry else FAILED / source.name
|
||||
if source.exists():
|
||||
shutil.move(str(source), target)
|
||||
@@ -151,27 +170,41 @@ def process_revisions() -> None:
|
||||
revise.revise(draft)
|
||||
current = db.get_job(job_id) or {}
|
||||
if current.get("status") == "complete":
|
||||
send("ASR 正式记录已生成", current.get("title") or current.get("original_name", ""),
|
||||
send("ASR 正式记录已生成",
|
||||
current.get("title") or current.get("original_name", ""),
|
||||
f"{PUBLIC_URL}/jobs/{job_id}/")
|
||||
elif current.get("status") != "review":
|
||||
db.update_job(job_id, "review", "定稿预览已生成,等待发布确认", draft=str(draft))
|
||||
db.update_job(
|
||||
job_id, "review", "定稿预览已生成,等待发布确认",
|
||||
draft=str(draft),
|
||||
)
|
||||
marker.unlink(missing_ok=True)
|
||||
except Exception as exc:
|
||||
log(f"revision marker={marker.name} failed type={type(exc).__name__}")
|
||||
log(f"revision marker={marker.name} failed type={type(exc).__name__} err={exc}")
|
||||
|
||||
|
||||
def recover() -> None:
|
||||
for job in db.pending_recovery():
|
||||
if job["status"] in {"transcribing", "refining"}:
|
||||
candidate = next((p for p in PROCESSING.iterdir() if p.name == job["stored_name"]), None)
|
||||
candidate = next(
|
||||
(p for p in PROCESSING.iterdir() if p.name == job["stored_name"]),
|
||||
None,
|
||||
)
|
||||
if candidate:
|
||||
shutil.move(str(candidate), INBOX / candidate.name)
|
||||
db.update_job(job["id"], "queued", "worker 重启后重新排队")
|
||||
|
||||
|
||||
def sync_vault_knowledge() -> None:
|
||||
try:
|
||||
lexicon.sync_from_vault()
|
||||
graph.sync()
|
||||
except Exception as exc:
|
||||
log(f"vault knowledge sync skipped: {exc}")
|
||||
|
||||
|
||||
def backup_databases() -> None:
|
||||
# STATE is local disk; durable copies belong on the NFS data volume.
|
||||
backup_dir = Path("/data/backups")
|
||||
backup_dir = DATA / "backups"
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("intake.db", "lexicon.db"):
|
||||
source = STATE / name
|
||||
@@ -184,22 +217,34 @@ def backup_databases() -> None:
|
||||
temp.replace(target)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def run_loop() -> None:
|
||||
os.environ.setdefault("ASR_ROLE", "worker")
|
||||
os.environ.setdefault("ASR_REVISION_QUEUE_DIR", str(REVISION_QUEUE))
|
||||
ensure_dirs()
|
||||
lock = (STATE / "worker.lock").open("w")
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
recover()
|
||||
sync_vault_knowledge()
|
||||
(STATE / "worker.heartbeat").write_text(str(time.time()))
|
||||
last_backup = 0.0
|
||||
last_vault_sync = time.time()
|
||||
while True:
|
||||
scan_inbox()
|
||||
process_revisions()
|
||||
if time.time() - last_backup >= 3600:
|
||||
now = time.time()
|
||||
if now - last_vault_sync >= 900:
|
||||
sync_vault_knowledge()
|
||||
last_vault_sync = now
|
||||
if now - last_backup >= 3600:
|
||||
backup_databases()
|
||||
last_backup = time.time()
|
||||
(STATE / "worker.heartbeat").write_text(str(time.time()))
|
||||
last_backup = now
|
||||
(STATE / "worker.heartbeat").write_text(str(now))
|
||||
time.sleep(POLL_SECONDS)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
run_loop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user