Skip to content

Latest commit

 

History

History
332 lines (265 loc) · 16.7 KB

File metadata and controls

332 lines (265 loc) · 16.7 KB

Exploitation de PortFolio-AI

Guide pour faire tourner, déployer, arrêter et dépanner le portfolio vocal publié sur https://mathiscapart.xyz. Toutes les commandes se lancent depuis la racine du dépôt, sous PowerShell sauf mention contraire.

1. Architecture

                    ┌──────── serveur front (Debian 13, toujours allumé) ────────┐
Visiteur ──HTTPS/WSS──> Cloudflare ──tunnel──> cloudflared ─┐
                                                            v
                                     Traefik (conteneur, port 80 interne)
                                      │                     │
                          /  (front)  v                     v  /api (préfixe retiré), réseau local
                        nginx (conteneur)          ─────────┼──── PC du GPU (Windows, allumé à la demande) ───
                        frontend/out/              API FastAPI (NATIF, 192.168.1.75:8000)
                                                      │       │        │         │
                                                      │       │        │         └─> TTS Pocket (NATIF, 127.0.0.1:8001)
                                                      │       │        └─> Ollama (NATIF, :11434) : qwen3:8b + embeddings
                                                      │       └─> Qdrant (conteneur, 127.0.0.1:6333)
                                                      └─> STT Kyutai (dans le process de l'API, GPU ROCm)
Brique Où Démarrage automatique au reboot
nginx, Traefik, cloudflared serveur front, conteneurs portfolio-* (projet portfolio) oui (unless-stopped)
Qdrant PC du GPU, conteneur qdrant (projet compose lobster) oui (unless-stopped)
Ollama application Windows oui (dossier Démarrage de la session)
API (STT + RAG + LLM) process Python natif, .venv-rocm non
TTS (voix clonée) process Python natif, .venv-tts non

Sur le PC, les conteneurs ne redémarrent que si Docker Desktop est lancé, et Ollama qu'à l'ouverture de la session Windows.

PC éteint : le serveur front continue de servir le site. /api/health répond 502 (Traefik abandonne la connexion au PC après 2 s), le front désactive le bouton Parler et annonce l'assistant hors ligne avec un lien LinkedIn. Qdrant reste sur le PC : une recherche a de toute façon besoin d'Ollama pour les embeddings.

Pourquoi l'IA n'est pas en Docker

  • Le GPU. La carte est une AMD RX 7700 XT sous Windows. Docker Desktop fait tourner des conteneurs Linux dans une VM WSL2 ; ROCm dans WSL2 n'est proposé que pour certaines cartes (support de la 7700 XT non vérifié). Ce qui marche ici, ce sont les wheels ROCm Windows (cp312-win_amd64, repo.radeon.com), qui ne s'installent pas dans un conteneur Linux. Le STT est donc natif.
  • L'API suit le STT : elle le charge dans son propre process.
  • Ollama accède directement au GPU sous Windows.
  • Le TTS tourne sur CPU : rien ne l'empêche d'être conteneurisé. Il est natif parce qu'il est arrivé après, pas par contrainte.

Coût de ce choix : démarrage manuel, pas d'isolation, venvs fragiles. Pistes pour conteneuriser davantage :

  1. Sortir le STT en service natif seul sur le GPU, passer API et TTS en Docker.
  2. Vérifier si ROCm fonctionne dans WSL2 avec cette carte : si oui, le STT peut aussi passer en Docker.
  3. Serveur sous Linux : ROCm y fonctionne en Docker, tout devient conteneurisable.

Environnements Python

venv Python Contenu Règle
.venv-rocm 3.12 torch 2.9.1+rocmsdk, moshi (STT), FastAPI, clients Qdrant/Ollama ne jamais y installer un paquet PyPI qui tire torch : il écraserait le build ROCm
.venv-tts 3.12 pocket-tts, torch CPU, FastAPI Pocket TTS y tourne 40x plus vite que sous torch ROCm

2. Prérequis (première installation)

  1. Docker Desktop lancé.
  2. Ollama installé, avec les modèles :
    ollama pull qwen3:8b
    ollama pull qwen3-embedding:0.6b
  3. Fichier .env à la racine, copié de .env.example et complété (TRAEFIK_DOMAIN, CLOUDFLARE_TUNNEL_TOKEN). Il n'est pas versionné.
  4. Les deux venvs : .venv-rocm depuis backend/stt/requirements.rocm.txt et les requirements de backend/rag et backend/api ; .venv-tts avec pocket-tts, fastapi, uvicorn.
  5. Voix clonée (facultatif, sinon voix estelle) :
    • accepter les conditions de https://huggingface.co/kyutai/pocket-tts puis .venv-tts/Scripts/hf auth login ;
    • déposer l'enregistrement dans backend/tts/voix/mathis.wav (10 à 30 s, pièce calme ; ce dossier n'est jamais versionné) ;
    • exporter l'état de la voix :
      .venv-tts\Scripts\python.exe -m pocket_tts export-voice backend/tts/voix/mathis.wav backend/tts/voix/mathis.safetensors --language french_24l
  6. Front : npm ci dans frontend/.

3. Démarrer tout

Dans l'ordre :

# 1. Qdrant (lié à 127.0.0.1)
docker compose up -d qdrant

# 2. API + TTS natifs (tue d'abord ce qui écoute sur 8000/8001)
#    API_HOST : l'API écoute sur l'IP locale pour que le serveur front la joigne
$env:API_HOST = "192.168.1.75"
powershell -ExecutionPolicy Bypass -File deploy\start-natif.ps1

# 3. Exposition, SUR LE SERVEUR FRONT (bash) : nginx, Traefik, cloudflared
#    --env-file obligatoire : sinon compose cherche deploy/.env
docker compose --env-file .env -f deploy/docker-compose.expose.yml up -d

start-natif.ps1 fixe ce que l'API attend :

Variable Valeur Rôle
API_HOST 127.0.0.1 (production : 192.168.1.75) interface d'écoute de l'API
CORS_ORIGINS https://mathiscapart.xyz seule origine acceptée sur /voice
TTS_VOIX backend\tts\voix\mathis.safetensors si présent voix clonée, sinon estelle
TORCHDYNAMO_DISABLE 1 Triton absent des wheels ROCm Windows

Chaque variable peut être surchargée en la définissant avant l'appel du script.

L'API met environ 30 s à démarrer (chargement du STT). Elle est prête quand deploy\logs\api.err.log contient Application startup complete.

4. Arrêter

# Arrêter l'API et le TTS natifs (le site statique reste en ligne, le vocal tombe)
powershell -ExecutionPolicy Bypass -File deploy\stop-natif.ps1

# Voir ce qui serait arrêté, sans rien arrêter
powershell -ExecutionPolicy Bypass -File deploy\stop-natif.ps1 -WhatIf

# Couper le site public (le tunnel tombe, le site renvoie une erreur Cloudflare)
docker compose --env-file .env -f deploy/docker-compose.expose.yml stop

# Arrêter Qdrant (les données restent dans le volume lobster_qdrant_data)
docker compose stop qdrant

# Libérer la VRAM prise par le LLM sans quitter Ollama
ollama stop qwen3:8b

Pour tout redémarrer, reprendre la section 3. stop conserve les conteneurs ; down les supprime (les données Qdrant restent, elles sont dans un volume).

5. Déployer une modification

Ce qui change Action Coupure
frontend/** rebuild du front aucune
backend/api/**, backend/stt/** redémarrer l'API ~30-40 s sur le vocal
backend/tts/** ou la voix redémarrer le TTS quelques secondes sur la voix
backend/rag/corpus/*.md réindexer et rebuilder le front aucune
deploy/traefik/config/*.yml redémarrer Traefik quelques secondes
deploy/docker-compose.expose.yml up -d de la stack d'exposition quelques secondes
docker-compose.yml (Qdrant) docker compose up -d qdrant quelques secondes

Front

nginx (sur le serveur front) sert directement frontend/out/ (montage) : le build se fait sur le PC, la copie sur le serveur front est la mise en ligne, sans redémarrage.

cd frontend
$env:NEXT_PUBLIC_API_URL = "https://mathiscapart.xyz/api"
npm run build
scp -r out <utilisateur>@<serveur front>:~/PortFolio-AI/frontend/

scp écrase les fichiers mais ne supprime pas ceux qui ont disparu du build.

Sans NEXT_PUBLIC_API_URL, le build échoue exprès (garde-fou de VoiceChat.tsx).

API et TTS

powershell -ExecutionPolicy Bypass -File deploy\start-natif.ps1

Le script redémarre les deux. Pour ne redémarrer que le TTS :

Get-NetTCPConnection -LocalPort 8001 -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
$env:PYTHONIOENCODING = "utf-8"
$env:TTS_VOIX = "$PWD\backend\tts\voix\mathis.safetensors"
Start-Process -WindowStyle Hidden -WorkingDirectory $PWD -FilePath ".venv-tts\Scripts\python.exe" `
    -ArgumentList "-m uvicorn backend.tts.server:app --host 127.0.0.1 --port 8001" `
    -RedirectStandardOutput deploy\logs\tts.out.log -RedirectStandardError deploy\logs\tts.err.log

Corpus (le parcours)

  1. Modifier backend/rag/corpus/*.md. Chaque fait doit être vrai et sourcé. Isoler un rôle ou un point précis dans une sous-section courte : un nom propre noyé dans un long paragraphe remonte mal à la recherche.
  2. Réindexer (remplace les passages de chaque fichier, sans doublon) :
    $env:PYTHONIOENCODING = "utf-8"
    .venv-rocm\Scripts\python.exe -c "from backend.rag.main import EmbeddingModel, QdrantVectorStore, Settings, ingest_directory; s=Settings(); q=QdrantVectorStore(host=s.qdrant_host, port=s.qdrant_port); e=EmbeddingModel(model_name=s.embedding_model, host=s.ollama_host, port=s.ollama_port); ingest_directory('backend/rag/corpus', q, e, s.qdrant_collection); print(q.client.count(s.qdrant_collection).count, 'passages')"
    L'ingestion refuse tout fichier contenant encore À REMPLIR.
  3. Rebuilder le front : la page parcours est générée à partir du même corpus.

Limite connue : un fichier supprimé du corpus n'est pas purgé de l'index. Pour retirer un fichier, supprimer ses points à la main ou recréer la collection.

Traefik

Le watch du provider file ne fonctionne pas sur le montage Windows : toute modification de deploy/traefik/config/portfolio.yml exige un redémarrage.

docker restart portfolio-traefik-1

Code sur GitHub

  • Branche → PR → relecture humaine → fusion. Jamais de commit direct sur main.
  • La CI (.github/workflows/ci.yml) tourne sur chaque PR et sur main : tests backend et front, typecheck, build, build Docker, gitleaks, pip-audit, npm audit.
  • Fusionner ne déploie rien : il n'y a pas de déploiement automatique. La prod tourne depuis le dossier local ; appliquer ensuite la section 5.

6. Vérifier que tout va bien

# Site et API publics
curl.exe -s -o NUL -w "site %{http_code}`n" https://mathiscapart.xyz/
curl.exe -s https://mathiscapart.xyz/api/health          # {"status":"ok"}

# Services natifs : 8000 sur 192.168.1.75, 8001 et 6333 sur 127.0.0.1
netstat -ano | findstr LISTENING | findstr ":8000 :8001 :6333 :11434"

# Conteneurs
docker ps --format "{{.Names}} {{.Status}}"

# Modèles chargés et VRAM
ollama ps

Contrôles de sécurité attendus :

Contrôle Résultat attendu
https://mathiscapart.xyz/api/docs 404
en-tête content-security-policy sur / présent
WebSocket /voice depuis une autre origine refusé (403)
6333 depuis une autre machine du réseau connexion refusée
8000 depuis une machine du réseau autre que le serveur front connexion refusée (pare-feu)
site avec le PC éteint page servie, message « hors ligne »

7. Journaux

Service Emplacement
API deploy\logs\api.err.log (uvicorn écrit sur stderr)
TTS deploy\logs\tts.err.log
Traefik docker logs portfolio-traefik-1
cloudflared docker logs portfolio-cloudflared-1
Qdrant docker logs qdrant

Les journaux natifs sont écrasés à chaque lancement de start-natif.ps1.

8. Dépannage

Symptôme Cause probable Correction
Erreur Cloudflare 502/1033 sur tout le site conteneurs arrêtés ou Docker Desktop fermé section 3, étape 3
Le site s'affiche mais « L'assistant vocal ne répond pas » API arrêtée ou en cours de démarrage start-natif.ps1, attendre startup complete
« Une session vocale est déjà en cours. » une seule conversation à la fois (un seul GPU) attendre la fin de l'autre session ; elle est coupée après 10 s de silence réseau
Premier son très lent (~10 s) après une période calme Ollama a déchargé le LLM (5 min d'inactivité) normal ; ollama ps montre le modèle rechargé
Réponse vocale très lente, LLM à ~10 t/s VRAM saturée : LLM chargé avec un contexte trop grand vérifier OPTIONS_LLM = {"num_ctx": 8192} dans l'API ; ollama ps doit montrer ~6,3 Go
Voix muette mais texte affiché service TTS arrêté vérifier que le port 8001 écoute ; relancer le TTS
Voix d'Estelle au lieu de la voix clonée mathis.safetensors absent au lancement exporter la voix (section 2), relancer le TTS
WebSocket refusé (403) depuis le vrai site CORS_ORIGINS ne contient pas le domaine lancer via start-natif.ps1 ou corriger la variable
Modification Traefik sans effet watch inopérant sous Windows docker restart portfolio-traefik-1
Un nouveau fichier statique répond 404 juste après le build 404 gardé en cache par Cloudflare attendre l'expiration (~quelques minutes)
docker compose -f deploy/... : TRAEFIK_DOMAIN manquant .env cherché dans deploy/ ajouter --env-file .env
L'assistant invente un souhait, un poste recherché information absente du corpus l'ajouter au corpus ; le prompt interdit déjà de l'inventer

9. Sécurité : l'essentiel

  • TTS (8001) et Qdrant (6333) écoutent sur 127.0.0.1. L'API (8000) écoute sur 192.168.1.75 pour que Traefik la joigne depuis le serveur front : le pare-feu Windows doit n'y laisser entrer que le serveur front, sinon une machine du réseau local contournerait le rate-limit. Ne jamais passer Qdrant à 0.0.0.0 : il n'a pas de clé API.
  • Attention aux règles « Python » du pare-feu : Windows en a créé qui autorisent python.exe en entrée sur tous les ports depuis n'importe où. Tant qu'elles existent, une règle limitée au serveur front ne sert à rien (une règle d'autorisation suffit à laisser passer). Cf. section 10.
  • Ollama écoute encore sur 0.0.0.0 (OLLAMA_HOST au niveau machine) : point ouvert de l'audit. Le passer à 127.0.0.1 le fermerait au réseau local sans gêner les conteneurs.
  • Protections côté Traefik : rate-limit 30 req/min par IP visiteur (Cf-Connecting-Ip), HSTS, CSP, Permissions-Policy (micro réservé au site).
  • Protections côté API : /docs fermé, contrôle d'Origin sur /voice, une session vocale à la fois, question limitée à 30 s, socket coupé après 10 s de silence, erreurs génériques.
  • Secrets et données personnelles : .env (jeton du tunnel) et backend/tts/voix/ (ta voix) ne sont jamais versionnés. Gitleaks vérifie l'historique en CI.
  • Dépendances : Dependabot chaque semaine ; pip-audit et npm audit en CI. Pour auditer les venvs de prod : pip-audit --path .venv-rocm/Lib/site-packages.

10. Mise en place du serveur front

Ordre à respecter : le tunnel ne doit jamais tourner sur les deux machines à la fois avec le même jeton. Cloudflare répartirait les visiteurs entre les deux connecteurs.

  1. Pare-feu du PC (PowerShell administrateur). Désactiver les règles génériques créées par Windows pour Python, puis n'autoriser que le serveur front :
    Get-NetFirewallApplicationFilter | Where-Object Program -match 'python' | Get-NetFirewallRule | Disable-NetFirewallRule
    New-NetFirewallRule -DisplayName "PortFolio-AI API depuis le serveur front" -Direction Inbound `
        -Protocol TCP -LocalPort 8000 -LocalAddress 192.168.1.75 -RemoteAddress <IP du serveur front> -Action Allow
  2. API sur l'IP locale : $env:API_HOST = "192.168.1.75" puis start-natif.ps1. Vérifier depuis le serveur front : curl -s http://192.168.1.75:8000/health.
  3. Sur le serveur front : cloner le dépôt, créer .env à la racine (TRAEFIK_DOMAIN, CLOUDFLARE_TUNNEL_TOKEN, repris du PC), copier frontend/out/ (section 5).
  4. Couper l'ancienne stack sur le PC : docker compose --env-file .env -f deploy/docker-compose.expose.yml down.
  5. Démarrer la stack sur le serveur front : docker compose --env-file .env -f deploy/docker-compose.expose.yml up -d.
  6. Vérifier (section 6), puis éteindre l'API sur le PC et vérifier que le site reste en ligne avec le message « hors ligne ».

Sur Debian, le watch du provider file de Traefik fonctionne probablement (la limite constatée venait du montage Windows) : non vérifié, redémarrer Traefik reste la méthode sûre.