deploy standalone ASR pipeline on tools-pve
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Vault side effects: draft file, pending index, git sync, notification.
|
||||
|
||||
Ported from the legacy shell implementation with its hard-won details preserved:
|
||||
* a same date+title never overwrites an existing draft (suffix -2, -3, …)
|
||||
* the pending index row is created BEFORE staging, so one commit carries both
|
||||
* the 5-minute `obsidian-sync.sh` cron may have already staged the files; that is
|
||||
tolerated rather than treated as an error
|
||||
* a failed push is reported as "local only", never as success
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from .config import CFG
|
||||
from .log import log
|
||||
|
||||
PENDING_HEADER = """---
|
||||
type: index
|
||||
title: 待确认草稿
|
||||
note: 自动维护,确认定稿后请把对应行删除
|
||||
---
|
||||
|
||||
# 待确认草稿 (ASR 自动精修)
|
||||
|
||||
| 日期 | 标题 | 草稿 |
|
||||
|---|---|---|
|
||||
"""
|
||||
|
||||
|
||||
def slug_parts(slug: str) -> tuple[str, str]:
|
||||
"""`2026-07-21-125915录音` -> ('2026-07-21', '125915录音')."""
|
||||
m = re.match(r"^(\d{4}-\d{2}-\d{2})[-_](.+)$", slug)
|
||||
if m:
|
||||
return m.group(1), m.group(2)
|
||||
import time
|
||||
return time.strftime("%Y-%m-%d"), slug
|
||||
|
||||
|
||||
def draft_path(date: str, title: str) -> Path:
|
||||
CFG.drafts.mkdir(parents=True, exist_ok=True)
|
||||
p = CFG.drafts / f"{date}-{title}.md"
|
||||
if not p.exists():
|
||||
return p
|
||||
n = 2
|
||||
while (CFG.drafts / f"{date}-{title}-{n}.md").exists():
|
||||
n += 1
|
||||
return CFG.drafts / f"{date}-{title}-{n}.md"
|
||||
|
||||
|
||||
def write_draft_file(path: Path, body: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not body.endswith("\n"):
|
||||
body += "\n"
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent,
|
||||
prefix=f".{path.name}.", delete=False) as f:
|
||||
f.write(body)
|
||||
tmp = Path(f.name)
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def find_draft_for_transcript(transcript: str) -> Path | None:
|
||||
"""Recover an interrupted run without paying for or creating a second draft."""
|
||||
if not CFG.drafts.exists():
|
||||
return None
|
||||
needle = f"transcript: {transcript}"
|
||||
for p in CFG.drafts.glob("*.md"):
|
||||
try:
|
||||
if needle in p.read_text(errors="ignore")[:2000]:
|
||||
return p
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def update_pending(date: str, title: str, path: Path) -> None:
|
||||
p = CFG.pending_index
|
||||
if not p.exists():
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(PENDING_HEADER, encoding="utf-8")
|
||||
base = path.stem
|
||||
text = p.read_text(errors="ignore")
|
||||
if f"[[{base}]]" in text:
|
||||
return
|
||||
text += f"| {date} | {title} | [[{base}]] |\n"
|
||||
write_draft_file(p, text)
|
||||
|
||||
|
||||
def _git(*args, check=False) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["git", *args], cwd=CFG.vault, capture_output=True,
|
||||
text=True, timeout=180, check=check)
|
||||
|
||||
|
||||
def sync(paths: list[Path], message: str) -> str:
|
||||
"""Commit + push. Returns 'pushed' | 'local' | 'noop' | 'disabled' | 'error'."""
|
||||
if not CFG.git_sync:
|
||||
return "disabled"
|
||||
try:
|
||||
rel = [str(p.relative_to(CFG.vault)) for p in paths if p.exists()]
|
||||
if not rel:
|
||||
return "noop"
|
||||
_git("add", "--", *rel)
|
||||
if _git("diff", "--cached", "--quiet").returncode == 0:
|
||||
log("nothing staged (likely already committed by the auto-sync cron)")
|
||||
return "noop"
|
||||
# Commit only our paths. The 5-minute vault sync may already have unrelated
|
||||
# files staged; absorbing those into this transaction makes rollback and
|
||||
# diagnosis needlessly difficult.
|
||||
c = _git("commit", "-q", "--only", "-m", message, "--", *rel)
|
||||
if c.returncode != 0:
|
||||
log(f"git commit failed: {c.stderr.strip()[:200]}", level="ERROR")
|
||||
return "error"
|
||||
p = _git("push", "-q")
|
||||
if p.returncode != 0:
|
||||
log(f"committed but push failed (local only): {p.stderr.strip()[:200]}",
|
||||
level="WARN")
|
||||
return "local"
|
||||
return "pushed"
|
||||
except Exception as e:
|
||||
log(f"git sync error: {e}", level="ERROR")
|
||||
return "error"
|
||||
|
||||
|
||||
def notify(title: str, body: str, *, redact_log: bool = False) -> None:
|
||||
if not CFG.notify:
|
||||
return
|
||||
n = CFG.notifier
|
||||
if not n.exists():
|
||||
log(f"NOTIFY (no notifier): {title} — {body}")
|
||||
return
|
||||
try:
|
||||
r = subprocess.run(["python3", str(n), "草稿已生成,待确认", title, body],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
detail = "[sensitive body redacted]" if redact_log else body
|
||||
log(f"NOTIFY: {title} — {detail} ({r.stdout.strip()[:80]})")
|
||||
except Exception as e:
|
||||
log(f"notify failed: {e}", level="WARN")
|
||||
|
||||
|
||||
def mark_done(slug: str) -> Path:
|
||||
CFG.state.mkdir(parents=True, exist_ok=True)
|
||||
m = CFG.state / f"{slug}.done"
|
||||
m.touch()
|
||||
return m
|
||||
|
||||
|
||||
def is_done(slug: str) -> bool:
|
||||
return (CFG.state / f"{slug}.done").exists()
|
||||
|
||||
|
||||
def pending_transcripts() -> list[Path]:
|
||||
if not CFG.inbox.exists():
|
||||
return []
|
||||
return sorted(p for p in CFG.inbox.glob("*.txt") if not is_done(p.stem))
|
||||
Reference in New Issue
Block a user