diff --git a/scripts/api.py b/scripts/api.py index 5fdeb3e..3143dae 100644 --- a/scripts/api.py +++ b/scripts/api.py @@ -690,44 +690,92 @@ async def run_dreamer(request: Request, auth: str = Depends(require_auth)): return JSONResponse({"started": False, "error": str(e)}) def transcribe_and_save(tmp_path, timestamp, nextcloud_url, nextcloud_user, nextcloud_password): - """Background task — transcribes audio and saves to Nextcloud after endpoint returns.""" + """Background task — transcribes audio and saves to Nextcloud after endpoint returns. + Audio is preserved in Journal/Media/ on every terminal path; failed and empty-transcript + captures still produce a markdown record in Journal/Captures/ with a status field.""" import requests as req_lib nc_auth = (nextcloud_user, nextcloud_password) + month_dir = timestamp[:7] + audio_ext = os.path.splitext(tmp_path)[1] or ".webm" + audio_filename = f"{timestamp}-voice{audio_ext}" + audio_relpath = f"Journal/Media/{month_dir}/{audio_filename}" + + def archive_audio() -> bool: + try: + with open(tmp_path, "rb") as f: + audio_bytes = f.read() + media_parent = f"{nextcloud_url}/remote.php/dav/files/{nextcloud_user}/Journal/Media" + media_dir = f"{media_parent}/{month_dir}" + req_lib.request("MKCOL", media_parent, auth=nc_auth, timeout=10) + req_lib.request("MKCOL", media_dir, auth=nc_auth, timeout=10) + req_lib.put(f"{media_dir}/{audio_filename}", data=audio_bytes, auth=nc_auth, timeout=60) + return True + except Exception as e: + print(f"Audio archival failed for {timestamp}: {e}") + return False + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + def write_capture(filename: str, content_md: str, status: str): + captures_dir = f"{nextcloud_url}/remote.php/dav/files/{nextcloud_user}/Journal/Captures" + try: + req_lib.request("MKCOL", captures_dir, auth=nc_auth, timeout=10) + req_lib.put(f"{captures_dir}/{filename}", data=content_md.encode("utf-8"), auth=nc_auth, timeout=30) + except Exception as e: + print(f"Capture markdown write failed for {timestamp}: {e}") + return + try: + payload = {"type": "capture_saved", "filename": filename, "timestamp": timestamp, "status": status} + req_lib.post("http://localhost:8000/api/events/notify", json=payload, timeout=3) + req_lib.post("http://localhost:8000/api/captures/events/notify", json=payload, timeout=3) + except Exception: + pass + + transcript = "" + transcribe_error = None try: segments, _ = whisper_model.transcribe( tmp_path, language="en", vad_filter=True, beam_size=1, initial_prompt=WHISPER_PROMPT ) transcript = " ".join(s.text.strip() for s in segments).strip() - os.unlink(tmp_path) - if not transcript: - print(f"Async transcription empty for {timestamp} — nothing saved") - return - 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" - captures_dir = f"{nextcloud_url}/remote.php/dav/files/{nextcloud_user}/Journal/Captures" - req_lib.request("MKCOL", captures_dir, auth=nc_auth, timeout=10) - url = f"{captures_dir}/{filename}" - req_lib.put(url, data=content_md.encode("utf-8"), auth=nc_auth, timeout=30) - print(f"Async transcription saved: {filename}") - # Notify SSE clients that transcription is complete - try: - import requests as _req - _req.post("http://localhost:8000/api/events/notify", json={ - "type": "capture_saved", - "filename": filename, - "timestamp": timestamp, - }, timeout=3) - _req.post("http://localhost:8000/api/captures/events/notify", json={ - "type": "capture_saved", - "filename": filename, - "timestamp": timestamp, - }, timeout=3) - except Exception: - pass except Exception as e: - if os.path.exists(tmp_path): - os.unlink(tmp_path) - print(f"Async transcription failed for {timestamp}: {e}") + transcribe_error = str(e) + + audio_archived = archive_audio() + audio_line = f"**audio_path:** {audio_relpath}\n" if audio_archived else "**audio_archive_failed:** true\n" + + if transcribe_error is not None: + filename = f"{timestamp}-voice-failed.md" + content_md = ( + f"# Capture — {timestamp}\n\n" + f"**type:** voice\n**modality:** audio\n**status:** failed_transcription\n" + f"{audio_line}" + f"**error:** {transcribe_error}\n" + ) + write_capture(filename, content_md, "failed_transcription") + print(f"Async transcription failed for {timestamp}: {transcribe_error}") + return + + if not transcript: + filename = f"{timestamp}-voice-empty.md" + content_md = ( + f"# Capture — {timestamp}\n\n" + f"**type:** voice\n**modality:** audio\n**status:** empty_transcript\n" + f"{audio_line}" + ) + write_capture(filename, content_md, "empty_transcript") + print(f"Async transcription empty for {timestamp}: audio archived") + return + + filename = f"{timestamp}-voice.md" + content_md = ( + f"# Capture — {timestamp}\n\n" + f"**type:** voice\n**modality:** audio\n**status:** saved\n" + f"{audio_line}\n---\n\n{transcript}\n" + ) + write_capture(filename, content_md, "saved") + print(f"Async transcription saved: {filename}") @app.post("/api/capture") @@ -834,7 +882,7 @@ Keep the full description to 150-250 words. Do not speculate beyond what is visi **type:** {capture_type} **modality:** {modality} -**status:** unprocessed +**status:** saved **media:** {media_path} {f"**project:** {project}" if project else ""}