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:
Evandro Fonseca Junior committed 2026-01-14 23:57:14 -03:00
1 parent 482b28513e
commit 08e154894a
24 files changed
+1110 -91

No files matched your search

+1
View File
@@ -0,0 +1 @@
# Package initialization
+1
View File
@@ -0,0 +1 @@
# Package initialization
+139
View File
@@ -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)
+98
View File
@@ -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
+44
View File
@@ -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)
}
+7 -1
View File
@@ -1,3 +1,9 @@
# requirements.txt gerado (estático, baseado na importação visível)
llama-cpp-python>=0.2.0
huggingface-hub>=0.20.0
huggingface-hub>=0.20.0
fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.0.0
psutil>=5.9.0
# gputil>=1.4.0 # Opcional: requer distutils (removido no Python 3.12+)
pyyaml>=6.0