import shutil import subprocess import requests from . import database as db, config from .config import merge_campaign_settings_with_env def _check_ffmpeg() -> dict: path = shutil.which("ffmpeg") if not path: return {"name": "ffmpeg", "ok": False, "message": "ffmpeg not found on PATH. This should be baked into the Docker image - if you're seeing this, the image build is broken."} return {"name": "ffmpeg", "ok": True, "message": path} def _check_gpu() -> dict: try: import torch if torch.cuda.is_available(): name = torch.cuda.get_device_name(0) free, total = torch.cuda.mem_get_info(0) free_gb, total_gb = free / 1e9, total / 1e9 return {"name": "gpu", "ok": True, "message": f"{name} — {free_gb:.1f}GB free of {total_gb:.1f}GB"} return {"name": "gpu", "ok": False, "message": "No CUDA GPU visible. Check that the container was started with --gpus all / GPU passthrough in compose, and that host NVIDIA drivers + Container Toolkit are installed. Transcription will fall back to CPU, which is much slower."} except Exception as e: return {"name": "gpu", "ok": False, "message": f"Could not query GPU: {e}"} def _check_hf_token(campaign_id: str = "default") -> dict: settings = merge_campaign_settings_with_env(db.get_campaign_settings(campaign_id)) token = settings.get("hf_token", "") if not token: return {"name": "huggingface_token", "ok": False, "message": "No HF token set. Diarization (speaker separation) will fail without one. Add it in Settings."} return {"name": "huggingface_token", "ok": True, "message": "Token is set (not validated against HF until first diarization run)."} def _check_llm_backend(campaign_id: str = "default") -> dict: settings = merge_campaign_settings_with_env(db.get_campaign_settings(campaign_id)) if settings.get("llm_mode") == "api": if not settings.get("api_key"): return {"name": "llm_backend", "ok": False, "message": "Hosted API selected but no API key set."} return {"name": "llm_backend", "ok": True, "message": f"Hosted API configured ({settings.get('api_base_url')})"} host = settings.get("ollama_host", "http://localhost:11434") model = settings.get("ollama_model", "") try: resp = requests.get(f"{host.rstrip('/')}/api/tags", timeout=5) resp.raise_for_status() models = [m["name"] for m in resp.json().get("models", [])] if model not in models: return {"name": "llm_backend", "ok": False, "message": f"Reached Ollama at {host}, but model '{model}' isn't pulled there. Available: {', '.join(models) or '(none)'}. Run: ollama pull {model}"} return {"name": "llm_backend", "ok": True, "message": f"Ollama reachable at {host}, model '{model}' available"} except requests.exceptions.ConnectionError: return {"name": "llm_backend", "ok": False, "message": f"Could not connect to Ollama at {host}. Check the host/port in Settings, and that Ollama is running and reachable from this container."} except Exception as e: return {"name": "llm_backend", "ok": False, "message": f"Error checking Ollama: {e}"} def _check_disk_space() -> dict: usage = shutil.disk_usage(config.DATA_DIR) free_gb = usage.free / 1e9 ok = free_gb > 5 return {"name": "disk_space", "ok": ok, "message": f"{free_gb:.1f}GB free in {config.DATA_DIR}" + ("" if ok else " — this is low, recordings and model caches need room")} def run_diagnostics(campaign_id: str = "default") -> list[dict]: return [ _check_ffmpeg(), _check_gpu(), _check_hf_token(campaign_id), _check_llm_backend(campaign_id), _check_disk_space(), ]