187 lines
6.3 KiB
Python
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())
|