feat: enhance application functionality and telemetry monitoring
- Added new scripts in `package.json` for Python dependency installation and sidecar compilation. - Integrated telemetry fetching in `src/App.tsx` to monitor CPU, RAM, GPU, and disk usage. - Implemented model downloading functionality with progress tracking in `src/App.tsx`. - Updated `Footer` and `Header` components to display telemetry data and settings options. - Enhanced Tauri configuration for sidecar execution and application window settings. - Updated Python requirements in `requirements.txt` to include additional dependencies for improved functionality.
This commit is contained in:
1 parent
482b28513e
commit
08e154894a
24 files changed
+1110
-91
No files matched your search
@@ -0,0 +1 @@
|
||||
# Package initialization
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Servidor FastAPI para expor telemetria e download de modelos.
|
||||
Utiliza Server-Sent Events (SSE) para streaming de progresso em tempo real.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
|
||||
# Detecta se está rodando como binário ou script
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Caminho do executável (PyInstaller)
|
||||
BASE_DIR = os.path.dirname(sys.executable)
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, "modelConfig.yaml")
|
||||
MODELS_DIR = os.path.join(BASE_DIR, "models")
|
||||
else:
|
||||
# Rodando como script Python
|
||||
# Ajusta PYTHONPATH antes dos imports relativos
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(script_dir)) # Sobe para raiz do projeto
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
BASE_DIR = project_root
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, "modelConfig.yaml")
|
||||
MODELS_DIR = os.path.join(BASE_DIR, "src-python", "app", "models")
|
||||
|
||||
# Importações relativas (funcionam quando executado como módulo)
|
||||
from .telemetry import SystemTelemetry
|
||||
from .model_downloader import ModelDownloader
|
||||
|
||||
app = FastAPI(title="LocalAI API", version="1.0.0")
|
||||
|
||||
# CORS para permitir comunicação com frontend Tauri
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Em produção, restringir ao localhost
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Instâncias globais
|
||||
downloader = ModelDownloader(models_dir=MODELS_DIR)
|
||||
|
||||
# Modelos de dados
|
||||
class ModelInfo(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
quant: str
|
||||
size_gb: float
|
||||
repo: str
|
||||
filename: str
|
||||
description: str
|
||||
|
||||
class DownloadRequest(BaseModel):
|
||||
model_id: str
|
||||
repo_id: str
|
||||
filename: str
|
||||
|
||||
# Cache de modelos disponíveis (pode ser carregado de JSON)
|
||||
MODELS_REGISTRY: List[Dict] = [
|
||||
{
|
||||
"id": "lfm-2.5-1.2b",
|
||||
"name": "Liquid LFM 2.5",
|
||||
"quant": "Q6_K",
|
||||
"size_gb": 1.12,
|
||||
"repo": "LiquidAI/LFM2.5-1.2B-Instruct-GGUF",
|
||||
"filename": "LFM2.5-1.2B-Instruct-Q6_K.gguf",
|
||||
"description": "Otimizado para arquitetura sequencial de alta performance."
|
||||
}
|
||||
]
|
||||
|
||||
@app.get("/api/telemetry")
|
||||
async def get_telemetry():
|
||||
"""Retorna telemetria do sistema em tempo real."""
|
||||
return SystemTelemetry.get_stats()
|
||||
|
||||
@app.get("/api/models")
|
||||
async def list_models():
|
||||
"""Lista todos os modelos disponíveis com status de download."""
|
||||
models_with_status = []
|
||||
for model in MODELS_REGISTRY:
|
||||
model_path = downloader.get_model_path(model["repo"], model["filename"])
|
||||
model_copy = model.copy()
|
||||
model_copy["downloaded"] = model_path.exists() if model_path else False
|
||||
models_with_status.append(model_copy)
|
||||
return {"models": models_with_status}
|
||||
|
||||
@app.post("/api/models/download")
|
||||
async def download_model(request: DownloadRequest):
|
||||
"""
|
||||
Inicia download de modelo com progresso via SSE.
|
||||
O frontend deve consumir como EventSource.
|
||||
"""
|
||||
async def generate_progress():
|
||||
try:
|
||||
async for progress_event in downloader.download_with_progress(
|
||||
repo_id=request.repo_id,
|
||||
filename=request.filename
|
||||
):
|
||||
yield f"data: {json.dumps(progress_event)}\n\n"
|
||||
except Exception as e:
|
||||
error_event = {"type": "error", "message": str(e)}
|
||||
yield f"data: {json.dumps(error_event)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate_progress(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/api/models/{model_id}/status")
|
||||
async def get_model_status(model_id: str):
|
||||
"""Verifica se um modelo específico está baixado."""
|
||||
model = next((m for m in MODELS_REGISTRY if m["id"] == model_id), None)
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Modelo não encontrado")
|
||||
|
||||
model_path = downloader.get_model_path(model["repo"], model["filename"])
|
||||
return {
|
||||
"model_id": model_id,
|
||||
"downloaded": model_path.exists() if model_path else False,
|
||||
"path": str(model_path) if model_path else None
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Módulo de download de modelos GGUF do HuggingFace com progresso em tempo real.
|
||||
Utiliza huggingface_hub com callbacks para reportar progresso via async generator.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Dict
|
||||
from huggingface_hub import hf_hub_download
|
||||
import asyncio
|
||||
|
||||
class ModelDownloader:
|
||||
"""Gerencia downloads de modelos com progresso assíncrono."""
|
||||
|
||||
def __init__(self, models_dir: str = "./src-python/app/models"):
|
||||
self.models_dir = Path(models_dir)
|
||||
self.models_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._downloads: Dict[str, float] = {} # Cache de progresso por modelo
|
||||
|
||||
def get_model_path(self, repo_id: str, filename: str) -> Path:
|
||||
"""Retorna o caminho esperado do modelo."""
|
||||
# huggingface_hub salva em cache_dir/repo_id/filename
|
||||
# Simplificamos para models_dir/repo_id/filename
|
||||
repo_name = repo_id.split("/")[-1]
|
||||
return self.models_dir / repo_name / filename
|
||||
|
||||
async def download_with_progress(
|
||||
self,
|
||||
repo_id: str,
|
||||
filename: str
|
||||
) -> AsyncGenerator[Dict, None]:
|
||||
"""
|
||||
Download assíncrono com progresso via callback.
|
||||
Gera eventos: {"type": "progress", "percent": float} ou {"type": "complete"}
|
||||
"""
|
||||
model_path = self.get_model_path(repo_id, filename)
|
||||
|
||||
# Verifica se já existe
|
||||
if model_path.exists():
|
||||
yield {"type": "complete", "percent": 100, "path": str(model_path)}
|
||||
return
|
||||
|
||||
# Callback para progresso
|
||||
last_percent = 0
|
||||
|
||||
def progress_callback(downloaded: int, total: int):
|
||||
nonlocal last_percent
|
||||
if total > 0:
|
||||
percent = min(100, int((downloaded / total) * 100))
|
||||
if percent != last_percent:
|
||||
last_percent = percent
|
||||
# Armazena para acesso assíncrono
|
||||
self._downloads[f"{repo_id}/{filename}"] = percent
|
||||
|
||||
try:
|
||||
# Executa download em thread separada para não bloquear
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def run_download():
|
||||
return hf_hub_download(
|
||||
repo_id=repo_id,
|
||||
filename=filename,
|
||||
local_dir=str(self.models_dir),
|
||||
local_dir_use_symlinks=False,
|
||||
resume_download=True,
|
||||
# huggingface_hub não expõe callback direto, então monitoramos o arquivo
|
||||
)
|
||||
|
||||
# Inicia download em executor
|
||||
download_task = loop.run_in_executor(None, run_download)
|
||||
|
||||
# Monitora progresso enquanto download ocorre
|
||||
while not download_task.done():
|
||||
key = f"{repo_id}/{filename}"
|
||||
percent = self._downloads.get(key, 0)
|
||||
|
||||
# Tenta estimar progresso pelo tamanho do arquivo parcial
|
||||
partial_path = self.get_model_path(repo_id, filename)
|
||||
if partial_path.exists():
|
||||
# Estima baseado no tamanho (aproximado, pois não temos total exato)
|
||||
size_mb = partial_path.stat().st_size / (1024 * 1024)
|
||||
# Assumindo ~1GB para modelos pequenos (ajustar conforme necessário)
|
||||
estimated_total_mb = 1200 # ~1.2GB
|
||||
if size_mb < estimated_total_mb:
|
||||
percent = min(95, int((size_mb / estimated_total_mb) * 100))
|
||||
|
||||
yield {"type": "progress", "percent": percent}
|
||||
await asyncio.sleep(0.5) # Atualiza a cada 500ms
|
||||
|
||||
# Download completo
|
||||
final_path = download_task.result()
|
||||
yield {"type": "complete", "percent": 100, "path": final_path}
|
||||
|
||||
# Limpa cache
|
||||
self._downloads.pop(f"{repo_id}/{filename}", None)
|
||||
|
||||
except Exception as e:
|
||||
yield {"type": "error", "message": str(e)}
|
||||
raise
|
||||
@@ -0,0 +1,44 @@
|
||||
import psutil
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# GPUtil é opcional (requer distutils, removido no Python 3.12+)
|
||||
try:
|
||||
import GPUtil
|
||||
HAS_GPUTIL = True
|
||||
except ImportError:
|
||||
HAS_GPUTIL = False
|
||||
|
||||
class SystemTelemetry:
|
||||
@staticmethod
|
||||
def get_stats():
|
||||
# CPU
|
||||
cpu_usage = psutil.cpu_percent(interval=None)
|
||||
|
||||
# RAM
|
||||
ram = psutil.virtual_memory()
|
||||
|
||||
# GPU (Pega a primeira disponível)
|
||||
gpu_data = {"usage": 0, "vram_used": 0, "vram_total": 0}
|
||||
if HAS_GPUTIL:
|
||||
try:
|
||||
gpus = GPUtil.getGPUs()
|
||||
if gpus:
|
||||
gpu_data = {
|
||||
"usage": gpus[0].load * 100,
|
||||
"vram_used": gpus[0].memoryUsed,
|
||||
"vram_total": gpus[0].memoryTotal
|
||||
}
|
||||
except Exception:
|
||||
pass # Fallback silencioso se GPUtil falhar
|
||||
|
||||
# Armazenamento (Partição do App)
|
||||
total, used, free = shutil.disk_usage("./")
|
||||
|
||||
return {
|
||||
"cpu": cpu_usage,
|
||||
"ram_used": ram.used / (1024**3),
|
||||
"ram_total": ram.total / (1024**3),
|
||||
"gpu": gpu_data,
|
||||
"disk_free_gb": free / (1024**3)
|
||||
}
|
||||
Reference in new issue
Block a user