Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact. - bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral), voice channel join + voice receive/playback, pluggable VNC screen broadcast (selfbot live / noVNC / screenshot) - bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS behind a thin localhost HTTP API - .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite, docs/language-comparison.md and docs/vnc-xfce-setup.md Language decision: hybrid (Python brain + Node/bun Discord layer) because Discord blocks bot video; native screen broadcast only works via a Node selfbot library.
This commit is contained in:
120
src/jarvis/dictation/history.py
Normal file
120
src/jarvis/dictation/history.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Dictation history — persists transcription results to a local JSON file.
|
||||
|
||||
Privacy-first: all data stays on disk, never leaves the machine.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def _default_history_path() -> Path:
|
||||
"""Return the default path for dictation history storage."""
|
||||
base = Path.home() / ".local" / "share" / "jarvis"
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return base / "dictation_history.json"
|
||||
|
||||
|
||||
class DictationHistory:
|
||||
"""Thread-safe, file-backed dictation history.
|
||||
|
||||
Each entry is a dict with keys:
|
||||
id – unique identifier (UUID4 hex)
|
||||
text – transcribed text
|
||||
timestamp – epoch seconds (float)
|
||||
duration – recording duration in seconds (float)
|
||||
"""
|
||||
|
||||
def __init__(self, path: Optional[Path] = None, max_entries: int = 500) -> None:
|
||||
self._path = path or _default_history_path()
|
||||
self._max_entries = max_entries
|
||||
self._lock = threading.Lock()
|
||||
self._entries: List[Dict[str, Any]] = self._load()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def add(self, text: str, duration: float = 0.0) -> Dict[str, Any]:
|
||||
"""Append a new dictation entry and persist. Returns the new entry."""
|
||||
entry: Dict[str, Any] = {
|
||||
"id": uuid.uuid4().hex,
|
||||
"text": text,
|
||||
"timestamp": time.time(),
|
||||
"duration": round(duration, 1),
|
||||
}
|
||||
with self._lock:
|
||||
# Re-read from disk to pick up external changes (e.g. deletions
|
||||
# made by the desktop app while the daemon runs in a subprocess).
|
||||
self._entries = self._load()
|
||||
self._entries.append(entry)
|
||||
# Trim oldest entries if over limit
|
||||
if len(self._entries) > self._max_entries:
|
||||
self._entries = self._entries[-self._max_entries:]
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def get_all(self) -> List[Dict[str, Any]]:
|
||||
"""Return all entries, newest first."""
|
||||
with self._lock:
|
||||
return list(reversed(self._entries))
|
||||
|
||||
def delete(self, entry_id: str) -> bool:
|
||||
"""Delete an entry by ID. Returns True if found and removed."""
|
||||
with self._lock:
|
||||
before = len(self._entries)
|
||||
self._entries = [e for e in self._entries if e["id"] != entry_id]
|
||||
if len(self._entries) < before:
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all entries."""
|
||||
with self._lock:
|
||||
self._entries = []
|
||||
self._save()
|
||||
|
||||
def reload_from_disk(self) -> None:
|
||||
"""Re-read entries from the JSON file (thread-safe).
|
||||
|
||||
Useful for external consumers (e.g. the desktop app) that need to
|
||||
pick up changes written by another process.
|
||||
"""
|
||||
with self._lock:
|
||||
self._entries = self._load()
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
with self._lock:
|
||||
return len(self._entries)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Persistence
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _load(self) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
if self._path.exists():
|
||||
with self._path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def _save(self) -> None:
|
||||
try:
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._path.open("w", encoding="utf-8") as f:
|
||||
json.dump(self._entries, f, ensure_ascii=False, indent=2)
|
||||
except Exception as exc:
|
||||
from jarvis.debug import debug_log
|
||||
debug_log(f"failed to save dictation history: {exc}", "dictation")
|
||||
Reference in New Issue
Block a user