Skip to content

Commit 45e463a

Browse files
authored
Merge pull request #5 from mathiscapart/mathiscapart/check-notion-portfolio
Correctifs mobile Safari, script d'arrêt et documentation
2 parents 354310d + 9adf664 commit 45e463a

13 files changed

Lines changed: 609 additions & 123 deletions

File tree

‎.env.example‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ OLLAMA_PORT=11434
1010
OLLAMA_CHAT_MODEL=qwen3:8b
1111
OLLAMA_TIMEOUT=60
1212
CORS_ORIGINS=http://localhost:3000
13+
14+
# Exposition publique (deploy/docker-compose.expose.yml)
15+
TRAEFIK_DOMAIN=mathiscapart.xyz
16+
CLOUDFLARE_TUNNEL_TOKEN=a-renseigner-ne-jamais-committer
17+
18+
# Services natifs (valeurs par defaut de deploy/start-natif.ps1)
19+
TTS_URL=http://127.0.0.1:8001
20+
TTS_VOIX=estelle

‎README.md‎

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,51 @@
11
# PortFolio-AI
22

3-
## Prérequis
4-
5-
- Docker et Docker Compose.
6-
- [Ollama](https://ollama.com) installé et lancé **nativement sur l'hôte** (pas en conteneur :
7-
les wheels ROCm utilisés ne sont disponibles que pour Windows, le GPU AMD ne passe pas
8-
vers un conteneur Linux).
9-
10-
Par défaut, Ollama n'écoute que sur `127.0.0.1` et n'est donc pas joignable depuis le
11-
conteneur `rag`. Pour l'exposer :
12-
13-
1. Définir la variable d'environnement Windows `OLLAMA_HOST=0.0.0.0`, puis redémarrer Ollama.
14-
2. Autoriser le port `11434` dans le pare-feu Windows.
15-
16-
Sans cette étape, le conteneur `rag` échoue avec des erreurs `connection refused` en
17-
essayant de joindre Ollama.
18-
19-
## Configuration
20-
21-
Copier `.env.example` en `.env` et ajuster les valeurs si besoin.
22-
23-
## Lancement
24-
25-
```bash
26-
docker compose up --build
27-
```
28-
29-
## API
30-
31-
Elle tourne en natif, pas en conteneur :
3+
Portfolio vocal de Mathis Capart, en ligne sur https://mathiscapart.xyz. Le
4+
visiteur pose sa question à voix haute ; l'assistant répond avec une copie de la
5+
voix de Mathis, uniquement à partir de son parcours.
6+
7+
Chaîne : reconnaissance vocale Kyutai (GPU AMD, ROCm) → recherche Qdrant →
8+
`qwen3:8b` via Ollama → synthèse Pocket TTS. Tout tourne en local sur un homelab
9+
Windows, exposé par Traefik et un tunnel Cloudflare, sans API d'inférence externe.
10+
11+
## Structure
12+
13+
| Dossier | Contenu |
14+
|---|---|
15+
| `backend/api/` | API FastAPI : `/health`, `/chat` (SSE), `/voice` (WebSocket) |
16+
| `backend/stt/` | reconnaissance vocale Kyutai en streaming |
17+
| `backend/tts/` | service de synthèse vocale (voix clonée dans `voix/`, non versionné) |
18+
| `backend/rag/` | découpage, ingestion et recherche ; `corpus/` = le parcours |
19+
| `frontend/` | Next.js en export statique : interface vocale et page parcours |
20+
| `deploy/` | Traefik, nginx, cloudflared, script de démarrage des services natifs |
21+
| `.github/` | CI et Dependabot |
22+
23+
## Démarrage rapide
24+
25+
```powershell
26+
docker compose up -d qdrant
27+
powershell -ExecutionPolicy Bypass -File deploy\start-natif.ps1
28+
docker compose --env-file .env -f deploy/docker-compose.expose.yml up -d
3229
33-
```
34-
python -m uvicorn backend.api.main:app --port 8000
30+
# Arrêter l'API et le TTS
31+
powershell -ExecutionPolicy Bypass -File deploy\stop-natif.ps1
3532
```
3633

37-
`GET /health` renvoie l'etat de Qdrant et d'Ollama.
38-
`POST /chat` streame la reponse en SSE, puis un evenement `sources` terminal.
34+
## Documentation
3935

40-
## Exposition publique
36+
Toute la documentation est dans [`docs/`](docs/) :
4137

42-
Configuration dans `deploy/` : Traefik en frontal, nginx pour l'export statique
43-
du front, cloudflared pour le tunnel. L'API et Ollama restent natifs sur l'hote,
44-
joints via `host.docker.internal`.
38+
| Document | Contenu |
39+
|---|---|
40+
| [exploitation.md](docs/exploitation.md) | architecture, démarrage, déploiement, arrêt, vérifications, dépannage, sécurité |
41+
| [frontend.md](docs/frontend.md) | front Next.js : variables de build, commandes, spécificités mobiles, tests |
42+
| [corpus-demo.md](docs/corpus-demo.md) | corpus fictif de démonstration |
4543

46-
Prerequis, dans l'ordre :
44+
Conventions et décisions techniques mesurées : [CLAUDE.md](CLAUDE.md).
4745

48-
1. `npm run build` dans `frontend/` avec `NEXT_PUBLIC_API_URL=https://<domaine>/api`
49-
2. l'API lancee : `python -m uvicorn backend.api.main:app --port 8000`
50-
3. `TRAEFIK_DOMAIN` et `CLOUDFLARE_TUNNEL_TOKEN` renseignes
51-
4. la route DNS creee cote Cloudflare vers ce tunnel
46+
## Tests
5247

53-
Lancement (le `--env-file` est obligatoire : compose cherche sinon `deploy/.env`) :
54-
55-
```
56-
docker compose --env-file .env -f deploy/docker-compose.expose.yml up -d
48+
```powershell
49+
python -m pytest backend/tests # backend, sans GPU
50+
cd frontend; npx vitest run; npx tsc --noEmit
5751
```
58-
59-
Un rate-limit est pose sur `/api` : l'endpoint de chat n'est pas authentifie et
60-
s'adosse a une seule carte graphique.

‎backend/api/main.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pydantic import BaseModel, Field, field_validator
2323
from qdrant_client.http.exceptions import ResponseHandlingException, UnexpectedResponse
2424
from starlette.concurrency import run_in_threadpool
25+
from starlette.websockets import WebSocketState
2526

2627
# Triton est absent des wheels ROCm Windows : le STT (Moshi) passe par
2728
# torch.compile et leve TritonMissing sans repli en mode eager. Doit etre
@@ -424,6 +425,10 @@ async def voice(websocket: WebSocket):
424425
await websocket.accept()
425426
try:
426427
await _gerer_session_vocale(websocket)
428+
# Fermeture explicite (1000) : sans trame de fermeture, Safari iOS voit
429+
# une coupure anormale et émet un "error" tardif côté navigateur.
430+
if websocket.client_state == WebSocketState.CONNECTED:
431+
await websocket.close()
427432
except WebSocketDisconnect:
428433
pass
429434
finally:

‎backend/rag/corpus_demo/README.md‎

Lines changed: 0 additions & 5 deletions
This file was deleted.

‎backend/tests/test_securite_api.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,18 @@ def test_voice_refuse_une_origine_etrangere():
2020
with client.websocket_connect("/voice", headers={"origin": "https://site-malveillant.example"}) as ws:
2121
ws.receive_json()
2222
assert erreur.value.code == 1008
23+
24+
25+
def test_voice_ferme_proprement_la_connexion_en_fin_de_session(monkeypatch):
26+
"""Sans trame de fermeture, Safari iOS voit une coupure anormale et émet
27+
un événement "error" tardif qui cassait la question suivante."""
28+
async def session_vide(websocket):
29+
await websocket.send_json({"type": "error", "message": "Aucune parole detectee."})
30+
31+
monkeypatch.setattr(api_main, "_gerer_session_vocale", session_vide)
32+
monkeypatch.setattr(api_main.app.state, "session_vocale_active", False, raising=False)
33+
with client.websocket_connect("/voice", headers={"origin": "http://localhost:3000"}) as ws:
34+
ws.receive_json()
35+
message = ws.receive()
36+
assert message["type"] == "websocket.close"
37+
assert message["code"] == 1000

‎deploy/stop-natif.ps1‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Arrete les deux process natifs lances par start-natif.ps1 :
2+
# - API (127.0.0.1:8000) : le vocal et /api tombent, le site statique reste en ligne
3+
# - TTS (127.0.0.1:8001)
4+
# Les conteneurs (Qdrant, nginx, Traefik, cloudflared) ne sont pas touches :
5+
# voir docs/exploitation.md, section 4, pour couper aussi le site.
6+
# Usage : powershell -ExecutionPolicy Bypass -File deploy\stop-natif.ps1 [-WhatIf]
7+
[CmdletBinding(SupportsShouldProcess)]
8+
param()
9+
10+
# Chargement reel du module meme sous -WhatIf (sinon il simule la creation de ses alias).
11+
$simulation = $WhatIfPreference
12+
$WhatIfPreference = $false
13+
Import-Module NetTCPIP
14+
$WhatIfPreference = $simulation
15+
16+
$services = @(
17+
@{ Port = 8000; Nom = "API" },
18+
@{ Port = 8001; Nom = "TTS" }
19+
)
20+
21+
foreach ($service in $services) {
22+
$port = $service.Port
23+
$nom = $service.Nom
24+
$ecoutes = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue)
25+
if ($ecoutes.Count -eq 0) {
26+
Write-Output "$nom (port $port) : deja arrete"
27+
continue
28+
}
29+
foreach ($id in ($ecoutes.OwningProcess | Sort-Object -Unique)) {
30+
if ($PSCmdlet.ShouldProcess("$nom (port $port, PID $id)", "Arreter")) {
31+
Stop-Process -Id $id -Force
32+
Write-Output "$nom (port $port) : arrete (PID $id)"
33+
}
34+
}
35+
}

‎docs/corpus-demo.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Corpus de démonstration
2+
3+
Concerne le dossier `backend/rag/corpus_demo/`.
4+
5+
Données de démonstration entièrement fictives (persona « Camille Verne »),
6+
générées pour exercer la chaîne d'ingestion et de recherche avant que le vrai
7+
corpus de `backend/rag/corpus/` soit rédigé. Il sert encore à tester le site sans
8+
données personnelles (`CORPUS_DIR=../backend/rag/corpus_demo`, voir
9+
[frontend.md](frontend.md)).
10+
11+
Ne jamais servir ce répertoire en production : il ne décrit personne de réel.

0 commit comments

Comments
 (0)