diff --git a/scripts/api.py b/scripts/api.py index 3ce744f..facd362 100644 --- a/scripts/api.py +++ b/scripts/api.py @@ -625,6 +625,96 @@ async def transcribe_audio(request: Request, audio: UploadFile = File(...), auth os.unlink(tmp_path) raise HTTPException(status_code=500, detail=str(e)) +@app.post("/api/capture") +async def capture_audio(audio: UploadFile = File(...)): + """Auth-free capture endpoint — saves transcribed audio to Nextcloud Journal/Captures/""" + if not whisper_model: + raise HTTPException(status_code=503, detail="Whisper not available") + tmp_path = None + try: + suffix = ".webm" + if audio.content_type and "mp4" in audio.content_type: + suffix = ".mp4" + elif audio.content_type and "ogg" in audio.content_type: + suffix = ".ogg" + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: + content = await audio.read() + tmp.write(content) + tmp_path = tmp.name + + segments, info = whisper_model.transcribe( + tmp_path, + language="en", + vad_filter=True, + initial_prompt=WHISPER_PROMPT + ) + transcript = " ".join(s.text.strip() for s in segments).strip() + os.unlink(tmp_path) + tmp_path = None + + if not transcript: + return JSONResponse({"ok": False, "error": "No speech detected"}) + + # Save to Nextcloud Journal/Captures/ via WebDAV + import requests as req_lib + from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M") + filename = f"{timestamp}-voice.md" + content_md = f"# Capture — {timestamp}\n\n**type:** voice\n**modality:** audio\n**status:** unprocessed\n\n---\n\n{transcript}\n" + + nextcloud_url = os.getenv("NEXTCLOUD_URL", "") + nextcloud_user = os.getenv("NEXTCLOUD_USER", "aaron") + nextcloud_password = os.getenv("NEXTCLOUD_PASSWORD", "") + captures_dir = f"{nextcloud_url}/remote.php/dav/files/{nextcloud_user}/Journal/Captures" + auth = (nextcloud_user, nextcloud_password) + + req_lib.request("MKCOL", captures_dir, auth=auth, timeout=10) + url = f"{captures_dir}/{filename}" + response = req_lib.put(url, data=content_md.encode("utf-8"), auth=auth, timeout=30) + response.raise_for_status() + + return JSONResponse({"ok": True, "filename": filename, "transcript": transcript}) + + except Exception as e: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/captures") +async def list_captures(): + """Returns recent captures from Nextcloud Journal/Captures/ — auth-free""" + try: + import requests as req_lib + nextcloud_url = os.getenv("NEXTCLOUD_URL", "") + nextcloud_user = os.getenv("NEXTCLOUD_USER", "aaron") + nextcloud_password = os.getenv("NEXTCLOUD_PASSWORD", "") + captures_dir = f"{nextcloud_url}/remote.php/dav/files/{nextcloud_user}/Journal/Captures" + auth = (nextcloud_user, nextcloud_password) + + propfind = req_lib.request("PROPFIND", captures_dir, auth=auth, timeout=10, + headers={"Depth": "1"}) + + if propfind.status_code == 404: + return JSONResponse({"captures": []}) + + import xml.etree.ElementTree as ET + root = ET.fromstring(propfind.text) + ns = {"d": "DAV:"} + captures = [] + for resp in root.findall("d:response", ns): + href = resp.findtext("d:href", namespaces=ns) or "" + if href.endswith("/"): + continue + name = href.split("/")[-1] + if not name.endswith(".md"): + continue + captures.append({"name": name.replace(".md", ""), "duration": ""}) + + captures.sort(key=lambda x: x["name"], reverse=True) + return JSONResponse({"captures": captures[:10]}) + except Exception as e: + return JSONResponse({"captures": []}) + @app.post("/api/reindex") async def trigger_reindex(auth: str = Depends(require_auth)): try: