Initial commit
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import BackgroundTasks, FastAPI, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
DATA_DIR = Path(os.environ.get("DATA_DIR", "./data"))
|
||||
AUDIO_DIR = DATA_DIR / "audio"
|
||||
DB_PATH = DATA_DIR / "voice-kb.sqlite3"
|
||||
WHISPER_SERVER_URL = os.environ.get("WHISPER_SERVER_URL", "").strip()
|
||||
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
|
||||
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app = FastAPI(title="Voice KB")
|
||||
|
||||
|
||||
def connect_db() -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
connection.row_factory = sqlite3.Row
|
||||
return connection
|
||||
|
||||
|
||||
def initialize_db() -> None:
|
||||
with connect_db() as connection:
|
||||
connection.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
audio_path TEXT NOT NULL,
|
||||
transcript TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL
|
||||
)"""
|
||||
)
|
||||
connection.execute(
|
||||
"""CREATE VIRTUAL TABLE IF NOT EXISTS note_search
|
||||
USING fts5(note_id UNINDEXED, transcript)"""
|
||||
)
|
||||
|
||||
|
||||
initialize_db()
|
||||
|
||||
|
||||
def update_transcript(note_id: str, transcript: str, status: str) -> None:
|
||||
with connect_db() as connection:
|
||||
connection.execute(
|
||||
"UPDATE notes SET transcript = ?, status = ? WHERE id = ?",
|
||||
(transcript, status, note_id),
|
||||
)
|
||||
connection.execute("DELETE FROM note_search WHERE note_id = ?", (note_id,))
|
||||
if transcript.strip():
|
||||
connection.execute(
|
||||
"INSERT INTO note_search (note_id, transcript) VALUES (?, ?)",
|
||||
(note_id, transcript.strip()),
|
||||
)
|
||||
|
||||
|
||||
async def transcribe_note(note_id: str, audio_path: Path) -> None:
|
||||
if not WHISPER_SERVER_URL:
|
||||
return
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=900) as client:
|
||||
with audio_path.open("rb") as audio_file:
|
||||
response = await client.post(
|
||||
WHISPER_SERVER_URL,
|
||||
files={"file": (audio_path.name, audio_file, "application/octet-stream")},
|
||||
data={"response_format": "json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
transcript = response.json().get("text", "").strip()
|
||||
update_transcript(note_id, transcript, "transcribed" if transcript else "empty_transcript")
|
||||
except Exception:
|
||||
update_transcript(note_id, "", "transcription_failed")
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/notes", status_code=201)
|
||||
async def create_note(
|
||||
background_tasks: BackgroundTasks,
|
||||
recording_id: str = Form(...),
|
||||
audio: UploadFile = File(...),
|
||||
created_at: str = Form(...),
|
||||
) -> dict[str, str]:
|
||||
try:
|
||||
note_id = str(uuid.UUID(recording_id))
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail="Invalid recording ID") from error
|
||||
with connect_db() as connection:
|
||||
existing = connection.execute("SELECT status FROM notes WHERE id = ?", (note_id,)).fetchone()
|
||||
if existing:
|
||||
return {"id": note_id, "status": existing["status"]}
|
||||
suffix = Path(audio.filename or "recording.webm").suffix.lower()
|
||||
if not suffix or len(suffix) > 10:
|
||||
suffix = ".audio"
|
||||
audio_path = AUDIO_DIR / f"{note_id}{suffix}"
|
||||
try:
|
||||
with audio_path.open("wb") as destination:
|
||||
while chunk := await audio.read(1024 * 1024):
|
||||
destination.write(chunk)
|
||||
except Exception as error:
|
||||
audio_path.unlink(missing_ok=True)
|
||||
raise HTTPException(status_code=500, detail="Could not store audio") from error
|
||||
finally:
|
||||
await audio.close()
|
||||
|
||||
try:
|
||||
parsed_date = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
||||
if parsed_date.tzinfo is None:
|
||||
parsed_date = parsed_date.replace(tzinfo=timezone.utc)
|
||||
created_at = parsed_date.astimezone(timezone.utc).isoformat()
|
||||
except ValueError:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
status = "awaiting_transcription" if not WHISPER_SERVER_URL else "transcribing"
|
||||
with connect_db() as connection:
|
||||
connection.execute(
|
||||
"INSERT OR IGNORE INTO notes (id, created_at, filename, audio_path, status) VALUES (?, ?, ?, ?, ?)",
|
||||
(note_id, created_at, audio.filename or "recording.audio", str(audio_path), status),
|
||||
)
|
||||
if WHISPER_SERVER_URL:
|
||||
background_tasks.add_task(transcribe_note, note_id, audio_path)
|
||||
return {"id": note_id, "status": status}
|
||||
|
||||
|
||||
@app.get("/api/notes")
|
||||
def list_notes(q: str = "") -> list[dict[str, str]]:
|
||||
with connect_db() as connection:
|
||||
search_terms = re.findall(r"[\w]+", q, flags=re.UNICODE)
|
||||
if search_terms:
|
||||
query = " AND ".join(f'"{term.replace(chr(34), chr(34) * 2)}"' for term in search_terms)
|
||||
rows = connection.execute(
|
||||
"""SELECT n.id, n.created_at, n.filename, n.transcript, n.status
|
||||
FROM note_search s JOIN notes n ON n.id = s.note_id
|
||||
WHERE note_search MATCH ? ORDER BY n.created_at DESC LIMIT 100""",
|
||||
(query,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = connection.execute(
|
||||
"""SELECT id, created_at, filename, transcript, status
|
||||
FROM notes ORDER BY created_at DESC LIMIT 100"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
@app.put("/api/notes/{note_id}/transcript")
|
||||
def save_transcript(note_id: str, payload: dict[str, str]) -> dict[str, str]:
|
||||
transcript = payload.get("transcript", "").strip()
|
||||
with connect_db() as connection:
|
||||
exists = connection.execute("SELECT 1 FROM notes WHERE id = ?", (note_id,)).fetchone()
|
||||
if not exists:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
update_transcript(note_id, transcript, "transcribed" if transcript else "awaiting_transcription")
|
||||
return {"id": note_id, "status": "transcribed" if transcript else "awaiting_transcription"}
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
|
||||
Reference in New Issue
Block a user