Dateien nach "app" hochladen
This commit is contained in:
1 parent
debc2a0bc8
commit
73ea0441fb
1 file changed
+498
+498
@@ -0,0 +1,498 @@
|
|||||||
|
"""
|
||||||
|
openHAB AI Bridge (simple) – text dialog + session context + Edge TTS.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
openHAB text → POST /api/v1/dialog → SQLite session
|
||||||
|
→ MQTT notify → openHAB Cloud push → iOS Shortcut
|
||||||
|
→ Ask Claude → POST /api/v1/sessions/{id}/answer
|
||||||
|
→ POST /api/v1/audio/speak → Pi speaker (Edge TTS)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Header, Depends
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=os.getenv("LOG_LEVEL", "INFO").upper(),
|
||||||
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||||
|
)
|
||||||
|
log = logging.getLogger("openhab-ai-bridge")
|
||||||
|
|
||||||
|
HOST = os.getenv("HOST", "0.0.0.0")
|
||||||
|
PORT = int(os.getenv("PORT", "8090"))
|
||||||
|
API_TOKEN = os.environ["API_TOKEN"]
|
||||||
|
DB_PATH = os.getenv("DB_PATH", "/data/bridge.db")
|
||||||
|
|
||||||
|
MAX_HISTORY = int(os.getenv("MAX_HISTORY_MESSAGES", "16"))
|
||||||
|
SESSION_TTL = int(os.getenv("SESSION_TTL_SECONDS", "7200"))
|
||||||
|
MAX_SESSIONS = int(os.getenv("MAX_SESSIONS", "20"))
|
||||||
|
|
||||||
|
MQTT_HOST = os.getenv("MQTT_HOST", "192.168.178.60")
|
||||||
|
MQTT_PORT = int(os.getenv("MQTT_PORT", "1884"))
|
||||||
|
MQTT_PREFIX = os.getenv("MQTT_PREFIX", "openhab/ai_bridge").strip("/")
|
||||||
|
MQTT_USER = os.getenv("MQTT_USERNAME", "")
|
||||||
|
MQTT_PASS = os.getenv("MQTT_PASSWORD", "")
|
||||||
|
MQTT_TLS = os.getenv("MQTT_TLS", "false").lower() == "true"
|
||||||
|
|
||||||
|
ALSA_PLAYBACK = os.getenv("ALSA_PLAYBACK_DEVICE", "default")
|
||||||
|
TTS_VOICE = os.getenv("TTS_VOICE", "de-DE-ConradNeural")
|
||||||
|
TTS_RATE = os.getenv("TTS_RATE", "+0%")
|
||||||
|
TTS_VOLUME = os.getenv("TTS_VOLUME", "+0%")
|
||||||
|
|
||||||
|
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
app = FastAPI(title="openHAB AI Bridge", version="3.0.0-simple")
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def db():
|
||||||
|
c = sqlite3.connect(DB_PATH, timeout=10)
|
||||||
|
c.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
c.execute("PRAGMA journal_mode=WAL")
|
||||||
|
c.execute("PRAGMA busy_timeout=5000")
|
||||||
|
yield c
|
||||||
|
c.commit()
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with db() as c:
|
||||||
|
c.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS sessions(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
prompt TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
last_response TEXT NOT NULL DEFAULT '')"""
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS messages(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL)"""
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_msg ON messages(session_id, id)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mqtt_pub(topic: str, payload: str, retain: bool = False):
|
||||||
|
cl = mqtt.Client(
|
||||||
|
mqtt.CallbackAPIVersion.VERSION2,
|
||||||
|
client_id=f"openhab-ai-bridge-{uuid.uuid4().hex[:8]}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if MQTT_USER:
|
||||||
|
cl.username_pw_set(MQTT_USER, MQTT_PASS)
|
||||||
|
if MQTT_TLS:
|
||||||
|
cl.tls_set()
|
||||||
|
cl.connect(MQTT_HOST, MQTT_PORT, 10)
|
||||||
|
info = cl.publish(topic, payload, qos=1, retain=retain)
|
||||||
|
info.wait_for_publish(5)
|
||||||
|
cl.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
log.error("MQTT publish failed topic=%s error=%s", topic, e)
|
||||||
|
|
||||||
|
|
||||||
|
def auth(authorization: str | None = Header(default=None)):
|
||||||
|
if authorization != f"Bearer {API_TOKEN}":
|
||||||
|
raise HTTPException(401, "Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
cutoff = time.time() - SESSION_TTL
|
||||||
|
with db() as c:
|
||||||
|
rows = c.execute(
|
||||||
|
"SELECT id, updated_at FROM sessions WHERE status NOT IN ('ended','expired')"
|
||||||
|
).fetchall()
|
||||||
|
for r in rows:
|
||||||
|
try:
|
||||||
|
if datetime.fromisoformat(r["updated_at"]).timestamp() < cutoff:
|
||||||
|
c.execute(
|
||||||
|
"UPDATE sessions SET status='expired', updated_at=? WHERE id=?",
|
||||||
|
(now(), r["id"]),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
c.execute(
|
||||||
|
"""DELETE FROM messages WHERE session_id IN
|
||||||
|
(SELECT id FROM sessions WHERE status IN ('ended','expired')
|
||||||
|
AND updated_at < datetime('now','-7 days'))"""
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"""DELETE FROM sessions WHERE status IN ('ended','expired')
|
||||||
|
AND updated_at < datetime('now','-7 days')"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def session_dict(r) -> dict:
|
||||||
|
return {
|
||||||
|
"session_id": r["id"],
|
||||||
|
"title": r["title"],
|
||||||
|
"prompt": r["prompt"],
|
||||||
|
"status": r["status"],
|
||||||
|
"created_at": r["created_at"],
|
||||||
|
"updated_at": r["updated_at"],
|
||||||
|
"last_response": r["last_response"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def publish_active_session(sid: str | None):
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/active_session", sid or "", True)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionCreate(BaseModel):
|
||||||
|
title: str = Field(default="KI-Anfrage", max_length=200)
|
||||||
|
prompt: str = Field(min_length=1, max_length=8000)
|
||||||
|
notify_text: str | None = Field(default=None, max_length=1000)
|
||||||
|
|
||||||
|
|
||||||
|
class Answer(BaseModel):
|
||||||
|
content: str = Field(min_length=1, max_length=12000)
|
||||||
|
|
||||||
|
|
||||||
|
class ContinueReq(BaseModel):
|
||||||
|
content: str = Field(min_length=1, max_length=8000)
|
||||||
|
|
||||||
|
|
||||||
|
class DialogReq(BaseModel):
|
||||||
|
text: str = Field(min_length=1, max_length=8000)
|
||||||
|
title: str = Field(default="openHAB Dialog", max_length=200)
|
||||||
|
notify_text: str | None = Field(default="KI_DIALOG", max_length=1000)
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakReq(BaseModel):
|
||||||
|
text: str = Field(min_length=1, max_length=12000)
|
||||||
|
voice: str | None = Field(default=None, max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def startup():
|
||||||
|
init_db()
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/availability", "ONLINE", True)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/status", "IDLE", True)
|
||||||
|
log.info(
|
||||||
|
"started port=%s mqtt=%s:%s prefix=%s",
|
||||||
|
PORT,
|
||||||
|
MQTT_HOST,
|
||||||
|
MQTT_PORT,
|
||||||
|
MQTT_PREFIX,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
def shutdown():
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/availability", "OFFLINE", True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"ok": True, "service": "openhab-ai-bridge", "version": "3.0.0-simple"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/pending")
|
||||||
|
def pending(_: None = Depends(auth)):
|
||||||
|
cleanup()
|
||||||
|
with db() as c:
|
||||||
|
r = c.execute(
|
||||||
|
"SELECT * FROM sessions WHERE status='pending' ORDER BY created_at LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
return {"pending": bool(r), **(session_dict(r) if r else {})}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/active")
|
||||||
|
def active(_: None = Depends(auth)):
|
||||||
|
cleanup()
|
||||||
|
with db() as c:
|
||||||
|
r = c.execute(
|
||||||
|
"""SELECT * FROM sessions
|
||||||
|
WHERE status IN ('pending','processing','active')
|
||||||
|
ORDER BY updated_at DESC LIMIT 1"""
|
||||||
|
).fetchone()
|
||||||
|
return {"active": bool(r), **(session_dict(r) if r else {})}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_session(title: str, prompt: str, notify_text: str | None, source: str) -> str:
|
||||||
|
cleanup()
|
||||||
|
with db() as c:
|
||||||
|
n = c.execute(
|
||||||
|
"SELECT COUNT(*) FROM sessions WHERE status IN ('pending','processing','active')"
|
||||||
|
).fetchone()[0]
|
||||||
|
if n >= MAX_SESSIONS:
|
||||||
|
raise HTTPException(429, "Too many active sessions")
|
||||||
|
sid = str(uuid.uuid4())
|
||||||
|
ts = now()
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO sessions VALUES(?,?,?,?,?,?,?)",
|
||||||
|
(sid, title, prompt, "pending", ts, ts, ""),
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO messages(session_id, role, content, created_at) VALUES(?,?,?,?)",
|
||||||
|
(sid, "user", prompt, ts),
|
||||||
|
)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/status", "PENDING", True)
|
||||||
|
publish_active_session(sid)
|
||||||
|
mqtt_pub(
|
||||||
|
f"{MQTT_PREFIX}/session/new",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"session_id": sid,
|
||||||
|
"title": title,
|
||||||
|
"prompt": prompt,
|
||||||
|
"notify_text": notify_text,
|
||||||
|
"source": source,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Notify topic for openHAB → Cloud Push
|
||||||
|
mqtt_pub(
|
||||||
|
f"{MQTT_PREFIX}/notify",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"session_id": sid,
|
||||||
|
"kind": "dialog",
|
||||||
|
"text": prompt[:200],
|
||||||
|
"notify_text": notify_text or "KI_DIALOG",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
log.info("session created id=%s source=%s title=%r", sid, source, title)
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions")
|
||||||
|
def create(req: SessionCreate, _: None = Depends(auth)):
|
||||||
|
sid = _create_session(req.title, req.prompt, req.notify_text, "api")
|
||||||
|
return {
|
||||||
|
"session_id": sid,
|
||||||
|
"status": "pending",
|
||||||
|
"title": req.title,
|
||||||
|
"prompt": req.prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/sessions/{sid}")
|
||||||
|
def get_session(sid: str, _: None = Depends(auth)):
|
||||||
|
with db() as c:
|
||||||
|
r = c.execute("SELECT * FROM sessions WHERE id=?", (sid,)).fetchone()
|
||||||
|
if not r:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
return session_dict(r)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/sessions/{sid}/context")
|
||||||
|
def context(sid: str, _: None = Depends(auth)):
|
||||||
|
with db() as c:
|
||||||
|
s = c.execute("SELECT * FROM sessions WHERE id=?", (sid,)).fetchone()
|
||||||
|
if not s:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
m = c.execute(
|
||||||
|
"SELECT role, content, created_at FROM messages "
|
||||||
|
"WHERE session_id=? ORDER BY id DESC LIMIT ?",
|
||||||
|
(sid, MAX_HISTORY),
|
||||||
|
).fetchall()
|
||||||
|
return {
|
||||||
|
"session_id": sid,
|
||||||
|
"title": s["title"],
|
||||||
|
"status": s["status"],
|
||||||
|
"messages": [dict(x) for x in reversed(m)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions/{sid}/answer")
|
||||||
|
def answer(sid: str, req: Answer, _: None = Depends(auth)):
|
||||||
|
with db() as c:
|
||||||
|
s = c.execute("SELECT id FROM sessions WHERE id=?", (sid,)).fetchone()
|
||||||
|
if not s:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
ts = now()
|
||||||
|
c.execute(
|
||||||
|
"UPDATE sessions SET status='active', updated_at=?, last_response=? WHERE id=?",
|
||||||
|
(ts, req.content, sid),
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO messages(session_id, role, content, created_at) VALUES(?,?,?,?)",
|
||||||
|
(sid, "assistant", req.content, ts),
|
||||||
|
)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/last_response", req.content, True)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/status", "ACTIVE", True)
|
||||||
|
publish_active_session(sid)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/session/{sid}/answer", req.content)
|
||||||
|
log.info("answer id=%s chars=%d", sid, len(req.content))
|
||||||
|
return {"ok": True, "session_id": sid, "status": "active"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions/{sid}/continue")
|
||||||
|
def cont(sid: str, req: ContinueReq, _: None = Depends(auth)):
|
||||||
|
with db() as c:
|
||||||
|
s = c.execute("SELECT status FROM sessions WHERE id=?", (sid,)).fetchone()
|
||||||
|
if not s:
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
if s["status"] in ("ended", "expired"):
|
||||||
|
raise HTTPException(409, "Session ended")
|
||||||
|
ts = now()
|
||||||
|
c.execute(
|
||||||
|
"UPDATE sessions SET status='processing', updated_at=? WHERE id=?",
|
||||||
|
(ts, sid),
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO messages(session_id, role, content, created_at) VALUES(?,?,?,?)",
|
||||||
|
(sid, "user", req.content, ts),
|
||||||
|
)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/status", "PROCESSING", True)
|
||||||
|
publish_active_session(sid)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/session/{sid}/user", req.content)
|
||||||
|
mqtt_pub(
|
||||||
|
f"{MQTT_PREFIX}/notify",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"session_id": sid,
|
||||||
|
"kind": "dialog",
|
||||||
|
"text": req.content[:200],
|
||||||
|
"notify_text": "KI_DIALOG",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {"ok": True, "session_id": sid, "status": "processing"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/dialog")
|
||||||
|
def dialog(req: DialogReq, _: None = Depends(auth)):
|
||||||
|
"""
|
||||||
|
openHAB entry point.
|
||||||
|
Reuses active session if present, otherwise creates a new one.
|
||||||
|
Publishes MQTT notify so openHAB can trigger the Cloud push.
|
||||||
|
"""
|
||||||
|
cleanup()
|
||||||
|
with db() as c:
|
||||||
|
s = c.execute(
|
||||||
|
"""SELECT * FROM sessions
|
||||||
|
WHERE status IN ('active','processing','pending')
|
||||||
|
ORDER BY updated_at DESC LIMIT 1"""
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if s:
|
||||||
|
sid = s["id"]
|
||||||
|
cont(sid, ContinueReq(content=req.text))
|
||||||
|
mqtt_pub(
|
||||||
|
f"{MQTT_PREFIX}/dialog/input",
|
||||||
|
json.dumps({"session_id": sid, "text": req.text}, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return {"session_id": sid, "status": "processing", "reused": True}
|
||||||
|
|
||||||
|
sid = _create_session(req.title, req.text, req.notify_text, "openhab")
|
||||||
|
mqtt_pub(
|
||||||
|
f"{MQTT_PREFIX}/dialog/input",
|
||||||
|
json.dumps({"session_id": sid, "text": req.text}, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return {"session_id": sid, "status": "pending", "reused": False}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/sessions/{sid}/end")
|
||||||
|
def end(sid: str, _: None = Depends(auth)):
|
||||||
|
with db() as c:
|
||||||
|
if not c.execute("SELECT id FROM sessions WHERE id=?", (sid,)).fetchone():
|
||||||
|
raise HTTPException(404, "Session not found")
|
||||||
|
c.execute(
|
||||||
|
"UPDATE sessions SET status='ended', updated_at=? WHERE id=?",
|
||||||
|
(now(), sid),
|
||||||
|
)
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/status", "IDLE", True)
|
||||||
|
publish_active_session("")
|
||||||
|
mqtt_pub(f"{MQTT_PREFIX}/session/{sid}/ended", "ENDED")
|
||||||
|
return {"ok": True, "session_id": sid, "status": "ended"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/audio/speak")
|
||||||
|
async def speak(req: SpeakReq, _: None = Depends(auth)):
|
||||||
|
"""Edge TTS (free) → WAV → physical Pi speaker via ALSA."""
|
||||||
|
outdir = Path("/data/audio")
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
ident = uuid.uuid4().hex
|
||||||
|
mp3 = outdir / f"{ident}.mp3"
|
||||||
|
wav = outdir / f"{ident}.wav"
|
||||||
|
voice = req.voice or TTS_VOICE
|
||||||
|
try:
|
||||||
|
import edge_tts
|
||||||
|
|
||||||
|
communicate = edge_tts.Communicate(
|
||||||
|
req.text, voice, rate=TTS_RATE, volume=TTS_VOLUME
|
||||||
|
)
|
||||||
|
await communicate.save(str(mp3))
|
||||||
|
p = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-i",
|
||||||
|
str(mp3),
|
||||||
|
"-ar",
|
||||||
|
"48000",
|
||||||
|
"-ac",
|
||||||
|
"2",
|
||||||
|
str(wav),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if p.returncode:
|
||||||
|
raise HTTPException(500, p.stderr[-1000:] or "TTS conversion failed")
|
||||||
|
p = subprocess.run(
|
||||||
|
["aplay", "-D", ALSA_PLAYBACK, str(wav)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=180,
|
||||||
|
)
|
||||||
|
if p.returncode:
|
||||||
|
raise HTTPException(500, p.stderr[-1000:] or "ALSA playback failed")
|
||||||
|
return {"ok": True, "voice": voice, "speaker": "pi-alsa"}
|
||||||
|
finally:
|
||||||
|
for f in (mp3, wav):
|
||||||
|
try:
|
||||||
|
f.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/audio/devices")
|
||||||
|
def audio_devices(_: None = Depends(auth)):
|
||||||
|
out = {}
|
||||||
|
for k, cmd in {
|
||||||
|
"playback": ["aplay", "-l"],
|
||||||
|
"capture": ["arecord", "-l"],
|
||||||
|
}.items():
|
||||||
|
try:
|
||||||
|
p = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||||
|
out[k] = {
|
||||||
|
"returncode": p.returncode,
|
||||||
|
"stdout": p.stdout,
|
||||||
|
"stderr": p.stderr,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
out[k] = {"error": str(e)}
|
||||||
|
return out
|
||||||
Reference in new issue
Block a user