2026-02-23 20:12:10 -06:00

187 lines
6.3 KiB
Python

from __future__ import annotations
import contextlib
import io
import json
import sys
import base64
import time
from pathlib import Path
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from journal.ai import analysis, chat
from journal.core.models import JournalEntry
def _emit(payload: dict[str, Any]) -> None:
print(json.dumps(payload, ensure_ascii=True), flush=True)
def _ok(data: Any) -> None:
_emit({"ok": True, "data": data})
def _error(message: str) -> None:
_emit({"ok": False, "error": message})
def _read_request() -> dict[str, Any]:
line = sys.stdin.readline()
if not line:
raise ValueError("Missing request body.")
try:
payload = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError("Invalid JSON request.") from exc
if not isinstance(payload, dict):
raise ValueError("Request must be a JSON object.")
return payload
def _safe_call(fn, *args, **kwargs):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
return fn(*args, **kwargs)
def _to_entry(item: Any) -> JournalEntry:
if isinstance(item, str):
return JournalEntry(date="", raw_content=item)
if isinstance(item, dict):
raw_content = str(item.get("raw_content") or item.get("content") or "")
date = str(item.get("date") or item.get("file_stem") or "")
return JournalEntry(date=date, raw_content=raw_content)
raise ValueError("Each entry must be a string or object.")
def _normalize_entries(payload: dict[str, Any]) -> list[JournalEntry]:
entries = payload.get("entries")
if entries is None:
return []
if not isinstance(entries, list):
raise ValueError("payload.entries must be an array.")
return [_to_entry(item) for item in entries]
def _run_action(action: str, payload: dict[str, Any]) -> Any:
if action == "health":
backend = _safe_call(analysis.get_nlp_backend)
return {
"provider": "python-sidecar",
"healthy": True,
"message": f"ok ({backend})",
}
if action == "summarize_entry":
content = payload.get("content")
if not isinstance(content, str) or not content.strip():
raise ValueError("payload.content is required.")
file_stem = payload.get("file_stem")
date = str(file_stem) if file_stem is not None else str(payload.get("date") or "")
entry = JournalEntry(date=date, raw_content=content)
return _safe_call(analysis.summarize_entry, entry)
if action == "summarize_all":
entries = _normalize_entries(payload)
return _safe_call(analysis.summarize_all_entries, entries)
if action == "chat":
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
raise ValueError("payload.prompt is required.")
return _safe_call(chat.get_cloud_ai_response, prompt)
if action == "embed":
content = payload.get("content")
if not isinstance(content, str) or not content.strip():
raise ValueError("payload.content is required.")
return _safe_call(analysis.generate_embedding, content)
if action == "speech.devices.list":
try:
import speech_recognition as sr
names = sr.Microphone.list_microphone_names()
devices = [
{"index": index, "name": name}
for index, name in enumerate(names)
]
warning = "No microphone devices detected." if not devices else None
return {"devices": devices, "warning": warning}
except Exception as exc:
return {"devices": [], "warning": f"Speech device listing unavailable: {exc}"}
if action == "speech.transcribe":
engine = str(payload.get("engine") or "whisper").strip().lower() or "whisper"
whisper_model = str(payload.get("whisper_model") or "base").strip() or "base"
simulate_delay_ms = payload.get("simulate_delay_ms")
if isinstance(simulate_delay_ms, int) and simulate_delay_ms > 0:
time.sleep(simulate_delay_ms / 1000.0)
passthrough = payload.get("text")
if isinstance(passthrough, str) and passthrough.strip():
return {"text": passthrough, "engine": engine}
audio_base64 = payload.get("audio_base64")
if not isinstance(audio_base64, str) or not audio_base64.strip():
raise ValueError("payload.audio_base64 is required.")
try:
audio_bytes = base64.b64decode(audio_base64, validate=True)
except Exception as exc:
raise ValueError("payload.audio_base64 must be valid base64.") from exc
import speech_recognition as sr
recognizer = sr.Recognizer()
with sr.AudioFile(io.BytesIO(audio_bytes)) as source:
audio = recognizer.record(source)
if engine == "google":
text = recognizer.recognize_google(audio)
elif engine == "whisper":
text = recognizer.recognize_whisper(audio, model=whisper_model)
elif engine == "faster-whisper":
if not hasattr(recognizer, "recognize_faster_whisper"):
raise ValueError("faster-whisper engine unavailable in current SpeechRecognition build.")
text = recognizer.recognize_faster_whisper(audio, model=whisper_model)
elif engine == "sphinx":
text = recognizer.recognize_sphinx(audio)
else:
raise ValueError(f"Unsupported speech engine: {engine}")
return {"text": text, "engine": engine}
raise ValueError(f"Unknown action: {action}")
def main() -> int:
try:
request = _read_request()
action = request.get("action")
payload = request.get("payload") or {}
if not isinstance(action, str) or not action.strip():
raise ValueError("Request action is required.")
if not isinstance(payload, dict):
raise ValueError("Request payload must be an object.")
result = _run_action(action.strip(), payload)
_ok(result)
return 0
except Exception as exc:
_error(str(exc))
return 1
if __name__ == "__main__":
raise SystemExit(main())