DataSage12 commited on
Commit
de63014
·
0 Parent(s):

Initial commit - HOLOKIA-AVATAR v2.2

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +111 -0
  2. .gitignore +175 -0
  3. Back-end/README.md +118 -0
  4. Back-end/app/__init__.py +0 -0
  5. Back-end/app/main.py +69 -0
  6. Back-end/requirements.txt +12 -0
  7. Back-end/services/live_stream_service.py +445 -0
  8. Back-end/services/llm_service.py +168 -0
  9. Back-end/services/stt_service.py +180 -0
  10. Back-end/services/tts_service.py +119 -0
  11. Back-end/start_services.py +121 -0
  12. Dockerfile +92 -0
  13. README.md +45 -0
  14. app.py +221 -0
  15. clean_and_restructure.ps1 +278 -0
  16. clean_and_restructure.sh +259 -0
  17. debug_websocket.py +48 -0
  18. deploy_to_hf_final.ps1 +252 -0
  19. env.example +37 -0
  20. frontend/index.html +12 -0
  21. frontend/package-lock.json +0 -0
  22. frontend/package.json +42 -0
  23. frontend/public/images/holokia.jpeg +3 -0
  24. frontend/public/images/wawasensei-white.png +3 -0
  25. frontend/public/images/wawasensei.png +3 -0
  26. frontend/public/pcm-worklet.js +108 -0
  27. frontend/public/textures/Rotated/flame_05_rotated.png +3 -0
  28. frontend/public/textures/Rotated/flame_06_rotated.png +3 -0
  29. frontend/public/textures/Rotated/muzzle_01_rotated.png +3 -0
  30. frontend/public/textures/Rotated/muzzle_02_rotated.png +3 -0
  31. frontend/public/textures/Rotated/muzzle_03_rotated.png +3 -0
  32. frontend/public/textures/Rotated/muzzle_04_rotated.png +3 -0
  33. frontend/public/textures/Rotated/muzzle_05_rotated.png +3 -0
  34. frontend/public/textures/Rotated/spark_05_rotated.png +3 -0
  35. frontend/public/textures/Rotated/spark_06_rotated.png +3 -0
  36. frontend/public/textures/Rotated/trace_01_rotated.png +3 -0
  37. frontend/public/textures/Rotated/trace_02_rotated.png +3 -0
  38. frontend/public/textures/Rotated/trace_03_rotated.png +3 -0
  39. frontend/public/textures/Rotated/trace_04_rotated.png +3 -0
  40. frontend/public/textures/Rotated/trace_05_rotated.png +3 -0
  41. frontend/public/textures/Rotated/trace_06_rotated.png +3 -0
  42. frontend/public/textures/Rotated/trace_07_rotated.png +3 -0
  43. frontend/public/textures/circle_01.png +3 -0
  44. frontend/public/textures/circle_02.png +3 -0
  45. frontend/public/textures/circle_03.png +3 -0
  46. frontend/public/textures/circle_04.png +3 -0
  47. frontend/public/textures/circle_05.png +3 -0
  48. frontend/public/textures/dirt_01.png +3 -0
  49. frontend/public/textures/dirt_02.png +3 -0
  50. frontend/public/textures/dirt_03.png +3 -0
.gitattributes ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Configuration Git LFS pour HOLOKIA-AVATAR
3
+ # ===========================================
4
+ # Modèles 3D
5
+ *.glb filter=lfs diff=lfs merge=lfs -text
6
+ *.fbx filter=lfs diff=lfs merge=lfs -text
7
+ *.obj filter=lfs diff=lfs merge=lfs -text
8
+ *.dae filter=lfs diff=lfs merge=lfs -text
9
+ *.3ds filter=lfs diff=lfs merge=lfs -text
10
+ *.blend filter=lfs diff=lfs merge=lfs -text
11
+ *.max filter=lfs diff=lfs merge=lfs -text
12
+ *.ma filter=lfs diff=lfs merge=lfs -text
13
+ *.mb filter=lfs diff=lfs merge=lfs -text
14
+ *.c4d filter=lfs diff=lfs merge=lfs -text
15
+ *.x3d filter=lfs diff=lfs merge=lfs -text
16
+ *.ply filter=lfs diff=lfs merge=lfs -text
17
+ *.stl filter=lfs diff=lfs merge=lfs -text
18
+ *.wrl filter=lfs diff=lfs merge=lfs -text
19
+ *.3dm filter=lfs diff=lfs merge=lfs -text
20
+ *.3dmf filter=lfs diff=lfs merge=lfs -text
21
+ *.ac filter=lfs diff=lfs merge=lfs -text
22
+ *.ac3d filter=lfs diff=lfs merge=lfs -text
23
+ *.acc filter=lfs diff=lfs merge=lfs -text
24
+ *.ase filter=lfs diff=lfs merge=lfs -text
25
+ *.ask filter=lfs diff=lfs merge=lfs -text
26
+ *.b3d filter=lfs diff=lfs merge=lfs -text
27
+ *.bvh filter=lfs diff=lfs merge=lfs -text
28
+ *.cob filter=lfs diff=lfs merge=lfs -text
29
+ *.csm filter=lfs diff=lfs merge=lfs -text
30
+ *.dxf filter=lfs diff=lfs merge=lfs -text
31
+ *.enff filter=lfs diff=lfs merge=lfs -text
32
+ *.hmp filter=lfs diff=lfs merge=lfs -text
33
+ *.irrmesh filter=lfs diff=lfs merge=lfs -text
34
+ *.irr filter=lfs diff=lfs merge=lfs -text
35
+ *.lwo filter=lfs diff=lfs merge=lfs -text
36
+ *.lws filter=lfs diff=lfs merge=lfs -text
37
+ *.lxo filter=lfs diff=lfs merge=lfs -text
38
+ *.md2 filter=lfs diff=lfs merge=lfs -text
39
+ *.md3 filter=lfs diff=lfs merge=lfs -text
40
+ *.md5anim filter=lfs diff=lfs merge=lfs -text
41
+ *.md5camera filter=lfs diff=lfs merge=lfs -text
42
+ *.md5mesh filter=lfs diff=lfs merge=lfs -text
43
+ *.mdc filter=lfs diff=lfs merge=lfs -text
44
+ *.mdl filter=lfs diff=lfs merge=lfs -text
45
+ *.mesh filter=lfs diff=lfs merge=lfs -text
46
+ *.mesh.xml filter=lfs diff=lfs merge=lfs -text
47
+ *.mot filter=lfs diff=lfs merge=lfs -text
48
+ *.ms3d filter=lfs diff=lfs merge=lfs -text
49
+ *.ndo filter=lfs diff=lfs merge=lfs -text
50
+ *.nff filter=lfs diff=lfs merge=lfs -text
51
+ *.off filter=lfs diff=lfs merge=lfs -text
52
+ *.ogex filter=lfs diff=lfs merge=lfs -text
53
+ *.pmx filter=lfs diff=lfs merge=lfs -text
54
+ *.prj filter=lfs diff=lfs merge=lfs -text
55
+ *.q3o filter=lfs diff=lfs merge=lfs -text
56
+ *.q3s filter=lfs diff=lfs merge=lfs -text
57
+ *.raw filter=lfs diff=lfs merge=lfs -text
58
+ *.scn filter=lfs diff=lfs merge=lfs -text
59
+ *.smd filter=lfs diff=lfs merge=lfs -text
60
+ *.ter filter=lfs diff=lfs merge=lfs -text
61
+ *.uc filter=lfs diff=lfs merge=lfs -text
62
+ *.vta filter=lfs diff=lfs merge=lfs -text
63
+ *.x filter=lfs diff=lfs merge=lfs -text
64
+ *.xgl filter=lfs diff=lfs merge=lfs -text
65
+ *.xml filter=lfs diff=lfs merge=lfs -text
66
+ *.zae filter=lfs diff=lfs merge=lfs -text
67
+ *.zgl filter=lfs diff=lfs merge=lfs -text
68
+ # Textures et images volumineuses
69
+ *.png filter=lfs diff=lfs merge=lfs -text
70
+ *.jpg filter=lfs diff=lfs merge=lfs -text
71
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
72
+ *.tga filter=lfs diff=lfs merge=lfs -text
73
+ *.tiff filter=lfs diff=lfs merge=lfs -text
74
+ *.tif filter=lfs diff=lfs merge=lfs -text
75
+ *.bmp filter=lfs diff=lfs merge=lfs -text
76
+ *.exr filter=lfs diff=lfs merge=lfs -text
77
+ *.hdr filter=lfs diff=lfs merge=lfs -text
78
+ # Audio volumineux
79
+ *.wav filter=lfs diff=lfs merge=lfs -text
80
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
81
+ *.ogg filter=lfs diff=lfs merge=lfs -text
82
+ *.flac filter=lfs diff=lfs merge=lfs -text
83
+ *.aac filter=lfs diff=lfs merge=lfs -text
84
+ # Vidéos
85
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
86
+ *.avi filter=lfs diff=lfs merge=lfs -text
87
+ *.mov filter=lfs diff=lfs merge=lfs -text
88
+ *.mkv filter=lfs diff=lfs merge=lfs -text
89
+ *.webm filter=lfs diff=lfs merge=lfs -text
90
+ # Archives
91
+ *.zip filter=lfs diff=lfs merge=lfs -text
92
+ *.rar filter=lfs diff=lfs merge=lfs -text
93
+ *.7z filter=lfs diff=lfs merge=lfs -text
94
+ *.tar.gz filter=lfs diff=lfs merge=lfs -text
95
+ *.tar.bz2 filter=lfs diff=lfs merge=lfs -text
96
+ # Fichiers binaires
97
+ *.bin filter=lfs diff=lfs merge=lfs -text
98
+ *.dat filter=lfs diff=lfs merge=lfs -text
99
+ *.db filter=lfs diff=lfs merge=lfs -text
100
+ *.sqlite filter=lfs diff=lfs merge=lfs -text
101
+ *.sqlite3 filter=lfs diff=lfs merge=lfs -text
102
+ # Fichiers de cache et temporaires
103
+ *.cache filter=lfs diff=lfs merge=lfs -text
104
+ *.tmp filter=lfs diff=lfs merge=lfs -text
105
+ *.temp filter=lfs diff=lfs merge=lfs -text
106
+ # Fichiers de logs volumineux
107
+ *.log filter=lfs diff=lfs merge=lfs -text
108
+ # Fichiers de sauvegarde
109
+ *.bak filter=lfs diff=lfs merge=lfs -text
110
+ *.backup filter=lfs diff=lfs merge=lfs -text
111
+ *.ico filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # .gitignore pour HOLOKIA-AVATAR
3
+ # ===========================================
4
+
5
+ # Fichiers Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # Environnements virtuels
29
+ .env
30
+ .venv
31
+ env/
32
+ venv/
33
+ ENV/
34
+ env.bak/
35
+ venv.bak/
36
+
37
+ # Variables d'environnement (IMPORTANT pour HF)
38
+ .env
39
+ .env.*
40
+ !.env.example
41
+
42
+ # IDE
43
+ .vscode/
44
+ .idea/
45
+ *.swp
46
+ *.swo
47
+ *~
48
+
49
+ # Logs
50
+ *.log
51
+ logs/
52
+ npm-debug.log*
53
+ yarn-debug.log*
54
+ yarn-error.log*
55
+
56
+ # Node.js
57
+ node_modules/
58
+ npm-debug.log*
59
+ yarn-debug.log*
60
+ yarn-error.log*
61
+ lerna-debug.log*
62
+
63
+ # Builds
64
+ dist/
65
+ build/
66
+ .cache/
67
+
68
+ # Fichiers temporaires
69
+ tmp/
70
+ temp/
71
+ *.tmp
72
+ *.temp
73
+
74
+ # Fichiers système
75
+ .DS_Store
76
+ Thumbs.db
77
+
78
+ # Cache TTS
79
+ Back-end/tts_cache/
80
+ tts_cache/
81
+
82
+ # Fichiers de configuration locale
83
+ .env.local
84
+ .env.development.local
85
+ .env.test.local
86
+ .env.production.local
87
+
88
+ # Fichiers de test
89
+ coverage/
90
+ .nyc_output/
91
+ .pytest_cache/
92
+
93
+ # Docker
94
+ .dockerignore
95
+
96
+ # Fichiers de déploiement (optionnels)
97
+ deploy_to_hf.sh
98
+ test_deployment.py
99
+ README_HF_DEPLOYMENT.md
100
+
101
+ # Fichiers de développement
102
+ build-frontend.sh
103
+ docker-compose*.yml
104
+ docker/
105
+
106
+ # Fichiers Git LFS (seront gérés par LFS)
107
+ *.glb
108
+ *.fbx
109
+ *.obj
110
+ *.dae
111
+ *.3ds
112
+ *.blend
113
+ *.max
114
+ *.ma
115
+ *.mb
116
+ *.c4d
117
+ *.fbx
118
+ *.x3d
119
+ *.ply
120
+ *.stl
121
+ *.wrl
122
+ *.x3d
123
+ *.3dm
124
+ *.3dmf
125
+ *.ac
126
+ *.ac3d
127
+ *.acc
128
+ *.ase
129
+ *.ask
130
+ *.b3d
131
+ *.bvh
132
+ *.cob
133
+ *.csm
134
+ *.dae
135
+ *.dxf
136
+ *.enff
137
+ *.hmp
138
+ *.irrmesh
139
+ *.irr
140
+ *.lwo
141
+ *.lws
142
+ *.lxo
143
+ *.md2
144
+ *.md3
145
+ *.md5anim
146
+ *.md5camera
147
+ *.md5mesh
148
+ *.mdc
149
+ *.mdl
150
+ *.mesh
151
+ *.mesh.xml
152
+ *.mot
153
+ *.ms3d
154
+ *.ndo
155
+ *.nff
156
+ *.off
157
+ *.ogex
158
+ *.ply
159
+ *.pmx
160
+ *.prj
161
+ *.q3o
162
+ *.q3s
163
+ *.raw
164
+ *.scn
165
+ *.smd
166
+ *.stl
167
+ *.ter
168
+ *.uc
169
+ *.vta
170
+ *.x
171
+ *.x3d
172
+ *.xgl
173
+ *.xml
174
+ *.zae
175
+ *.zgl
Back-end/README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HOLOKIA-AVATAR Backend
2
+
3
+ Ce dossier contient le backend du projet: il gère la génération de texte IA (LLM), la synthèse vocale (TTS), la reconnaissance vocale (STT), et le streaming audio en temps réel avec Websocket.
4
+
5
+ ---
6
+
7
+ ## Structure du dossier
8
+
9
+ ```
10
+ Back-end/
11
+ ├── .env # Variables d'environnement (API keys, config)
12
+ ├── requirements.txt # Dépendances Python
13
+ ├── start_service.py # Script de lancement des services # Script de démarrage principal
14
+ |__ tts_cache # Le cache
15
+ ├── app/
16
+ │ ├── __init__.py
17
+ │ └── main.py # API principale FastAPI
18
+ ├── env/ # Environnement virtuel Python (venv)
19
+ ├── services/
20
+ │ ├── live_stream_service.py
21
+ │ ├── llm_service.py
22
+ │ ├── stt_service.py
23
+ │ ├── tts_service.py
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Prérequis
29
+
30
+ - Python 3.10 ou supérieur
31
+ - Installer les dépendances :
32
+ ```sh
33
+ pip install -r requirements.txt
34
+ ```
35
+ - Configurer le fichier `.env` avec vos clés API et paramètres nécessaires (voir exemple ci-dessous).
36
+
37
+ ---
38
+
39
+ ## Configuration
40
+
41
+ Exemple de fichier `.env` :
42
+ ```
43
+ GROQ_API_KEY=your_groq_api_key
44
+
45
+ ```
46
+
47
+ Adaptez les clés et chemins selon votre environnement.
48
+
49
+ ---
50
+
51
+ ## Lancement du backend
52
+
53
+ ### Démarrage global
54
+
55
+ ```sh
56
+ python start_services.py
57
+ ```
58
+ Ce script lance tous les services backend (API principale, TTS, STT, streaming...).
59
+
60
+ ### Démarrage manuel de l’API FastAPI
61
+
62
+ ```sh
63
+ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Fonctionnalités principales
69
+
70
+ - **Reconnaissance vocale (STT)**
71
+ Endpoint : `POST /stt/transcribe`
72
+ Envoie un fichier audio, reçoit le texte transcrit.
73
+
74
+ - **Synthèse vocale (TTS)**
75
+ Endpoint : `POST /tts/generate-tts`
76
+ Envoie du texte, reçoit le chemin du fichier audio généré.
77
+
78
+ - **Génération de texte IA (LLM)**
79
+ Endpoint : `POST /llm/generate`
80
+ Envoie un prompt, reçoit la réponse générée.
81
+
82
+ - **Streaming audio en temps réel**
83
+ Endpoint : `/live_stream/{user_id}`
84
+ Permet la communication audio temps réel avec le backend.
85
+
86
+ - **Health check**
87
+ Endpoint : `GET /health`
88
+ Vérifie l’état du backend.
89
+
90
+ ---
91
+
92
+ ## Dossiers de cache et de test
93
+
94
+ - `tts_cache/` : fichiers audio générés par le TTS
95
+
96
+ ---
97
+
98
+ ## Bonnes pratiques
99
+
100
+ - Utilisez l’environnement virtuel fourni (`env/`) pour isoler les dépendances.
101
+ - Protégez votre fichier `.env` et ne le partagez pas publiquement.
102
+ - Consultez et adaptez `lipsync_config.yaml` selon vos besoins de synchronisation labiale.
103
+ - En production, restreignez les origines CORS dans `main.py`.
104
+
105
+ ---
106
+
107
+ ## Contribution
108
+
109
+ Pour toute modification :
110
+ - Documentez votre code
111
+ - Ajoutez des tests si nécessaire
112
+ - Respectez la structure existante
113
+
114
+ ---
115
+
116
+ ## Support
117
+
118
+ Pour toute question ou problème, Veillez contacter Holokia particuliérement Mr Sinaly ou Mr Omar.
Back-end/app/__init__.py ADDED
File without changes
Back-end/app/main.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/main.py
2
+ from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from services import stt_service, tts_service, llm_service, live_stream_service # type: ignore
5
+ import uvicorn
6
+
7
+ app = FastAPI(
8
+ title="Holokia Avatar Backend",
9
+ description="Backend pour avatar 3D interactif avec STT, TTS, LLM et LiveKit streaming",
10
+ version="1.0.0"
11
+ )
12
+
13
+ # ✅ Activer CORS pour éviter les blocages côté front
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"], # ⚠️ En prod, mettre le domaine du front
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ # 🩺 Health check
23
+ @app.get("/health")
24
+ async def health():
25
+ return {"status": "ok", "message": "Backend operational"}
26
+
27
+ # 🎤 STT - Audio → Texte
28
+ @app.post("/stt/transcribe")
29
+ async def transcribe_audio(file: UploadFile = File(...)):
30
+ try:
31
+ result = await stt_service.transcribe_audio(file)
32
+ return {"text": result}
33
+ except Exception as e:
34
+ return {"error": str(e)}
35
+
36
+ # 🗣 TTS - Texte → Audio
37
+ @app.post("/tts/generate")
38
+ async def generate_tts(text: str):
39
+ try:
40
+ audio_path = await tts_service.text_to_speech(text)
41
+ return {"audio_file": audio_path}
42
+ except Exception as e:
43
+ return {"error": str(e)}
44
+
45
+ # 🤖 LLM - Prompt → Texte
46
+ @app.post("/llm/generate")
47
+ async def generate_llm(prompt: str):
48
+ try:
49
+ response = await llm_service.generate_response(prompt)
50
+ return {"response": response}
51
+ except Exception as e:
52
+ return {"error": str(e)}
53
+
54
+ # 📡 Live streaming WebSocket
55
+ @app.websocket("/live-stream")
56
+ async def websocket_endpoint(websocket: WebSocket):
57
+ await websocket.accept()
58
+ try:
59
+ await live_stream_service.handle_connection(websocket)
60
+ except WebSocketDisconnect:
61
+ print("🔌 Client déconnecté du live stream")
62
+ except Exception as e:
63
+ print(f"❌ Erreur WebSocket : {e}")
64
+ finally:
65
+ await websocket.close()
66
+
67
+ # 🚀 Point d'entrée
68
+ if __name__ == "__main__":
69
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
Back-end/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115
2
+ uvicorn[standard]>=0.30
3
+ aiohttp>=3.9
4
+ python-multipart>=0.0.9
5
+ gTTS>=2.5
6
+ pydub>=0.25
7
+ faster-whisper>=1.0
8
+ ctranslate2>=4.6.0
9
+ langdetect>=1.0.9
10
+ httpx>=0.27
11
+ langchain-groq # si tu utilises Groq
12
+ langgraph
Back-end/services/live_stream_service.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import logging
4
+ import json
5
+ import asyncio
6
+ import aiohttp # type: ignore
7
+ import tempfile
8
+ import wave
9
+ import audioop
10
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
11
+ from fastapi.websockets import WebSocketState
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from typing import Dict, List, Optional, TypedDict
14
+ import uvicorn
15
+ from langgraph.graph import StateGraph, END, START
16
+ from tenacity import retry, stop_after_attempt, wait_exponential
17
+
18
+ # Configuration du logger
19
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", stream=sys.stdout)
20
+ logger = logging.getLogger("live_stream_service")
21
+
22
+ # URLs des micro-services
23
+ STT_SERVICE_URL = "http://localhost:5001/transcribe"
24
+ LLM_SERVICE_URL = "http://localhost:5002/generate"
25
+ TTS_SERVICE_URL = "http://localhost:5000/generate-tts"
26
+
27
+ # Paramètres audio
28
+ SAMPLE_RATE = 16000
29
+ CHANNELS = 1
30
+ SAMPLE_WIDTH = 2
31
+ SILENCE_THRESHOLD = 1000
32
+ SILENCE_DURATION = 1.0
33
+ MIN_AUDIO_DURATION = 1.0
34
+ MAX_CONNECTIONS_PER_ROOM = 100 # Limite de connexions par salle
35
+
36
+ app = FastAPI()
37
+
38
+ # CORS
39
+ origins = ["http://localhost:5173", "http://127.0.0.1:5173"]
40
+ app.add_middleware(
41
+ CORSMiddleware,
42
+ allow_origins=origins,
43
+ allow_credentials=True,
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+ # Gestion des salles et participants
49
+ rooms: Dict[str, List[WebSocket]] = {}
50
+ participants_lang: Dict[str, str] = {}
51
+ user_histories: Dict[str, List[Dict[str, str]]] = {} # Historique persistant par user_id
52
+
53
+ # État du graphe LangGraph
54
+ class ChatState(TypedDict):
55
+ audio_bytes: bytes
56
+ transcript: str
57
+ lang: Optional[str]
58
+ llm_response: str
59
+ audio_response: bytes
60
+ user_id: str
61
+ room_id: str
62
+ history: List[Dict[str, str]] # Historique: [{"role": "user|assistant", "content": "..."}]
63
+
64
+ async def safe_delete_file(file_path: str, max_attempts: int = 3, delay: float = 0.1) -> None:
65
+ """Supprime un fichier avec retry en cas de PermissionError."""
66
+ for attempt in range(max_attempts):
67
+ try:
68
+ if os.path.exists(file_path):
69
+ os.unlink(file_path)
70
+ logger.debug(f"Fichier supprimé: {file_path}")
71
+ return
72
+ except PermissionError as e:
73
+ logger.warning(f"Tentative {attempt + 1}/{max_attempts} de suppression de {file_path} échouée: {e}")
74
+ if attempt < max_attempts - 1:
75
+ await asyncio.sleep(delay)
76
+ except Exception as e:
77
+ logger.error(f"Erreur lors de la suppression de {file_path}: {e}")
78
+ return
79
+ logger.error(f"Échec de la suppression de {file_path} après {max_attempts} tentatives")
80
+
81
+ # Nœuds du graphe avec retry
82
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
83
+ async def transcribe_node(state: ChatState) -> ChatState:
84
+ """Appelle le service STT pour transcrire l'audio avec retry."""
85
+ audio_bytes = state.get("audio_bytes", b"")
86
+ user_id = state.get("user_id", "unknown")
87
+ print(f"[STT] Début transcription pour {user_id}, audio size: {len(audio_bytes)} bytes")
88
+
89
+ if not audio_bytes:
90
+ print("[STT] Audio vide, skip")
91
+ return {"transcript": "", "lang": state.get("lang", "fr")}
92
+
93
+ duration = len(audio_bytes) / (SAMPLE_RATE * SAMPLE_WIDTH * CHANNELS)
94
+ if duration < MIN_AUDIO_DURATION:
95
+ logger.debug(f"[STT] Audio trop court ({duration:.2f}s), skip")
96
+ return {"transcript": "", "lang": state.get("lang", "fr")}
97
+
98
+ try:
99
+ rms_global = audioop.rms(audio_bytes, SAMPLE_WIDTH)
100
+ logger.debug(f"[STT] RMS global: {rms_global}")
101
+ if rms_global < SILENCE_THRESHOLD:
102
+ logger.debug(f"[STT] Audio silencieux (RMS={rms_global}), skip")
103
+ return {"transcript": "", "lang": state.get("lang", "fr")}
104
+ except Exception as e:
105
+ logger.warning(f"[STT] Erreur RMS global: {e}")
106
+ return {"transcript": "", "lang": state.get("lang", "fr")}
107
+
108
+ tmp_path = None
109
+ file_handle = None
110
+ try:
111
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmpfile:
112
+ with wave.open(tmpfile.name, "wb") as wf:
113
+ wf.setnchannels(CHANNELS)
114
+ wf.setsampwidth(SAMPLE_WIDTH)
115
+ wf.setframerate(SAMPLE_RATE)
116
+ wf.writeframes(audio_bytes)
117
+ tmp_path = tmpfile.name
118
+
119
+ form = aiohttp.FormData()
120
+ file_handle = open(tmp_path, "rb")
121
+ form.add_field("file", file_handle, filename="audio.wav", content_type="audio/wav")
122
+ if state.get("lang"):
123
+ form.add_field("language", state["lang"])
124
+
125
+ async with aiohttp.ClientSession() as session:
126
+ async with session.post(STT_SERVICE_URL, data=form) as resp:
127
+ if resp.status != 200:
128
+ text = await resp.text()
129
+ raise RuntimeError(f"STT service error {resp.status}: {text}")
130
+ result = await resp.json()
131
+ transcript = result.get("transcript", "")
132
+ lang = result.get("lang", state.get("lang", "fr"))
133
+ if transcript.strip():
134
+ history = state.get("history", [])
135
+ history.append({"role": "user", "content": transcript})
136
+ return {"transcript": transcript, "lang": lang, "history": history}
137
+ return {"transcript": "", "lang": lang}
138
+ except Exception as e:
139
+ logger.error(f"[STT] Erreur: {e}", exc_info=True)
140
+ raise # Laisser tenacity gérer le retry
141
+ finally:
142
+ if file_handle:
143
+ file_handle.close()
144
+ if tmp_path:
145
+ await safe_delete_file(tmp_path)
146
+
147
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
148
+ async def llm_node(state: ChatState) -> ChatState:
149
+ """Appelle le service LLM avec le transcript et l’historique."""
150
+ transcript = state.get("transcript", "")
151
+ user_id = state.get("user_id", "")
152
+ if not transcript.strip():
153
+ logger.debug("[LLM] Transcript vide, skip")
154
+ return {"llm_response": ""}
155
+
156
+ # Construire le prompt avec l’historique
157
+ history = state.get("history", [])
158
+ logger.debug(f"[LLM] Historique envoyé: {history}")
159
+ try:
160
+ async with aiohttp.ClientSession() as session:
161
+ async with session.post(
162
+ LLM_SERVICE_URL,
163
+ json={
164
+ "text": transcript,
165
+ "user_id": user_id,
166
+ "history": history[-3:]
167
+ }
168
+ ) as resp:
169
+ if resp.status != 200:
170
+ text = await resp.text()
171
+ raise RuntimeError(f"LLM service error {resp.status}: {text}")
172
+ result = await resp.json()
173
+ response = result.get("response", "")
174
+ new_history = result.get("history", history)
175
+ if response.strip():
176
+ logger.info(f"[LLM] Réponse: {response}")
177
+ return {"llm_response": response, "history": new_history}
178
+ return {"llm_response": ""}
179
+ except Exception as e:
180
+ logger.error(f"[LLM] Erreur: {e}", exc_info=True)
181
+ raise # Laisser tenacity gérer le retry
182
+
183
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
184
+ async def tts_node(state: ChatState) -> ChatState:
185
+ """Appelle le service TTS pour générer l’audio."""
186
+ llm_response = state.get("llm_response", "")
187
+ if not llm_response.strip():
188
+ logger.debug("[TTS] Pas de réponse LLM, skip")
189
+ return {"audio_response": b""}
190
+
191
+ logger.debug(f"[TTS] Appel avec texte: {llm_response}, lang: {state['lang']}")
192
+ try:
193
+ async with aiohttp.ClientSession() as session:
194
+ async with session.post(TTS_SERVICE_URL, json={"text": llm_response, "lang": state["lang"]}) as resp:
195
+ if resp.status != 200:
196
+ text_resp = await resp.text()
197
+ raise RuntimeError(f"TTS service error {resp.status}: {text_resp}")
198
+ result = await resp.json()
199
+ audio_url = result.get("url")
200
+ if not audio_url:
201
+ raise RuntimeError("TTS no URL")
202
+
203
+ # Construire l'URL complète si c'est une URL relative
204
+ if audio_url.startswith("/"):
205
+ audio_url = f"http://localhost:5000{audio_url}"
206
+
207
+ async with session.get(audio_url) as audio_resp:
208
+ if audio_resp.status != 200:
209
+ raise RuntimeError(f"TTS file fetch error {audio_resp.status}")
210
+ audio_data = await audio_resp.read()
211
+ logger.info(f"[TTS] Audio généré, taille: {len(audio_data)} bytes")
212
+ return {"audio_response": audio_data}
213
+ except Exception as e:
214
+ logger.error(f"[TTS] Erreur: {e}", exc_info=True)
215
+ raise # Laisser tenacity gérer le retry
216
+
217
+ # Construiction du graphe LangGraph
218
+ workflow = StateGraph(ChatState)
219
+ workflow.add_node("transcribe", transcribe_node)
220
+ workflow.add_node("llm", llm_node)
221
+ workflow.add_node("tts", tts_node)
222
+ workflow.add_edge(START, "transcribe")
223
+ workflow.add_edge("transcribe", "llm")
224
+ workflow.add_edge("llm", "tts")
225
+ workflow.add_edge("tts", END)
226
+ graph = workflow.compile()
227
+
228
+ async def broadcast(room_id: str, message, sender: WebSocket = None):
229
+ """Diffuse message à la salle, sauf au sender."""
230
+ for ws in rooms.get(room_id, []):
231
+ if ws != sender and ws.application_state == WebSocketState.CONNECTED:
232
+ try:
233
+ if isinstance(message, bytes):
234
+ await ws.send_bytes(message)
235
+ else:
236
+ await ws.send_text(message)
237
+ except Exception:
238
+ rooms[room_id].remove(ws) if ws in rooms.get(room_id, []) else None
239
+
240
+ def is_silence(audio_chunk: bytes) -> bool:
241
+ """Vérifie si chunk est silence via RMS."""
242
+ if not audio_chunk or len(audio_chunk) < SAMPLE_WIDTH * 2:
243
+ return True
244
+ if len(audio_chunk) % SAMPLE_WIDTH != 0:
245
+ audio_chunk = audio_chunk[:-(len(audio_chunk) % SAMPLE_WIDTH)]
246
+ try:
247
+ rms = audioop.rms(audio_chunk, SAMPLE_WIDTH)
248
+ logger.debug(f"[is_silence] RMS={rms}")
249
+ return rms < SILENCE_THRESHOLD
250
+ except Exception as e:
251
+ logger.warning(f"[is_silence] Erreur RMS: {e}")
252
+ return True
253
+
254
+ @app.websocket("/live_stream/{user_id}")
255
+ async def websocket_endpoint(ws: WebSocket, user_id: str, lang: Optional[str] = None, room_id: str = "default"):
256
+ """Gère connexion WebSocket pour stream audio avec LangGraph."""
257
+ await ws.accept()
258
+ print(f"[Live] Connecté: {user_id} (lang={lang or 'auto'}, room={room_id})")
259
+ logger.info(f"[Live] Connecté: {user_id} (lang={lang or 'auto'}, room={room_id})")
260
+
261
+ if room_id not in rooms:
262
+ rooms[room_id] = []
263
+ if len(rooms[room_id]) >= MAX_CONNECTIONS_PER_ROOM:
264
+ logger.warning(f"[Live] Limite de connexions atteinte pour la salle {room_id}")
265
+ await ws.close(code=1000, reason="Salle pleine")
266
+ return
267
+ rooms[room_id].append(ws)
268
+ participants_lang[user_id] = lang or "auto"
269
+
270
+ # Initialise l'historique pour cet utilisateur s'il n'existe pas
271
+ if user_id not in user_histories:
272
+ user_histories[user_id] = []
273
+ history = user_histories[user_id]
274
+
275
+ buffer = bytearray()
276
+ silent_counter = 0.0
277
+
278
+ try:
279
+ while True:
280
+ try:
281
+ message = await asyncio.wait_for(ws.receive(), timeout=60)
282
+ print(f"[Live] Message reçu de {user_id}: {type(message)}")
283
+ except asyncio.TimeoutError:
284
+ print(f"[Live] Timeout WS pour {user_id}")
285
+ logger.warning(f"[Live] Timeout WS pour {user_id}")
286
+ break
287
+ except WebSocketDisconnect as e:
288
+ print(f"[Live] Disconnect ({e.code}) pour {user_id}")
289
+ logger.info(f"[Live] Disconnect ({e.code}) pour {user_id}")
290
+ break
291
+ except RuntimeError as e:
292
+ print(f"[Live] Receive error: {e}")
293
+ logger.info(f"[Live] Receive error: {e}")
294
+ break
295
+ except Exception as e:
296
+ print(f"[Live] ws.receive error: {e}")
297
+ logger.warning(f"[Live] ws.receive error: {e}")
298
+ break
299
+
300
+ if "bytes" in message:
301
+ chunk = message["bytes"]
302
+ if not chunk:
303
+ continue
304
+
305
+ if len(chunk) % SAMPLE_WIDTH != 0:
306
+ chunk = chunk[:-(len(chunk) % SAMPLE_WIDTH)]
307
+ logger.debug(f"[Live] Chunk aligné ({len(chunk)} bytes)")
308
+
309
+ buffer.extend(chunk)
310
+ frames = len(chunk) / SAMPLE_WIDTH
311
+ time_delta = frames / SAMPLE_RATE
312
+
313
+ if is_silence(chunk):
314
+ silent_counter += time_delta
315
+ else:
316
+ silent_counter = 0.0
317
+
318
+ if silent_counter >= SILENCE_DURATION and buffer:
319
+ audio_snapshot = bytes(buffer)
320
+ buffer.clear()
321
+ silent_counter = 0.0
322
+
323
+ # Exécute le graphe LangGraph
324
+ state = {
325
+ "audio_bytes": audio_snapshot,
326
+ "user_id": user_id,
327
+ "room_id": room_id,
328
+ "lang": lang or "fr",
329
+ "history": history,
330
+ "transcript": "",
331
+ "llm_response": "",
332
+ "audio_response": b""
333
+ }
334
+ try:
335
+ print(f"[Live] Exécution du graphe pour {user_id}, audio size: {len(audio_snapshot)} bytes")
336
+ result = await graph.ainvoke(state)
337
+ print(f"[Live] Graphe exécuté avec succès pour {user_id}")
338
+ except Exception as e:
339
+ print(f"[Live] ERREUR lors de l'exécution du graphe pour {user_id}: {e}")
340
+ print(f"[Live] Audio size: {len(audio_snapshot)} bytes, lang: {lang}")
341
+ import traceback
342
+ print(f"[Live] Traceback: {traceback.format_exc()}")
343
+ if ws.application_state == WebSocketState.CONNECTED:
344
+ await ws.send_text(json.dumps({"type": "error", "message": f"Erreur de traitement audio: {str(e)}"}))
345
+ continue # Continue au lieu de break pour permettre d'autres tentatives
346
+
347
+ # Mettre à jour l'historique global
348
+ history = result.get("history", history)
349
+ user_histories[user_id] = history
350
+
351
+ # Envoie les résultats au user et à la salle
352
+ if result["transcript"].strip():
353
+ effective_lang = result["lang"] or lang or "fr"
354
+ logger.info(f"[STT] {user_id}: {result['transcript']} (lang={effective_lang})")
355
+ await broadcast(room_id, f"{user_id}({effective_lang}): {result['transcript']}", sender=ws)
356
+ if result["llm_response"].strip():
357
+ logger.debug(f"[Handle Response] Envoi réponse texte: {result['llm_response']}")
358
+ await ws.send_text(f"Système: {result['llm_response']}")
359
+ if result["audio_response"]:
360
+ logger.debug(f"[Handle Response] Envoi audio, taille: {len(result['audio_response'])} bytes")
361
+ await ws.send_bytes(result["audio_response"])
362
+ await broadcast(room_id, result["audio_response"], sender=ws)
363
+
364
+ elif "text" in message:
365
+ try:
366
+ data = json.loads(message["text"])
367
+ ttype = data.get("type")
368
+ print(f"[Live] Message texte reçu de {user_id}: {ttype}")
369
+ if ttype == "start":
370
+ print(f"[Live] Début stream: {user_id}")
371
+ logger.info(f"[Live] Début stream: {user_id}")
372
+ elif ttype == "ping":
373
+ print(f"[Live] Ping reçu de {user_id}")
374
+ logger.debug(f"[Live] Ping de {user_id}")
375
+ if ws.application_state == WebSocketState.CONNECTED:
376
+ await ws.send_text(json.dumps({"type": "pong"}))
377
+ elif ttype == "stop":
378
+ logger.info(f"[Live] Arrêt stream: {user_id}")
379
+ if buffer:
380
+ state = {
381
+ "audio_bytes": bytes(buffer),
382
+ "user_id": user_id,
383
+ "room_id": room_id,
384
+ "lang": lang or "fr",
385
+ "history": history,
386
+ "transcript": "",
387
+ "llm_response": "",
388
+ "audio_response": b""
389
+ }
390
+ try:
391
+ result = await graph.ainvoke(state)
392
+ except Exception as e:
393
+ logger.error(f"[Live] Erreur lors de l'exécution du graphe (stop): {e}", exc_info=True)
394
+ if ws.application_state == WebSocketState.CONNECTED:
395
+ await ws.send_text(json.dumps({"type": "error", "message": "Désolé, une erreur s'est produite, veuillez réessayer."}))
396
+ break
397
+ history = result.get("history", history)
398
+ user_histories[user_id] = history
399
+ if result["transcript"].strip():
400
+ effective_lang = result["lang"] or lang or "fr"
401
+ logger.info(f"[STT] (final) {user_id}: {result['transcript']} (lang={effective_lang})")
402
+ await broadcast(room_id, f"{user_id}({effective_lang}): {result['transcript']}", sender=ws)
403
+ if result["llm_response"].strip():
404
+ await ws.send_text(f"Système: {result['llm_response']}")
405
+ if result["audio_response"]:
406
+ await ws.send_bytes(result["audio_response"])
407
+ await broadcast(room_id, result["audio_response"], sender=ws)
408
+ if ws.application_state == WebSocketState.CONNECTED:
409
+ await ws.send_text(json.dumps({"type": "stopped"}))
410
+ await ws.close()
411
+ return
412
+ else:
413
+ logger.warning(f"[Live] Type inconnu: {ttype}")
414
+ if ws.application_state == WebSocketState.CONNECTED:
415
+ await ws.send_text(json.dumps({"type": "error", "message": "Désolé, une erreur s'est produite, veuillez réessayer."}))
416
+ except json.JSONDecodeError:
417
+ logger.error(f"[Live] JSON invalide: {message['text']}")
418
+ if ws.application_state == WebSocketState.CONNECTED:
419
+ await ws.send_text(json.dumps({"type": "error", "message": "Désolé, une erreur s'est produite, veuillez réessayer."}))
420
+ break
421
+ except Exception as e:
422
+ logger.error(f"[Live] Erreur générale: {e}", exc_info=True)
423
+ if ws.application_state == WebSocketState.CONNECTED:
424
+ try:
425
+ await ws.send_text(json.dumps({"type": "error", "message": "Désolé, une erreur s'est produite, veuillez réessayer."}))
426
+ except Exception as send_error:
427
+ logger.error(f"[Live] Erreur lors de l'envoi du message d'erreur: {send_error}")
428
+ finally:
429
+ rooms[room_id].remove(ws) if ws in rooms.get(room_id, []) else None
430
+ participants_lang.pop(user_id, None)
431
+ buffer.clear()
432
+ logger.info(f"[Live] Cleanup: {user_id}")
433
+ if ws.application_state == WebSocketState.CONNECTED:
434
+ try:
435
+ await ws.close()
436
+ except Exception as close_error:
437
+ logger.error(f"[Live] Erreur lors de la fermeture de la connexion: {close_error}")
438
+
439
+ @app.get("/health")
440
+ async def health_check():
441
+ """Vérifie l’état du service."""
442
+ return {"status": "ok", "service": "live_stream"}
443
+
444
+ if __name__ == "__main__":
445
+ uvicorn.run(app, host="0.0.0.0", port=5003, ws_ping_interval=20, ws_ping_timeout=120)
Back-end/services/llm_service.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ import logging
4
+ import uvicorn
5
+ from langdetect import detect, DetectorFactory, LangDetectException
6
+ from langchain_groq import ChatGroq
7
+ from dotenv import load_dotenv
8
+ import os
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from typing import List, Dict
11
+ import re
12
+
13
+ load_dotenv()
14
+
15
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
16
+ logger = logging.getLogger("LLM_Service")
17
+
18
+ app = FastAPI()
19
+
20
+ origins = ["http://localhost:5173", "http://127.0.0.1:5173"]
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=origins,
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ api_key = os.getenv("GROQ_API_KEY")
30
+ if not api_key:
31
+ logger.error("GROQ_API_KEY manquante dans l'environnement")
32
+ raise RuntimeError("GROQ_API_KEY non configurée")
33
+
34
+ MODEL_NAME = "meta-llama/llama-4-scout-17b-16e-instruct"
35
+ logger.info(f"Chargement du modèle LLM Groq : {MODEL_NAME}")
36
+ llm = ChatGroq(model=MODEL_NAME, temperature=0)
37
+
38
+ DetectorFactory.seed = 0
39
+
40
+ EN_WORDS = {"hi", "hello", "hey", "ok", "thanks", "bye", "yes", "no"}
41
+ AR_WORDS = {"salam", "salaam", "marhaban", "مرحبا", "سلام", "شكرا", "أهلا"}
42
+ FR_WORDS = {"bonjour", "salut", "merci", "oui", "non", "s'il vous plaît", "svp"}
43
+
44
+ user_histories: Dict[str, List[Dict[str, str]]] = {}
45
+
46
+ def normalize_text(text: str) -> str:
47
+ text = re.sub(r'[^\w\s]', '', text.lower().strip())
48
+ text = re.sub(r'\s+', ' ', text)
49
+ return text
50
+
51
+ def detect_language(user_text: str, user_id: str, default_lang: str = "fr") -> str:
52
+ if not user_text:
53
+ return default_lang
54
+
55
+ text_normalized = normalize_text(user_text)
56
+
57
+ if text_normalized in EN_WORDS:
58
+ return "en"
59
+ if text_normalized in AR_WORDS:
60
+ return "ar"
61
+ if text_normalized in FR_WORDS:
62
+ return "fr"
63
+
64
+ history = user_histories.get(user_id, [])
65
+ if history and len(user_text) < 10:
66
+ last_msgs = [msg["content"] for msg in history[-2:] if msg["role"] == "user"]
67
+ for msg in last_msgs:
68
+ try:
69
+ lang = detect(msg)
70
+ if lang in {"fr", "en", "ar"}:
71
+ return lang
72
+ except LangDetectException:
73
+ pass
74
+
75
+ try:
76
+ lang = detect(user_text)
77
+ except LangDetectException:
78
+ return default_lang
79
+
80
+ if lang not in {"fr", "en", "ar"}:
81
+ return default_lang
82
+
83
+ return lang
84
+
85
+ class LLMRequest(BaseModel):
86
+ text: str
87
+ user_id: str
88
+ history: List[Dict[str, str]] = []
89
+
90
+ @app.post("/generate")
91
+ async def generate_response(request: LLMRequest):
92
+ user_text = request.text.strip()
93
+ user_id = request.user_id
94
+ if not user_text:
95
+ raise HTTPException(status_code=400, detail="Le texte ne peut pas être vide")
96
+ if not user_id:
97
+ raise HTTPException(status_code=400, detail="L'identifiant utilisateur est requis")
98
+
99
+ lang = detect_language(user_text, user_id, default_lang="fr")
100
+ logger.info(f"Requête LLM reçue ({len(user_text)} caractères), langue: {lang}, user_id: {user_id}")
101
+
102
+ if lang == "fr":
103
+ system_prompt = (
104
+ "Tu es HOLOKIA, un avatar IA conversationnel. "
105
+ "Réponds uniquement aux questions posées avec précision, clarté et simplicité. "
106
+ "Sois toujours poli et chaleureux dans ta manière de parler. "
107
+ "Si la question n’est pas claire, demande gentiment une précision. "
108
+ "Ne donne pas d’informations inutiles et reste concentré sur le sujet."
109
+ )
110
+ elif lang == "ar":
111
+ system_prompt = (
112
+ "أنت HOLOKIA، شخصية ذكاء اصطناعي محادثة. "
113
+ "أجب فقط على الأسئلة المطروحة بدقة ووضوح وبأسلوب بسيط. "
114
+ "كن دائمًا مهذبًا ودودًا في طريقة كلامك. "
115
+ "إذا لم يكن السؤال واضحًا، اطلب بلطف توضيحًا. "
116
+ "لا تضف معلومات غير ضرورية وابقَ مركزًا على الموضوع."
117
+ )
118
+ else:
119
+ system_prompt = (
120
+ "You are HOLOKIA, a conversational AI avatar. "
121
+ "Answer only the questions asked, with accuracy, clarity, and simplicity. "
122
+ "Always remain polite and friendly in your tone. "
123
+ "If the question is unclear, kindly ask for clarification. "
124
+ "Do not add unnecessary information and stay focused on the topic."
125
+ )
126
+
127
+ history = request.history if request.history else user_histories.get(user_id, [])
128
+ history.append({"role": "user", "content": user_text})
129
+
130
+ messages = [("system", system_prompt)] + [
131
+ (msg["role"], msg["content"]) for msg in history[-3:]
132
+ ]
133
+
134
+ total_length = sum(len(msg[1]) for msg in messages)
135
+ if total_length > 4000:
136
+ logger.warning(f"Prompt trop long ({total_length} caractères), truncation")
137
+ messages = [("system", system_prompt)] + messages[-2:]
138
+
139
+ logger.debug(f"Prompt envoyé au LLM: {messages}")
140
+
141
+ try:
142
+ response = llm.invoke(messages)
143
+ response_text = response.content.strip()
144
+ history.append({"role": "assistant", "content": response_text})
145
+ user_histories[user_id] = history[-5:]
146
+ logger.info("Réponse LLM générée avec succès")
147
+ return {
148
+ "response": response_text,
149
+ "lang": lang,
150
+ "history": user_histories[user_id]
151
+ }
152
+ except Exception as e:
153
+ logger.error(f"Erreur LLM: {str(e)}", exc_info=True)
154
+ raise HTTPException(status_code=500, detail="Échec de génération de réponse")
155
+
156
+ @app.get("/health")
157
+ async def health_check():
158
+ return {"status": "ok", "service": "llm"}
159
+
160
+ @app.options("/generate")
161
+ async def options_generate():
162
+ return {"message": "OK"}
163
+
164
+ def run_service():
165
+ uvicorn.run(app, host="0.0.0.0", port=5002)
166
+
167
+ if __name__ == "__main__":
168
+ run_service()
Back-end/services/stt_service.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ from fastapi import FastAPI, UploadFile, File, HTTPException
3
+ import logging
4
+ import os
5
+ import uvicorn
6
+ from faster_whisper import WhisperModel
7
+ from tempfile import NamedTemporaryFile
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydub import AudioSegment
10
+ from langdetect import detect, DetectorFactory, LangDetectException
11
+ import glob
12
+
13
+ # Configuration du logger
14
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s")
15
+ logger = logging.getLogger("STT_Service")
16
+
17
+ # Initialisation FastAPI
18
+ app = FastAPI()
19
+
20
+ # CORS pour frontend local
21
+ origins = ["http://localhost:5173", "http://127.0.0.1:5173"]
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=origins,
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # Paramètres
31
+ MODEL_SIZE = "small" # Léger et rapide
32
+ MIN_AUDIO_SIZE = 1024 # Bytes (~0.03s à 16kHz)
33
+ MIN_AUDIO_DURATION = 0.75 # Sec min pour traitement
34
+ SAMPLE_RATE = 16000 # Hz, 16-bit mono
35
+ MAX_TEMP_FILES = 1000 # Limite de fichiers temporaires
36
+
37
+ # Initialisation du modèle Whisper
38
+ logger.info(f"Chargement du modèle Faster-Whisper ({MODEL_SIZE})")
39
+ try:
40
+ model = WhisperModel(MODEL_SIZE, device="cpu", compute_type="int8")
41
+ logger.info("Modèle Whisper chargé avec succès")
42
+ except Exception as e:
43
+ logger.error(f"Erreur lors du chargement du modèle Whisper: {e}")
44
+ model = None
45
+
46
+ # Rendre langdetect plus stable
47
+ DetectorFactory.seed = 0
48
+
49
+ # Correction langues fréquentes
50
+ EN_WORDS = {"hi", "hello", "hey", "ok", "thanks", "bye", "yes", "no"}
51
+ AR_WORDS = {"salam", "salaam", "marhaban", "مرحبا", "سلام", "شكرا", "أهلا"}
52
+
53
+ def validate_language(transcript: str, whisper_lang: str, probability: float) -> str:
54
+ """Valide ou corrige la langue détectée par Whisper avec langdetect."""
55
+ if not transcript.strip():
56
+ return whisper_lang or "fr"
57
+
58
+ try:
59
+ detected_lang = detect(transcript)
60
+ except LangDetectException:
61
+ return whisper_lang or "fr"
62
+
63
+ # Normalisation et correction
64
+ text_lower = transcript.lower().strip()
65
+ if text_lower in EN_WORDS:
66
+ return "en"
67
+ if text_lower in AR_WORDS:
68
+ return "ar"
69
+
70
+ # Si probabilité faible (<0.9) et langdetect donne une langue différente, préférer langdetect
71
+ if probability < 0.9 and detected_lang in {"fr", "en", "ar"}:
72
+ logger.debug(f"Langue corrigée par langdetect: {whisper_lang} -> {detected_lang}")
73
+ return detected_lang
74
+
75
+ # Sinon, on garde la langue de Whisper
76
+ return whisper_lang or "fr"
77
+
78
+ def clean_temp_files():
79
+ """Supprime les fichiers temporaires excédentaires."""
80
+ temp_files = glob.glob(f"{tempfile.gettempdir()}/*.wav")
81
+ if len(temp_files) > MAX_TEMP_FILES:
82
+ temp_files.sort(key=os.path.getmtime)
83
+ for file in temp_files[:-MAX_TEMP_FILES]:
84
+ try:
85
+ os.unlink(file)
86
+ logger.info(f"Fichier temporaire supprimé: {file}")
87
+ except Exception as e:
88
+ logger.warning(f"Erreur lors de la suppression de {file}: {e}")
89
+
90
+ @app.post("/transcribe")
91
+ async def transcribe_audio(file: UploadFile = File(...), language: str = None):
92
+ """
93
+ Transcrit un fichier audio en texte avec Faster-Whisper.
94
+ - file: Audio (WAV, MP3, M4A, WebM, etc.)
95
+ - language: Langue cible (optionnel: en, ar, fr, etc.), sinon autodétection
96
+ """
97
+ logger.info(f"Fichier reçu: filename={file.filename}, content_type={file.content_type}")
98
+
99
+ # Vérifier si le modèle est chargé
100
+ if model is None:
101
+ logger.error("Modèle Whisper non chargé")
102
+ raise HTTPException(status_code=503, detail="Service STT non disponible")
103
+
104
+ temp_audio_path = None
105
+ try:
106
+ # Nettoie les fichiers temporaires si nécessaire
107
+ clean_temp_files()
108
+
109
+ # Sauvegarde temporaire
110
+ suffix = os.path.splitext(file.filename)[1].lower()
111
+ content = await file.read()
112
+ if len(content) < MIN_AUDIO_SIZE:
113
+ logger.debug(f"Audio trop petit ({len(content)} bytes), skip")
114
+ return {"transcript": "", "lang": language or "unknown", "status": "skipped"}
115
+
116
+ with NamedTemporaryFile(delete=False, suffix=suffix) as temp_audio:
117
+ temp_audio.write(content)
118
+ temp_audio_path = temp_audio.name
119
+
120
+ # Vérification durée
121
+ duration = os.path.getsize(temp_audio_path) / (SAMPLE_RATE * 2) # 16kHz, 16-bit mono
122
+ logger.info(f"Processing audio, duration: {duration:.3f}s")
123
+ if duration < MIN_AUDIO_DURATION:
124
+ logger.debug(f"Audio trop court ({duration:.3f}s), skip")
125
+ return {"transcript": "", "lang": language or "unknown", "status": "skipped"}
126
+
127
+ # Conversion si non supporté
128
+ supported_formats = [".wav", ".mp3", ".m4a", ".webm"]
129
+ wav_path = temp_audio_path
130
+ if suffix != ".wav":
131
+ try:
132
+ audio = AudioSegment.from_file(temp_audio_path)
133
+ with NamedTemporaryFile(delete=False, suffix=".wav") as wav_fd:
134
+ audio.export(wav_fd.name, format="wav")
135
+ wav_path = wav_fd.name
136
+ logger.info(f"Converti en WAV: {wav_path}")
137
+ except Exception as e:
138
+ logger.error(f"Erreur conversion WAV: {e}")
139
+ raise HTTPException(status_code=400, detail="Erreur conversion audio")
140
+
141
+ # Transcription
142
+ segments, info = model.transcribe(wav_path, language=language if language else None)
143
+ transcript = " ".join([s.text.strip() for s in segments if s.text.strip()])
144
+
145
+ # Validation de la langue
146
+ detected_lang = language or validate_language(transcript, info.language, info.language_probability)
147
+ logger.info(f"Langue détectée: {info.language}, Probabilité: {info.language_probability:.2f}, Langue finale: {detected_lang}")
148
+ logger.info(f"Transcript: '{transcript}'")
149
+
150
+ return {
151
+ "transcript": transcript,
152
+ "lang": detected_lang,
153
+ "status": "success" if transcript else "empty"
154
+ }
155
+
156
+ except Exception as e:
157
+ logger.error(f"Échec transcription: {e}", exc_info=True)
158
+ raise HTTPException(status_code=500, detail="Erreur STT")
159
+
160
+ finally:
161
+ # Nettoyage fichiers temporaires
162
+ try:
163
+ if temp_audio_path and os.path.exists(temp_audio_path):
164
+ os.unlink(temp_audio_path)
165
+ if "wav_path" in locals() and wav_path != temp_audio_path and os.path.exists(wav_path):
166
+ os.unlink(wav_path)
167
+ except Exception as e:
168
+ logger.warning(f"Erreur nettoyage fichiers: {e}")
169
+
170
+ @app.get("/health")
171
+ async def health_check():
172
+ """Vérifie l’état du service."""
173
+ return {"status": "ok", "service": "stt"}
174
+
175
+ def run_service():
176
+ """Lance le service STT."""
177
+ uvicorn.run(app, host="0.0.0.0", port=5001)
178
+
179
+ if __name__ == "__main__":
180
+ run_service()
Back-end/services/tts_service.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # File: tts_service.py
2
+ from fastapi import FastAPI, HTTPException
3
+ from fastapi.responses import FileResponse
4
+ from pydantic import BaseModel
5
+ from gtts import gTTS
6
+ import hashlib
7
+ import os
8
+ import sys
9
+ import logging
10
+ import uvicorn
11
+ from pathlib import Path
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+
14
+ # Configuration du logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
18
+ stream=sys.stdout
19
+ )
20
+ logger = logging.getLogger("TTS_Service")
21
+
22
+ app = FastAPI()
23
+
24
+ # Chemins absolus
25
+ BASE_DIR = Path(__file__).parent.parent.resolve()
26
+ CACHE_DIR = BASE_DIR / "tts_cache"
27
+ CACHE_DIR.mkdir(exist_ok=True)
28
+
29
+ # Configuration CORS
30
+ origins = ["*"] # Autoriser toutes les origines pour les tests
31
+
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=origins,
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # Langues supportées
41
+ SUPPORTED_LANGUAGES = {"fr", "en", "ar", "es", "de", "it"}
42
+
43
+ class TTSRequest(BaseModel):
44
+ text: str
45
+ lang: str
46
+
47
+ @app.post("/generate-tts")
48
+ async def generate_tts(request: TTSRequest):
49
+ text = request.text.strip()
50
+ lang = request.lang.strip().lower()
51
+
52
+ # Validation des entrées
53
+ if not text:
54
+ raise HTTPException(status_code=400, detail="Le texte ne peut pas être vide")
55
+ if lang not in SUPPORTED_LANGUAGES:
56
+ raise HTTPException(
57
+ status_code=400,
58
+ detail=f"Langue non supportée. Choisissez parmi: {', '.join(SUPPORTED_LANGUAGES)}"
59
+ )
60
+
61
+ logger.info(f"Génération TTS: {len(text)} caractères en {lang}")
62
+
63
+ # Génération du hash
64
+ normalized_text = text.replace(" ", "_").lower()
65
+ text_hash = hashlib.md5(f"{normalized_text}_{lang}".encode('utf-8')).hexdigest()
66
+ filename = f"{text_hash}.mp3"
67
+ filepath = CACHE_DIR / filename
68
+
69
+ # Utilisation du cache si disponible
70
+ if filepath.exists():
71
+ logger.info(f"Fichier existant: {filepath}")
72
+ return {
73
+ "audioPath": str(filepath.relative_to(BASE_DIR)),
74
+ "url": f"/audio/{filepath.relative_to(BASE_DIR)}"
75
+ }
76
+
77
+ # Génération du fichier audio
78
+ try:
79
+ tts = gTTS(text=text, lang=lang)
80
+ tts.save(str(filepath))
81
+ logger.info(f"Fichier généré: {filepath}")
82
+
83
+ return {
84
+ "audioPath": str(filepath.relative_to(BASE_DIR)),
85
+ "url": f"/audio/{filepath.relative_to(BASE_DIR)}"
86
+ }
87
+ except Exception as e:
88
+ logger.error(f"Erreur TTS: {str(e)}", exc_info=True)
89
+ raise HTTPException(
90
+ status_code=500,
91
+ detail=f"Échec de génération audio: {str(e)}"
92
+ )
93
+
94
+ @app.get("/audio/{file_path:path}")
95
+ async def get_audio(file_path: str):
96
+ # Sécurité: empêcher les accès en dehors du cache
97
+ if ".." in file_path or file_path.startswith("/"):
98
+ raise HTTPException(status_code=403, detail="Accès non autorisé")
99
+
100
+ full_path = BASE_DIR / file_path
101
+
102
+ # Vérifier que le chemin est dans le cache
103
+ if not str(full_path).startswith(str(CACHE_DIR)):
104
+ raise HTTPException(status_code=403, detail="Accès non autorisé")
105
+
106
+ if not full_path.exists():
107
+ raise HTTPException(status_code=404, detail="Fichier audio non trouvé")
108
+
109
+ return FileResponse(full_path, media_type="audio/mpeg")
110
+
111
+ @app.get("/health")
112
+ async def health_check():
113
+ return {"status": "ok", "service": "tts"}
114
+
115
+ def run_service():
116
+ uvicorn.run(app, host="0.0.0.0", port=5000)
117
+
118
+ if __name__ == "__main__":
119
+ run_service()
Back-end/start_services.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import logging
5
+ from logging.handlers import RotatingFileHandler
6
+ from datetime import datetime
7
+ import threading
8
+ import time
9
+
10
+ # ─────────────── UTF-8 Windows ───────────────
11
+ if os.name == 'nt':
12
+ import ctypes
13
+ ctypes.windll.kernel32.SetConsoleCP(65001)
14
+ ctypes.windll.kernel32.SetConsoleOutputCP(65001)
15
+
16
+ # ─────────────── Répertoires de logs ───────────────
17
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ LOG_DIR = os.path.join(BASE_DIR, "logs")
19
+ os.makedirs(LOG_DIR, exist_ok=True)
20
+
21
+ # ─────────────── Logger central ───────────────
22
+ central_logger = logging.getLogger("Holokia")
23
+ central_logger.setLevel(logging.INFO)
24
+
25
+ console_handler = logging.StreamHandler(sys.stdout)
26
+ console_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
27
+
28
+ central_file_handler = RotatingFileHandler(
29
+ os.path.join(LOG_DIR, "holokia.log"),
30
+ maxBytes=5*1024*1024,
31
+ backupCount=5,
32
+ encoding="utf-8"
33
+ )
34
+ central_file_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
35
+
36
+ central_logger.addHandler(console_handler)
37
+ central_logger.addHandler(central_file_handler)
38
+
39
+ central_logger.info("🚀 Démarrage de tous les services Holokia Avatar")
40
+ central_logger.info(f"📅 Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
41
+
42
+ # ─────────────── Services ───────────────
43
+ SERVICES = {
44
+ "TTS": {"script": "services/tts_service.py", "port": 5000, "prefix": "[TTS]"},
45
+ "STT": {"script": "services/stt_service.py", "port": 5001, "prefix": "[STT]"},
46
+ "LLM": {"script": "services/llm_service.py", "port": 5002, "prefix": "[LLM]"},
47
+ "LiveStream": {"script": "services/live_stream_service.py", "port": 5003, "prefix": "[Live]"},
48
+ # "Backend": {"script": os.path.join(BASE_DIR, "app/main.py"), "port": int(os.getenv("PORT", 8000)), "prefix": "[Backend]"}
49
+ }
50
+
51
+ processes = {}
52
+
53
+ # ─────────────── Fonction pour créer un logger par service ───────────────
54
+ def create_service_logger(name):
55
+ logger = logging.getLogger(name)
56
+ logger.setLevel(logging.INFO)
57
+
58
+ file_handler = RotatingFileHandler(
59
+ os.path.join(LOG_DIR, f"{name}.log"),
60
+ maxBytes=5*1024*1024,
61
+ backupCount=3,
62
+ encoding="utf-8"
63
+ )
64
+ file_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
65
+
66
+ # Eviter d'ajouter plusieurs handlers si déjà créé
67
+ if not logger.handlers:
68
+ logger.addHandler(console_handler)
69
+ logger.addHandler(file_handler)
70
+
71
+ return logger
72
+
73
+ # ─────────────── Démarrer un service ───────────────
74
+ def start_service(name, info):
75
+ prefix = info["prefix"]
76
+ port = info["port"]
77
+ script = info["script"]
78
+
79
+ service_logger = create_service_logger(name)
80
+
81
+ # Ajuster PYTHONPATH
82
+ #env = os.environ.copy()
83
+ #env["PYTHONPATH"] = f"{BASE_DIR};{os.path.dirname(script)};{env.get('PYTHONPATH','')}"
84
+
85
+ service_logger.info(f"{prefix} 🚀 Démarrage du service sur le port {port} ...")
86
+
87
+ proc = subprocess.Popen(
88
+ [sys.executable, script],
89
+ stdout=sys.stdout,
90
+ stderr=sys.stderr,
91
+ bufsize=1,
92
+ universal_newlines=True,
93
+ #env=env
94
+ )
95
+
96
+ # Les logs des services apparaîtront directement dans stdout
97
+
98
+ return proc
99
+
100
+ # ─────────────── Lancement de tous les services ───────────────
101
+ for name, info in SERVICES.items():
102
+ proc = start_service(name, info)
103
+ processes[name] = proc
104
+
105
+ central_logger.info("⚙️ Tous les services ont été lancés. Ctrl+C pour arrêter.")
106
+
107
+ # ─────────────── Boucle de surveillance ───────────────
108
+ try:
109
+ while True:
110
+ for name, proc in list(processes.items()):
111
+ ret = proc.poll()
112
+ if ret is not None:
113
+ central_logger.error(f"{SERVICES[name]['prefix']} ❌ Service arrêté ! Code: {ret}")
114
+ proc_new = start_service(name, SERVICES[name])
115
+ processes[name] = proc_new
116
+ time.sleep(2)
117
+ except KeyboardInterrupt:
118
+ central_logger.info("⏹ Arrêt de tous les services...")
119
+ for name, proc in processes.items():
120
+ proc.terminate()
121
+ central_logger.info("✅ Tous les services ont été arrêtés.")
Dockerfile ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Dockerfile pour Hugging Face Spaces
3
+ # HOLOKIA-AVATAR - Avatar 3D Interactif
4
+ # ===========================================
5
+
6
+ # Stage 1: Build frontend
7
+ FROM node:18-alpine AS frontend-build
8
+
9
+ WORKDIR /app/frontend
10
+
11
+ # Copy package files
12
+ COPY frontend/package*.json ./
13
+ COPY frontend/wawa-lipsync/package*.json ./wawa-lipsync/
14
+
15
+ # Install dependencies (including dev dependencies for build)
16
+ RUN npm ci
17
+
18
+ # Copy source code
19
+ COPY frontend/ .
20
+
21
+ # Install wawa-lipsync dependencies and build
22
+ WORKDIR /app/frontend/wawa-lipsync
23
+ RUN npm install
24
+ RUN npm run build
25
+
26
+ # Build main application
27
+ WORKDIR /app/frontend
28
+ RUN npm run build
29
+
30
+ # Stage 2: Python backend
31
+ FROM python:3.11-slim
32
+
33
+ # Install system dependencies
34
+ RUN apt-get update && apt-get install -y \
35
+ curl \
36
+ ffmpeg \
37
+ nginx \
38
+ wget \
39
+ build-essential \
40
+ && rm -rf /var/lib/apt/lists/*
41
+
42
+ # Create non-root user
43
+ RUN useradd --create-home --shell /bin/bash app
44
+
45
+ # Set working directory
46
+ WORKDIR /app
47
+
48
+ # Copy requirements first for better caching
49
+ COPY Back-end/requirements.txt .
50
+
51
+ # Install Python dependencies
52
+ RUN pip install --no-cache-dir --upgrade pip && \
53
+ pip install --no-cache-dir -r requirements.txt
54
+
55
+ # Copy backend application code
56
+ COPY Back-end/ .
57
+
58
+ # Copy frontend built files from previous stage
59
+ COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html
60
+
61
+ # Copy nginx configuration
62
+ COPY docker/nginx.conf /etc/nginx/nginx.conf
63
+
64
+ # Copy app.py (entry point for HF)
65
+ COPY app.py .
66
+
67
+ # Copy diagnostic scripts
68
+ COPY debug_websocket.py .
69
+ COPY test_services_detailed.py .
70
+
71
+ # Create necessary directories and set permissions
72
+ RUN mkdir -p logs tts_cache /var/cache/nginx /var/log/nginx /var/run /var/lib/nginx && \
73
+ chown -R app:app /app && \
74
+ chown -R app:app /usr/share/nginx/html && \
75
+ chown -R app:app /var/cache/nginx /var/log/nginx /var/run /var/lib/nginx
76
+
77
+ # Set environment variables
78
+ ENV PYTHONPATH=/app
79
+ ENV PORT=7860
80
+
81
+ # Expose port (Hugging Face standard)
82
+ EXPOSE 7860
83
+
84
+ # Health check
85
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
86
+ CMD curl -f http://localhost:7860/health || exit 1
87
+
88
+ # Switch to non-root user
89
+ USER app
90
+
91
+ # Start application
92
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: HOLOKIA-AVATAR
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: Avatar 3D intelligent avec IA conversationnelle
10
+ ---
11
+
12
+ # 🤖 HOLOKIA-AVATAR - Avatar 3D Interactif
13
+
14
+ Un avatar 3D intelligent avec capacités de conversation en temps réel, utilisant l'IA pour des interactions naturelles.
15
+
16
+ ## ✨ Fonctionnalités
17
+
18
+ - 🎭 **Avatar 3D interactif** avec synchronisation labiale
19
+ - 🗣️ **Reconnaissance vocale** (Speech-to-Text)
20
+ - 🔊 **Synthèse vocale** (Text-to-Speech)
21
+ - 🧠 **IA conversationnelle** (Groq LLM)
22
+ - 📡 **Streaming temps réel** via WebSocket
23
+ - 🌐 **Interface web moderne** avec React et Three.js
24
+
25
+ ## 🚀 Utilisation
26
+
27
+ 1. **Parlez** à l'avatar via le microphone
28
+ 2. **L'avatar écoute** et comprend votre message
29
+ 3. **L'IA génère** une réponse intelligente
30
+ 4. **L'avatar parle** avec synchronisation labiale
31
+ 5. **Interaction continue** en temps réel
32
+
33
+ ## 🔧 Configuration
34
+
35
+ Ajoutez votre clé API Groq dans les **Secrets** du Space :
36
+ - Nom : `GROQ_API_KEY`
37
+ - Valeur : `gsk_...` (votre clé API)
38
+
39
+ ## 📄 Licence
40
+
41
+ MIT License
42
+
43
+ ---
44
+
45
+ **Développé avec ❤️ par l'équipe HOLOKIA**
app.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HOLOKIA-AVATAR - Point d'entrée pour Hugging Face Spaces
4
+ Avatar 3D interactif avec IA conversationnelle
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import time
11
+ import logging
12
+ from pathlib import Path
13
+
14
+ # Configuration du logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(levelname)s - %(message)s"
18
+ )
19
+ logger = logging.getLogger("HOLOKIA-AVATAR")
20
+
21
+ def check_requirements():
22
+ """Vérifie que toutes les dépendances sont installées."""
23
+ try:
24
+ import fastapi
25
+ import uvicorn
26
+ import gtts
27
+ import faster_whisper
28
+ import langchain_groq
29
+ logger.info("✅ Toutes les dépendances sont installées")
30
+ return True
31
+ except ImportError as e:
32
+ logger.error(f"❌ Dépendance manquante: {e}")
33
+ return False
34
+
35
+ def check_environment():
36
+ """Vérifie les variables d'environnement requises."""
37
+ groq_key = os.getenv("GROQ_API_KEY")
38
+ if not groq_key:
39
+ logger.warning("⚠️ GROQ_API_KEY n'est pas définie")
40
+ logger.info("💡 Ajoutez votre clé API Groq dans les secrets du Space")
41
+ # Ne pas arrêter l'application, juste avertir
42
+ return True
43
+
44
+ logger.info("✅ Variables d'environnement configurées")
45
+ return True
46
+
47
+ def check_frontend():
48
+ """Vérifie que le frontend est disponible."""
49
+ nginx_html = Path("/usr/share/nginx/html")
50
+
51
+ if nginx_html.exists() and any(nginx_html.iterdir()):
52
+ logger.info("✅ Frontend disponible (construit dans le Dockerfile)")
53
+ return True
54
+ else:
55
+ logger.error("❌ Frontend non disponible")
56
+ return False
57
+
58
+ def test_services():
59
+ """Teste que tous les services backend sont accessibles."""
60
+ logger.info("🔍 Test des services backend...")
61
+
62
+ import requests
63
+
64
+ services = [
65
+ ("TTS", "http://localhost:5000/health"),
66
+ ("STT", "http://localhost:5001/health"),
67
+ ("LLM", "http://localhost:5002/health"),
68
+ ("Live Stream", "http://localhost:5003/health"),
69
+ ]
70
+
71
+ all_ok = True
72
+ for name, url in services:
73
+ try:
74
+ response = requests.get(url, timeout=5)
75
+ if response.status_code == 200:
76
+ logger.info(f"✅ {name}: OK")
77
+ else:
78
+ logger.error(f"❌ {name}: Erreur (status {response.status_code})")
79
+ all_ok = False
80
+ except Exception as e:
81
+ logger.error(f"❌ {name}: Erreur de connexion - {e}")
82
+ all_ok = False
83
+
84
+ return all_ok
85
+
86
+ def start_services():
87
+ """Démarre tous les services backend."""
88
+ logger.info("🚀 Démarrage des services backend...")
89
+
90
+ try:
91
+ # Vérifier que le script existe
92
+ start_script = Path("start_services.py")
93
+ if not start_script.exists():
94
+ logger.error(f"❌ Script de démarrage non trouvé: {start_script}")
95
+ return False
96
+
97
+ # Démarrer les services en arrière-plan
98
+ logger.info("⏳ Démarrage des services en arrière-plan...")
99
+ process = subprocess.Popen([
100
+ sys.executable, str(start_script)
101
+ ], cwd=os.getcwd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
102
+
103
+ # Attendre un peu pour que les services démarrent
104
+ time.sleep(15)
105
+
106
+ # Vérifier que le processus est toujours en vie
107
+ if process.poll() is not None:
108
+ logger.error("❌ Les services backend se sont arrêtés prématurément")
109
+ stdout, stderr = process.communicate()
110
+ logger.error(f"STDOUT: {stdout.decode()}")
111
+ logger.error(f"STDERR: {stderr.decode()}")
112
+ return False
113
+
114
+ # Tester que les services sont accessibles
115
+ if not test_services():
116
+ logger.error("❌ Certains services ne sont pas accessibles")
117
+ return False
118
+
119
+ # Test détaillé des services
120
+ logger.info("🔍 Test détaillé des services...")
121
+ try:
122
+ result = subprocess.run([sys.executable, "debug_websocket.py"],
123
+ capture_output=True, text=True, timeout=30)
124
+ if result.stdout:
125
+ logger.info(f"Résultats des tests WebSocket:\n{result.stdout}")
126
+ if result.stderr:
127
+ logger.error(f"Erreurs des tests WebSocket:\n{result.stderr}")
128
+ except Exception as e:
129
+ logger.warning(f"Impossible d'exécuter les tests WebSocket: {e}")
130
+
131
+ # Test détaillé des services
132
+ logger.info("🔍 Test détaillé des services...")
133
+ try:
134
+ result = subprocess.run([sys.executable, "test_services_detailed.py"],
135
+ capture_output=True, text=True, timeout=120)
136
+ if result.stdout:
137
+ logger.info(f"Résultats des tests détaillés:\n{result.stdout}")
138
+ if result.stderr:
139
+ logger.error(f"Erreurs des tests détaillés:\n{result.stderr}")
140
+ except Exception as e:
141
+ logger.warning(f"Impossible d'exécuter les tests détaillés: {e}")
142
+
143
+ logger.info("✅ Services backend démarrés et testés")
144
+ return True
145
+
146
+ except Exception as e:
147
+ logger.error(f"❌ Erreur lors du démarrage des services: {e}")
148
+ return False
149
+
150
+ def start_nginx():
151
+ """Démarre Nginx."""
152
+ logger.info("🌐 Démarrage de Nginx...")
153
+
154
+ try:
155
+ # Vérifier la configuration Nginx
156
+ result = subprocess.run(["nginx", "-t"], capture_output=True, text=True)
157
+ if result.returncode != 0:
158
+ logger.error(f"❌ Configuration Nginx invalide: {result.stderr}")
159
+ return False
160
+
161
+ logger.info("✅ Configuration Nginx valide")
162
+
163
+ # Démarrer Nginx en mode daemon
164
+ subprocess.run(["nginx"], check=True)
165
+ logger.info("✅ Nginx démarré")
166
+
167
+ return True
168
+
169
+ except subprocess.CalledProcessError as e:
170
+ logger.error(f"❌ Erreur Nginx: {e}")
171
+ return False
172
+ except Exception as e:
173
+ logger.error(f"❌ Erreur inattendue avec Nginx: {e}")
174
+ return False
175
+
176
+ def main():
177
+ """Point d'entrée principal."""
178
+ logger.info("🤖 HOLOKIA-AVATAR - Démarrage sur Hugging Face Spaces (v2.2)")
179
+ logger.info(f"📁 Répertoire de travail: {os.getcwd()}")
180
+ logger.info(f"🐍 Version Python: {sys.version}")
181
+
182
+ try:
183
+ # Vérifications préliminaires
184
+ if not check_requirements():
185
+ logger.error("❌ Vérification des dépendances échouée")
186
+ sys.exit(1)
187
+
188
+ if not check_environment():
189
+ logger.warning("⚠️ Vérification de l'environnement échouée, mais on continue")
190
+
191
+ if not check_frontend():
192
+ logger.error("❌ Vérification du frontend échouée")
193
+ sys.exit(1)
194
+
195
+ # Démarrage des services
196
+ if not start_services():
197
+ logger.error("❌ Impossible de démarrer les services backend")
198
+ # Ne pas arrêter, essayer de continuer avec Nginx seulement
199
+
200
+ # Démarrage de Nginx
201
+ if not start_nginx():
202
+ logger.error("❌ Impossible de démarrer Nginx")
203
+ sys.exit(1)
204
+
205
+ logger.info("🎉 Application démarrée avec succès!")
206
+ logger.info("🌐 Serveur web accessible sur le port 7860")
207
+
208
+ # Garder l'application en vie
209
+ try:
210
+ while True:
211
+ time.sleep(60)
212
+ logger.info("💓 Application en vie...")
213
+ except KeyboardInterrupt:
214
+ logger.info("🛑 Arrêt de l'application")
215
+
216
+ except Exception as e:
217
+ logger.error(f"❌ Erreur critique: {e}")
218
+ sys.exit(1)
219
+
220
+ if __name__ == "__main__":
221
+ main()
clean_and_restructure.ps1 ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Script PowerShell de nettoyage et restructuration pour Hugging Face
3
+ # HOLOKIA-AVATAR - Avatar 3D Interactif
4
+ # ===========================================
5
+
6
+ param(
7
+ [switch]$Help
8
+ )
9
+
10
+ # Fonctions utilitaires
11
+ function Write-Info {
12
+ param([string]$Message)
13
+ Write-Host "[INFO] $Message" -ForegroundColor Blue
14
+ }
15
+
16
+ function Write-Success {
17
+ param([string]$Message)
18
+ Write-Host "[SUCCESS] $Message" -ForegroundColor Green
19
+ }
20
+
21
+ function Write-Warning {
22
+ param([string]$Message)
23
+ Write-Host "[WARNING] $Message" -ForegroundColor Yellow
24
+ }
25
+
26
+ function Write-Error {
27
+ param([string]$Message)
28
+ Write-Host "[ERROR] $Message" -ForegroundColor Red
29
+ }
30
+
31
+ # Aide
32
+ if ($Help) {
33
+ Write-Host "Usage: .\clean_and_restructure.ps1 [OPTIONS]"
34
+ Write-Host ""
35
+ Write-Host "Ce script nettoie l'historique Git et restructure le projet pour Hugging Face Spaces."
36
+ Write-Host ""
37
+ Write-Host "Options:"
38
+ Write-Host " -Help Afficher cette aide"
39
+ Write-Host ""
40
+ Write-Host "ATTENTION: Ce script va supprimer l'historique Git existant!"
41
+ Write-Host "Une sauvegarde sera créée dans ..\backup_holokia_avatar\"
42
+ Write-Host ""
43
+ exit 0
44
+ }
45
+
46
+ # Vérifications préliminaires
47
+ function Test-Requirements {
48
+ Write-Info "Vérification des prérequis..."
49
+
50
+ # Vérifier Git
51
+ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
52
+ Write-Error "Git n'est pas installé"
53
+ exit 1
54
+ }
55
+
56
+ # Vérifier Git LFS
57
+ if (-not (Get-Command git-lfs -ErrorAction SilentlyContinue)) {
58
+ Write-Warning "Git LFS n'est pas installé"
59
+ Write-Info "Téléchargez et installez Git LFS depuis: https://git-lfs.github.io/"
60
+ Write-Info "Ou utilisez: winget install Git.Git-LFS"
61
+ exit 1
62
+ }
63
+
64
+ Write-Success "Tous les prérequis sont satisfaits"
65
+ }
66
+
67
+ # Sauvegarde des fichiers importants
68
+ function Backup-ImportantFiles {
69
+ Write-Info "Sauvegarde des fichiers importants..."
70
+
71
+ # Créer un dossier de sauvegarde
72
+ $backupDir = "..\backup_holokia_avatar"
73
+ if (-not (Test-Path $backupDir)) {
74
+ New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
75
+ }
76
+
77
+ # Sauvegarder les fichiers de configuration
78
+ Copy-Item -Path "." -Destination $backupDir -Recurse -Force
79
+
80
+ Write-Success "Sauvegarde terminée dans $backupDir"
81
+ }
82
+
83
+ # Nettoyage de l'historique Git
84
+ function Clear-GitHistory {
85
+ Write-Info "Nettoyage de l'historique Git..."
86
+
87
+ # Supprimer le dossier .git existant
88
+ if (Test-Path ".git") {
89
+ Write-Info "Suppression de l'historique Git existant..."
90
+ Remove-Item -Path ".git" -Recurse -Force
91
+ }
92
+
93
+ # Initialiser un nouveau dépôt Git
94
+ Write-Info "Initialisation d'un nouveau dépôt Git..."
95
+ git init
96
+
97
+ # Configurer Git LFS
98
+ Write-Info "Configuration de Git LFS..."
99
+ git lfs install
100
+
101
+ # Ajouter les fichiers LFS
102
+ git lfs track "*.glb"
103
+ git lfs track "*.fbx"
104
+ git lfs track "*.obj"
105
+ git lfs track "*.png"
106
+ git lfs track "*.jpg"
107
+ git lfs track "*.jpeg"
108
+ git lfs track "*.wav"
109
+ git lfs track "*.mp3"
110
+
111
+ Write-Success "Historique Git nettoyé et LFS configuré"
112
+ }
113
+
114
+ # Restructuration des fichiers
115
+ function Restructure-Files {
116
+ Write-Info "Restructuration des fichiers pour Hugging Face..."
117
+
118
+ # Supprimer les fichiers de développement
119
+ $filesToRemove = @(
120
+ "build-frontend.sh",
121
+ "deploy_to_hf.sh",
122
+ "test_deployment.py",
123
+ "README_HF_DEPLOYMENT.md",
124
+ "huggingface_hub_config.yaml"
125
+ )
126
+
127
+ foreach ($file in $filesToRemove) {
128
+ if (Test-Path $file) {
129
+ Remove-Item -Path $file -Force
130
+ Write-Info "Supprimé: $file"
131
+ }
132
+ }
133
+
134
+ # Supprimer les dossiers de développement
135
+ $dirsToRemove = @(
136
+ "docker",
137
+ "Back-end\tts_cache",
138
+ "Back-end\logs",
139
+ "frontend\node_modules",
140
+ "frontend\dist",
141
+ "__pycache__",
142
+ ".pytest_cache"
143
+ )
144
+
145
+ foreach ($dir in $dirsToRemove) {
146
+ if (Test-Path $dir) {
147
+ Remove-Item -Path $dir -Recurse -Force
148
+ Write-Info "Supprimé: $dir"
149
+ }
150
+ }
151
+
152
+ # Supprimer les fichiers docker-compose
153
+ Get-ChildItem -Path "." -Name "docker-compose*.yml" | ForEach-Object {
154
+ Remove-Item -Path $_ -Force
155
+ Write-Info "Supprimé: $_"
156
+ }
157
+
158
+ # Supprimer les fichiers .env (sauf .env.example)
159
+ Get-ChildItem -Path "." -Name ".env*" -Recurse | Where-Object { $_.Name -ne ".env.example" } | ForEach-Object {
160
+ Remove-Item -Path $_.FullName -Force
161
+ Write-Info "Supprimé: $($_.FullName)"
162
+ }
163
+
164
+ Write-Success "Restructuration terminée"
165
+ }
166
+
167
+ # Création des fichiers nécessaires pour HF
168
+ function New-HuggingFaceFiles {
169
+ Write-Info "Création des fichiers nécessaires pour Hugging Face..."
170
+
171
+ # Créer un README.md simple pour HF
172
+ $readmeContent = @"
173
+ ---
174
+ title: HOLOKIA-AVATAR
175
+ emoji: 🤖
176
+ colorFrom: blue
177
+ colorTo: purple
178
+ sdk: docker
179
+ pinned: false
180
+ license: mit
181
+ short_description: Avatar 3D intelligent avec IA conversationnelle en temps réel
182
+ ---
183
+
184
+ # 🤖 HOLOKIA-AVATAR - Avatar 3D Interactif
185
+
186
+ Un avatar 3D intelligent avec capacités de conversation en temps réel, utilisant l'IA pour des interactions naturelles.
187
+
188
+ ## ✨ Fonctionnalités
189
+
190
+ - 🎭 **Avatar 3D interactif** avec synchronisation labiale
191
+ - 🗣️ **Reconnaissance vocale** (Speech-to-Text)
192
+ - 🔊 **Synthèse vocale** (Text-to-Speech)
193
+ - 🧠 **IA conversationnelle** (Groq LLM)
194
+ - 📡 **Streaming temps réel** via WebSocket
195
+ - 🌐 **Interface web moderne** avec React et Three.js
196
+
197
+ ## 🚀 Utilisation
198
+
199
+ 1. **Parlez** à l'avatar via le microphone
200
+ 2. **L'avatar écoute** et comprend votre message
201
+ 3. **L'IA génère** une réponse intelligente
202
+ 4. **L'avatar parle** avec synchronisation labiale
203
+ 5. **Interaction continue** en temps réel
204
+
205
+ ## 🔧 Configuration
206
+
207
+ Ajoutez votre clé API Groq dans les **Secrets** du Space :
208
+ - Nom : `GROQ_API_KEY`
209
+ - Valeur : `gsk_...` (votre clé API)
210
+
211
+ ## 📄 Licence
212
+
213
+ MIT License
214
+
215
+ ---
216
+
217
+ **Développé avec ❤️ par l'équipe HOLOKIA**
218
+ "@
219
+
220
+ $readmeContent | Out-File -FilePath "README.md" -Encoding UTF8
221
+
222
+ Write-Success "Fichiers HF créés"
223
+ }
224
+
225
+ # Configuration Git
226
+ function Set-GitConfiguration {
227
+ Write-Info "Configuration Git..."
228
+
229
+ # Ajouter tous les fichiers
230
+ git add .
231
+
232
+ # Commit initial
233
+ git commit -m "Initial commit: HOLOKIA-AVATAR for Hugging Face Spaces"
234
+
235
+ Write-Success "Configuration Git terminée"
236
+ }
237
+
238
+ # Fonction principale
239
+ function Main {
240
+ Write-Host "🧹 HOLOKIA-AVATAR - Nettoyage et restructuration pour Hugging Face" -ForegroundColor Cyan
241
+ Write-Host "==================================================================" -ForegroundColor Cyan
242
+ Write-Host ""
243
+
244
+ # Vérifications
245
+ Test-Requirements
246
+
247
+ # Sauvegarde
248
+ Backup-ImportantFiles
249
+
250
+ # Nettoyage
251
+ Clear-GitHistory
252
+
253
+ # Restructuration
254
+ Restructure-Files
255
+
256
+ # Création des fichiers HF
257
+ New-HuggingFaceFiles
258
+
259
+ # Configuration Git
260
+ Set-GitConfiguration
261
+
262
+ Write-Host ""
263
+ Write-Success "🎉 Nettoyage et restructuration terminés!"
264
+ Write-Host ""
265
+ Write-Info "Prochaines étapes:"
266
+ Write-Host "1. Ajoutez le remote Hugging Face:"
267
+ Write-Host " git remote add origin https://huggingface.co/spaces/DataSage12/avatar-v2"
268
+ Write-Host ""
269
+ Write-Host "2. Poussez le code:"
270
+ Write-Host " git push -u origin main"
271
+ Write-Host ""
272
+ Write-Host "3. Ajoutez votre clé API Groq dans les secrets du Space"
273
+ Write-Host ""
274
+ Write-Warning "Sauvegarde disponible dans: ..\backup_holokia_avatar\"
275
+ }
276
+
277
+ # Exécution
278
+ Main
clean_and_restructure.sh ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # ===========================================
4
+ # Script de nettoyage et restructuration pour Hugging Face
5
+ # HOLOKIA-AVATAR - Avatar 3D Interactif
6
+ # ===========================================
7
+
8
+ set -e
9
+
10
+ # Couleurs pour les logs
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ BLUE='\033[0;34m'
15
+ NC='\033[0m' # No Color
16
+
17
+ # Fonctions utilitaires
18
+ log_info() {
19
+ echo -e "${BLUE}[INFO]${NC} $1"
20
+ }
21
+
22
+ log_success() {
23
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
24
+ }
25
+
26
+ log_warning() {
27
+ echo -e "${YELLOW}[WARNING]${NC} $1"
28
+ }
29
+
30
+ log_error() {
31
+ echo -e "${RED}[ERROR]${NC} $1"
32
+ }
33
+
34
+ # Vérifications préliminaires
35
+ check_requirements() {
36
+ log_info "Vérification des prérequis..."
37
+
38
+ # Vérifier Git
39
+ if ! command -v git &> /dev/null; then
40
+ log_error "Git n'est pas installé"
41
+ exit 1
42
+ fi
43
+
44
+ # Vérifier Git LFS
45
+ if ! command -v git-lfs &> /dev/null; then
46
+ log_warning "Git LFS n'est pas installé"
47
+ log_info "Installation de Git LFS..."
48
+ if command -v apt-get &> /dev/null; then
49
+ sudo apt-get install git-lfs
50
+ elif command -v brew &> /dev/null; then
51
+ brew install git-lfs
52
+ else
53
+ log_error "Impossible d'installer Git LFS automatiquement"
54
+ log_info "Installez Git LFS manuellement: https://git-lfs.github.io/"
55
+ exit 1
56
+ fi
57
+ fi
58
+
59
+ log_success "Tous les prérequis sont satisfaits"
60
+ }
61
+
62
+ # Sauvegarde des fichiers importants
63
+ backup_important_files() {
64
+ log_info "Sauvegarde des fichiers importants..."
65
+
66
+ # Créer un dossier de sauvegarde
67
+ mkdir -p ../backup_holokia_avatar
68
+
69
+ # Sauvegarder les fichiers de configuration
70
+ cp -r . ../backup_holokia_avatar/ 2>/dev/null || true
71
+
72
+ log_success "Sauvegarde terminée dans ../backup_holokia_avatar/"
73
+ }
74
+
75
+ # Nettoyage de l'historique Git
76
+ clean_git_history() {
77
+ log_info "Nettoyage de l'historique Git..."
78
+
79
+ # Supprimer le dossier .git existant
80
+ if [ -d ".git" ]; then
81
+ log_info "Suppression de l'historique Git existant..."
82
+ rm -rf .git
83
+ fi
84
+
85
+ # Initialiser un nouveau dépôt Git
86
+ log_info "Initialisation d'un nouveau dépôt Git..."
87
+ git init
88
+
89
+ # Configurer Git LFS
90
+ log_info "Configuration de Git LFS..."
91
+ git lfs install
92
+
93
+ # Ajouter les fichiers LFS
94
+ git lfs track "*.glb"
95
+ git lfs track "*.fbx"
96
+ git lfs track "*.obj"
97
+ git lfs track "*.png"
98
+ git lfs track "*.jpg"
99
+ git lfs track "*.jpeg"
100
+ git lfs track "*.wav"
101
+ git lfs track "*.mp3"
102
+
103
+ log_success "Historique Git nettoyé et LFS configuré"
104
+ }
105
+
106
+ # Restructuration des fichiers
107
+ restructure_files() {
108
+ log_info "Restructuration des fichiers pour Hugging Face..."
109
+
110
+ # Supprimer les fichiers de développement
111
+ rm -f build-frontend.sh
112
+ rm -f deploy_to_hf.sh
113
+ rm -f test_deployment.py
114
+ rm -f README_HF_DEPLOYMENT.md
115
+ rm -f huggingface_hub_config.yaml
116
+ rm -rf docker/
117
+ rm -f docker-compose*.yml
118
+
119
+ # Supprimer les fichiers de cache et temporaires
120
+ rm -rf Back-end/tts_cache/
121
+ rm -rf Back-end/logs/
122
+ rm -rf frontend/node_modules/
123
+ rm -rf frontend/dist/
124
+ rm -rf __pycache__/
125
+ rm -rf .pytest_cache/
126
+
127
+ # Supprimer les fichiers .env s'ils existent
128
+ find . -name ".env*" -not -name ".env.example" -delete 2>/dev/null || true
129
+
130
+ log_success "Restructuration terminée"
131
+ }
132
+
133
+ # Création des fichiers nécessaires pour HF
134
+ create_hf_files() {
135
+ log_info "Création des fichiers nécessaires pour Hugging Face..."
136
+
137
+ # Créer un README.md simple pour HF
138
+ cat > README.md << 'EOF'
139
+ ---
140
+ title: HOLOKIA-AVATAR
141
+ emoji: 🤖
142
+ colorFrom: blue
143
+ colorTo: purple
144
+ sdk: docker
145
+ pinned: false
146
+ license: mit
147
+ short_description: Avatar 3D intelligent avec IA conversationnelle en temps réel
148
+ ---
149
+
150
+ # 🤖 HOLOKIA-AVATAR - Avatar 3D Interactif
151
+
152
+ Un avatar 3D intelligent avec capacités de conversation en temps réel, utilisant l'IA pour des interactions naturelles.
153
+
154
+ ## ✨ Fonctionnalités
155
+
156
+ - 🎭 **Avatar 3D interactif** avec synchronisation labiale
157
+ - 🗣️ **Reconnaissance vocale** (Speech-to-Text)
158
+ - 🔊 **Synthèse vocale** (Text-to-Speech)
159
+ - 🧠 **IA conversationnelle** (Groq LLM)
160
+ - 📡 **Streaming temps réel** via WebSocket
161
+ - 🌐 **Interface web moderne** avec React et Three.js
162
+
163
+ ## 🚀 Utilisation
164
+
165
+ 1. **Parlez** à l'avatar via le microphone
166
+ 2. **L'avatar écoute** et comprend votre message
167
+ 3. **L'IA génère** une réponse intelligente
168
+ 4. **L'avatar parle** avec synchronisation labiale
169
+ 5. **Interaction continue** en temps réel
170
+
171
+ ## 🔧 Configuration
172
+
173
+ Ajoutez votre clé API Groq dans les **Secrets** du Space :
174
+ - Nom : `GROQ_API_KEY`
175
+ - Valeur : `gsk_...` (votre clé API)
176
+
177
+ ## 📄 Licence
178
+
179
+ MIT License
180
+
181
+ ---
182
+
183
+ **Développé avec ❤️ par l'équipe HOLOKIA**
184
+ EOF
185
+
186
+ log_success "Fichiers HF créés"
187
+ }
188
+
189
+ # Configuration Git
190
+ setup_git() {
191
+ log_info "Configuration Git..."
192
+
193
+ # Ajouter tous les fichiers
194
+ git add .
195
+
196
+ # Commit initial
197
+ git commit -m "Initial commit: HOLOKIA-AVATAR for Hugging Face Spaces"
198
+
199
+ log_success "Configuration Git terminée"
200
+ }
201
+
202
+ # Fonction principale
203
+ main() {
204
+ echo "🧹 HOLOKIA-AVATAR - Nettoyage et restructuration pour Hugging Face"
205
+ echo "=================================================================="
206
+ echo ""
207
+
208
+ # Vérifications
209
+ check_requirements
210
+
211
+ # Sauvegarde
212
+ backup_important_files
213
+
214
+ # Nettoyage
215
+ clean_git_history
216
+
217
+ # Restructuration
218
+ restructure_files
219
+
220
+ # Création des fichiers HF
221
+ create_hf_files
222
+
223
+ # Configuration Git
224
+ setup_git
225
+
226
+ echo ""
227
+ log_success "🎉 Nettoyage et restructuration terminés!"
228
+ echo ""
229
+ log_info "Prochaines étapes:"
230
+ echo "1. Ajoutez le remote Hugging Face:"
231
+ echo " git remote add origin https://huggingface.co/spaces/DataSage12/avatar-v2"
232
+ echo ""
233
+ echo "2. Poussez le code:"
234
+ echo " git push -u origin main"
235
+ echo ""
236
+ echo "3. Ajoutez votre clé API Groq dans les secrets du Space"
237
+ echo ""
238
+ log_warning "Sauvegarde disponible dans: ../backup_holokia_avatar/"
239
+ }
240
+
241
+ # Gestion des arguments
242
+ case "${1:-}" in
243
+ --help|-h)
244
+ echo "Usage: $0 [OPTIONS]"
245
+ echo ""
246
+ echo "Ce script nettoie l'historique Git et restructure le projet pour Hugging Face Spaces."
247
+ echo ""
248
+ echo "Options:"
249
+ echo " --help Afficher cette aide"
250
+ echo ""
251
+ echo "ATTENTION: Ce script va supprimer l'historique Git existant!"
252
+ echo "Une sauvegarde sera créée dans ../backup_holokia_avatar/"
253
+ echo ""
254
+ exit 0
255
+ ;;
256
+ *)
257
+ main "$@"
258
+ ;;
259
+ esac
debug_websocket.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script de diagnostic pour le problème WebSocket
4
+ """
5
+
6
+ import asyncio
7
+ import aiohttp
8
+ import json
9
+ import sys
10
+
11
+ async def test_websocket_connection():
12
+ """Teste la connexion WebSocket"""
13
+ print("🔍 Test de connexion WebSocket...")
14
+
15
+ try:
16
+ # Test de connexion WebSocket
17
+ async with aiohttp.ClientSession() as session:
18
+ async with session.ws_connect("ws://localhost:5003/live_stream/test_user?lang=fr&room_id=test") as ws:
19
+ print("✅ Connexion WebSocket établie")
20
+
21
+ # Envoyer un message ping
22
+ ping_message = {"type": "ping"}
23
+ await ws.send_str(json.dumps(ping_message))
24
+ print("✅ Message ping envoyé")
25
+
26
+ # Attendre une réponse
27
+ try:
28
+ async with asyncio.timeout(5):
29
+ msg = await ws.receive()
30
+ print(f"✅ Réponse reçue: {msg}")
31
+ except asyncio.TimeoutError:
32
+ print("⚠️ Aucune réponse reçue dans les 5 secondes")
33
+
34
+ await ws.close()
35
+ print("✅ Connexion WebSocket fermée")
36
+
37
+ except Exception as e:
38
+ print(f"❌ Erreur WebSocket: {e}")
39
+ import traceback
40
+ print(f"Traceback: {traceback.format_exc()}")
41
+
42
+ async def main():
43
+ print("🚀 Démarrage du diagnostic WebSocket...")
44
+ await test_websocket_connection()
45
+ print("✅ Diagnostic terminé")
46
+
47
+ if __name__ == "__main__":
48
+ asyncio.run(main())
deploy_to_hf_final.ps1 ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Script de déploiement final pour Hugging Face Spaces
3
+ # HOLOKIA-AVATAR - Avatar 3D Interactif
4
+ # ===========================================
5
+
6
+ param(
7
+ [switch]$Help,
8
+ [string]$HuggingFaceToken = ""
9
+ )
10
+
11
+ # Configuration
12
+ $HF_USERNAME = "DataSage12"
13
+ $SPACE_NAME = "avatar-v2"
14
+ $REPO_URL = "https://huggingface.co/spaces/$HF_USERNAME/$SPACE_NAME"
15
+
16
+ # Fonctions utilitaires
17
+ function Write-Info {
18
+ param([string]$Message)
19
+ Write-Host "[INFO] $Message" -ForegroundColor Blue
20
+ }
21
+
22
+ function Write-Success {
23
+ param([string]$Message)
24
+ Write-Host "[SUCCESS] $Message" -ForegroundColor Green
25
+ }
26
+
27
+ function Write-Warning {
28
+ param([string]$Message)
29
+ Write-Host "[WARNING] $Message" -ForegroundColor Yellow
30
+ }
31
+
32
+ function Write-Error {
33
+ param([string]$Message)
34
+ Write-Host "[ERROR] $Message" -ForegroundColor Red
35
+ }
36
+
37
+ # Aide
38
+ if ($Help) {
39
+ Write-Host "Usage: .\deploy_to_hf_final.ps1 [OPTIONS]"
40
+ Write-Host ""
41
+ Write-Host "Options:"
42
+ Write-Host " -Help Afficher cette aide"
43
+ Write-Host " -HuggingFaceToken <token> Token d'accès Hugging Face (optionnel)"
44
+ Write-Host ""
45
+ Write-Host "Ce script déploie HOLOKIA-AVATAR sur Hugging Face Spaces."
46
+ Write-Host ""
47
+ Write-Host "Variables d'environnement requises:"
48
+ Write-Host " GROQ_API_KEY Clé API Groq"
49
+ Write-Host ""
50
+ exit 0
51
+ }
52
+
53
+ # Vérifications préliminaires
54
+ function Test-Requirements {
55
+ Write-Info "Vérification des prérequis..."
56
+
57
+ # Vérifier Git
58
+ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
59
+ Write-Error "Git n'est pas installé"
60
+ exit 1
61
+ }
62
+
63
+ # Vérifier Git LFS
64
+ if (-not (Get-Command git-lfs -ErrorAction SilentlyContinue)) {
65
+ Write-Error "Git LFS n'est pas installé"
66
+ Write-Info "Installez Git LFS: winget install Git.Git-LFS"
67
+ exit 1
68
+ }
69
+
70
+ # Vérifier la clé API Groq
71
+ $groqKey = $env:GROQ_API_KEY
72
+ if (-not $groqKey) {
73
+ Write-Error "GROQ_API_KEY n'est pas définie"
74
+ Write-Info "Définissez votre clé API avec: `$env:GROQ_API_KEY = 'your_key_here'"
75
+ exit 1
76
+ }
77
+
78
+ Write-Success "Tous les prérequis sont satisfaits"
79
+ Write-Info "Clé API Groq: $($groqKey.Substring(0, 10))..."
80
+ }
81
+
82
+ # Vérifier la structure du projet
83
+ function Test-ProjectStructure {
84
+ Write-Info "Vérification de la structure du projet..."
85
+
86
+ $requiredFiles = @(
87
+ "Dockerfile",
88
+ "app.py",
89
+ "requirements.txt",
90
+ "Back-end\start_services.py",
91
+ "Back-end\services\tts_service.py",
92
+ "Back-end\services\stt_service.py",
93
+ "Back-end\services\llm_service.py",
94
+ "Back-end\services\live_stream_service.py",
95
+ "frontend\package.json",
96
+ "frontend\src\App.jsx"
97
+ )
98
+
99
+ $missingFiles = @()
100
+ foreach ($file in $requiredFiles) {
101
+ if (-not (Test-Path $file)) {
102
+ $missingFiles += $file
103
+ }
104
+ }
105
+
106
+ if ($missingFiles.Count -gt 0) {
107
+ Write-Error "Fichiers manquants:"
108
+ foreach ($file in $missingFiles) {
109
+ Write-Host " - $file" -ForegroundColor Red
110
+ }
111
+ exit 1
112
+ }
113
+
114
+ Write-Success "Structure du projet validée"
115
+ }
116
+
117
+ # Configuration Git
118
+ function Set-GitConfiguration {
119
+ Write-Info "Configuration Git..."
120
+
121
+ # Vérifier si le remote HF existe
122
+ $existingRemote = git remote get-url origin 2>$null
123
+ if ($existingRemote) {
124
+ Write-Info "Remote existant: $existingRemote"
125
+ if ($existingRemote -ne $REPO_URL) {
126
+ Write-Info "Mise à jour du remote..."
127
+ git remote set-url origin $REPO_URL
128
+ }
129
+ } else {
130
+ Write-Info "Ajout du remote Hugging Face..."
131
+ git remote add origin $REPO_URL
132
+ }
133
+
134
+ # Configurer l'authentification si un token est fourni
135
+ if ($HuggingFaceToken) {
136
+ Write-Info "Configuration de l'authentification..."
137
+ $authUrl = $REPO_URL -replace "https://", "https://$HuggingFaceToken@"
138
+ git remote set-url origin $authUrl
139
+ }
140
+
141
+ Write-Success "Configuration Git terminée"
142
+ }
143
+
144
+ # Test de l'image Docker
145
+ function Test-DockerImage {
146
+ Write-Info "Test de l'image Docker localement..."
147
+
148
+ # Vérifier Docker
149
+ if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
150
+ Write-Warning "Docker n'est pas installé, test ignoré"
151
+ return
152
+ }
153
+
154
+ try {
155
+ # Construire l'image
156
+ Write-Info "Construction de l'image Docker..."
157
+ docker build -t holokia-avatar:test .
158
+
159
+ # Tester l'image
160
+ Write-Info "Test de l'image..."
161
+ $containerId = docker run -d -p 7860:7860 -e GROQ_API_KEY=$env:GROQ_API_KEY holokia-avatar:test
162
+
163
+ # Attendre le démarrage
164
+ Write-Info "Attente du démarrage du service..."
165
+ Start-Sleep -Seconds 60
166
+
167
+ # Vérifier la santé
168
+ try {
169
+ $response = Invoke-WebRequest -Uri "http://localhost:7860/health" -TimeoutSec 10
170
+ if ($response.StatusCode -eq 200) {
171
+ Write-Success "Test local réussi"
172
+ } else {
173
+ Write-Warning "Test local: code de statut $($response.StatusCode)"
174
+ }
175
+ } catch {
176
+ Write-Warning "Test local: service non accessible"
177
+ }
178
+
179
+ # Nettoyer
180
+ docker stop $containerId
181
+ docker rm $containerId
182
+ docker rmi holokia-avatar:test
183
+
184
+ } catch {
185
+ Write-Warning "Erreur lors du test Docker: $($_.Exception.Message)"
186
+ }
187
+ }
188
+
189
+ # Déploiement
190
+ function Deploy-ToHuggingFace {
191
+ Write-Info "Déploiement sur Hugging Face Spaces..."
192
+
193
+ # Ajouter tous les fichiers
194
+ git add .
195
+
196
+ # Commit
197
+ $commitMessage = "Deploy to Hugging Face Spaces - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
198
+ git commit -m $commitMessage
199
+
200
+ # Push vers HF
201
+ Write-Info "Push vers Hugging Face..."
202
+ git push -u origin main
203
+
204
+ Write-Success "Déploiement terminé!"
205
+ }
206
+
207
+ # Affichage des informations
208
+ function Show-Info {
209
+ Write-Info "Informations de déploiement:"
210
+ Write-Host " - Utilisateur HF: $HF_USERNAME"
211
+ Write-Host " - Nom du Space: $SPACE_NAME"
212
+ Write-Host " - URL du Space: $REPO_URL"
213
+ Write-Host " - Clé API Groq: $($env:GROQ_API_KEY.Substring(0, 10))..."
214
+ Write-Host ""
215
+ }
216
+
217
+ # Fonction principale
218
+ function Main {
219
+ Write-Host "🚀 HOLOKIA-AVATAR - Déploiement sur Hugging Face Spaces" -ForegroundColor Cyan
220
+ Write-Host "=====================================================" -ForegroundColor Cyan
221
+ Write-Host ""
222
+
223
+ Show-Info
224
+
225
+ # Vérifications
226
+ Test-Requirements
227
+ Test-ProjectStructure
228
+
229
+ # Test Docker (optionnel)
230
+ Test-DockerImage
231
+
232
+ # Configuration Git
233
+ Set-GitConfiguration
234
+
235
+ # Déploiement
236
+ Deploy-ToHuggingFace
237
+
238
+ Write-Host ""
239
+ Write-Success "🎉 Déploiement terminé avec succès!"
240
+ Write-Host ""
241
+ Write-Info "Votre Space sera disponible à:"
242
+ Write-Host " $REPO_URL"
243
+ Write-Host ""
244
+ Write-Info "N'oubliez pas d'ajouter votre clé API Groq dans les secrets du Space:"
245
+ Write-Host " - Nom: GROQ_API_KEY"
246
+ Write-Host " - Valeur: $env:GROQ_API_KEY"
247
+ Write-Host ""
248
+ Write-Info "Le Space va automatiquement se construire et démarrer dans quelques minutes."
249
+ }
250
+
251
+ # Exécution
252
+ Main
env.example ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Variables d'environnement pour HOLOKIA-AVATAR
3
+ # ===========================================
4
+
5
+ # Clé API Groq (requise)
6
+ GROQ_API_KEY=gsk_your_groq_api_key_here
7
+
8
+ # Configuration Hugging Face (pour le déploiement)
9
+ HF_USERNAME=your_huggingface_username
10
+ SPACE_NAME=holokia-avatar
11
+
12
+ # Configuration des services
13
+ PYTHONPATH=/app
14
+ PORT=7860
15
+
16
+ # Configuration des logs
17
+ LOG_LEVEL=INFO
18
+
19
+ # Configuration TTS
20
+ TTS_CACHE_DIR=/app/tts_cache
21
+ TTS_LANGUAGES=fr,en,ar
22
+
23
+ # Configuration STT
24
+ STT_MODEL_SIZE=small
25
+ STT_MIN_AUDIO_DURATION=0.75
26
+
27
+ # Configuration LLM
28
+ LLM_MODEL_NAME=meta-llama/llama-4-scout-17b-16e-instruct
29
+ LLM_TEMPERATURE=0
30
+
31
+ # Configuration WebSocket
32
+ WS_PING_INTERVAL=20
33
+ WS_PING_TIMEOUT=120
34
+
35
+ # Configuration Nginx
36
+ NGINX_WORKER_PROCESSES=auto
37
+ NGINX_WORKER_CONNECTIONS=1024
frontend/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Holokia — Avatar vocal</title>
7
+ </head>
8
+ <body className="bg-gray-50 text-gray-900">
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "commonjs",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@react-three/drei": "^10.6.1",
13
+ "@react-three/fiber": "^9.3.0",
14
+ "@react-three/postprocessing": "^3.0.4",
15
+ "@tailwindcss/vite": "^4.0.9",
16
+ "axios": "^1.11.0",
17
+ "franc-min": "^6.2.0",
18
+ "leva": "^0.10.0",
19
+ "postcss": "^8.5.6",
20
+ "react": "^19.1.1",
21
+ "react-dom": "^19.1.1",
22
+ "react-icons": "^5.5.0",
23
+ "three": "^0.174.0",
24
+ "uuid": "^11.1.0",
25
+ "wawa-lipsync": "^0.0.1",
26
+ "wawa-vfx": "^1.0.16",
27
+ "zustand": "^5.0.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19.0.10",
31
+ "@types/react-dom": "^19.0.4",
32
+ "@vitejs/plugin-react": "^4.3.4",
33
+ "autoprefixer": "^10.4.21",
34
+ "framer-motion": "^11.18.2",
35
+ "globals": "^15.15.0",
36
+ "postcss": "^8.4.38",
37
+ "postcss-import": "^16.0.1",
38
+ "postcss-nested": "^6.0.1",
39
+ "tailwindcss": "^3.4.1",
40
+ "vite": "^6.2.0"
41
+ }
42
+ }
frontend/public/images/holokia.jpeg ADDED

Git LFS Details

  • SHA256: 8cb9fe6a62fb131b7a8b13fa8ec1bbe020979c76aac203e26ce193af931593f1
  • Pointer size: 130 Bytes
  • Size of remote file: 40.8 kB
frontend/public/images/wawasensei-white.png ADDED

Git LFS Details

  • SHA256: 3ff1a3e6ada5327651f1a5d6365d8cf0fef61b0b316d970bf135a22719d03894
  • Pointer size: 130 Bytes
  • Size of remote file: 17.9 kB
frontend/public/images/wawasensei.png ADDED

Git LFS Details

  • SHA256: 53f6fd7dd3e69d77880f3b8b809cfa63525dbe41e79c52fa595d3a805d430b60
  • Pointer size: 130 Bytes
  • Size of remote file: 12.2 kB
frontend/public/pcm-worklet.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class PCMProcessor extends AudioWorkletProcessor {
2
+ constructor(options) {
3
+ super();
4
+ const o = options?.processorOptions || {};
5
+ this.targetSampleRate = o.sampleRate || 16000;
6
+ this.inputSampleRate = o.inputSampleRate || sampleRate;
7
+ this.chunkSize = o.chunkSize || 1024;
8
+ this.silenceThreshold = o.silenceThreshold || 0.01;
9
+ this.silenceWindow = o.silenceWindow || 3;
10
+ this.buffer = [];
11
+ this.silenceCount = 0;
12
+
13
+ this.port.onmessage = (e) => {
14
+ const data = e.data || {};
15
+ if (data.type === "flush") {
16
+ this._flush();
17
+ } else if (data.type === "config") {
18
+ if (typeof data.targetSampleRate === "number") this.targetSampleRate = data.targetSampleRate;
19
+ if (typeof data.inputSampleRate === "number") this.inputSampleRate = data.inputSampleRate;
20
+ if (typeof data.chunkSize === "number" && data.chunkSize > 0) this.chunkSize = data.chunkSize;
21
+ if (typeof data.silenceThreshold === "number") this.silenceThreshold = data.silenceThreshold;
22
+ if (typeof data.silenceWindow === "number") this.silenceWindow = data.silenceWindow;
23
+ }
24
+ };
25
+ }
26
+
27
+ _isSilence(chunk) {
28
+ if (!chunk || chunk.length === 0) return true;
29
+ let sum = 0;
30
+ for (let i = 0; i < chunk.length; i++) {
31
+ sum += chunk[i] * chunk[i];
32
+ }
33
+ const rms = Math.sqrt(sum / chunk.length);
34
+ return rms < this.silenceThreshold;
35
+ }
36
+
37
+ _resampleFloat32(buffer, inSR, outSR) {
38
+ if (inSR === outSR) return buffer;
39
+ const ratio = inSR / outSR;
40
+ const newLength = Math.max(1, Math.round(buffer.length / ratio));
41
+ const out = new Float32Array(newLength);
42
+ for (let i = 0; i < newLength; i++) {
43
+ const idx = i * ratio;
44
+ const idxFloor = Math.floor(idx);
45
+ const idxCeil = Math.min(buffer.length - 1, idxFloor + 1);
46
+ const t = idx - idxFloor;
47
+ out[i] = (1 - t) * buffer[idxFloor] + t * buffer[idxCeil];
48
+ }
49
+ return out;
50
+ }
51
+
52
+ _floatTo16BitPCM(float32Array) {
53
+ const out = new Int16Array(float32Array.length);
54
+ for (let i = 0; i < float32Array.length; i++) {
55
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
56
+ out[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
57
+ }
58
+ return out;
59
+ }
60
+
61
+ _emitChunkIfReady() {
62
+ if (this.buffer.length >= this.chunkSize) {
63
+ const take = this.buffer.splice(0, this.chunkSize);
64
+ if (take.length === 0) return;
65
+ const f32 = new Float32Array(take);
66
+ const pcm16 = this._floatTo16BitPCM(f32);
67
+ this.port.postMessage({ buffer: pcm16.buffer }, [pcm16.buffer]);
68
+ }
69
+ }
70
+
71
+ _flush() {
72
+ if (this.buffer.length > 0) {
73
+ const f32 = new Float32Array(this.buffer);
74
+ this.buffer.length = 0;
75
+ const pcm16 = this._floatTo16BitPCM(f32);
76
+ this.port.postMessage({ buffer: pcm16.buffer }, [pcm16.buffer]);
77
+ }
78
+ }
79
+
80
+ process(inputs) {
81
+ const input = inputs[0];
82
+ if (!input || input.length === 0) return true;
83
+
84
+ const channel = input[0];
85
+ if (!channel) return true;
86
+
87
+ if (this._isSilence(channel)) {
88
+ this.silenceCount++;
89
+ if (this.silenceCount >= this.silenceWindow) {
90
+ return true;
91
+ }
92
+ } else {
93
+ this.silenceCount = 0;
94
+ }
95
+
96
+ const resampled = this._resampleFloat32(channel, this.inputSampleRate, this.targetSampleRate);
97
+
98
+ for (let i = 0; i < resampled.length; i++) {
99
+ this.buffer.push(resampled[i]);
100
+ if (this.buffer.length >= this.chunkSize) {
101
+ this._emitChunkIfReady();
102
+ }
103
+ }
104
+ return true;
105
+ }
106
+ }
107
+
108
+ registerProcessor("pcm-processor", PCMProcessor);
frontend/public/textures/Rotated/flame_05_rotated.png ADDED

Git LFS Details

  • SHA256: 08afb8deef9edbfb47571805a2f5b1316c5c99d5056f23b13d060eece3a752de
  • Pointer size: 130 Bytes
  • Size of remote file: 12.2 kB
frontend/public/textures/Rotated/flame_06_rotated.png ADDED

Git LFS Details

  • SHA256: c72289cfe6e25d2763eef9db9b55a38f04d027ba0ca7fe6d4830b2ebd9b63dce
  • Pointer size: 130 Bytes
  • Size of remote file: 15 kB
frontend/public/textures/Rotated/muzzle_01_rotated.png ADDED

Git LFS Details

  • SHA256: 6b0bae33c25ef54702f40272d121412131e8f089ae03c958533f4887288d8f59
  • Pointer size: 130 Bytes
  • Size of remote file: 75.7 kB
frontend/public/textures/Rotated/muzzle_02_rotated.png ADDED

Git LFS Details

  • SHA256: 6bca0de7b38b9bc7ea7ad38940720ebfe138363155848cfb7bf15e56681defe9
  • Pointer size: 130 Bytes
  • Size of remote file: 49.4 kB
frontend/public/textures/Rotated/muzzle_03_rotated.png ADDED

Git LFS Details

  • SHA256: 3a625935b4b2a89beb0f2aefb3db3a50c062f2bd3a99cf2a8aaf3f53cef437bb
  • Pointer size: 130 Bytes
  • Size of remote file: 48.1 kB
frontend/public/textures/Rotated/muzzle_04_rotated.png ADDED

Git LFS Details

  • SHA256: 76a51de75e489e5007d4e0140824120c7eda19877127d78e6531cfb0166df279
  • Pointer size: 130 Bytes
  • Size of remote file: 51.2 kB
frontend/public/textures/Rotated/muzzle_05_rotated.png ADDED

Git LFS Details

  • SHA256: c86c85323f6a2ad0e0d1349f17bc88b3951b8ac653c4b0bc133fb0baa28eaae9
  • Pointer size: 130 Bytes
  • Size of remote file: 44.5 kB
frontend/public/textures/Rotated/spark_05_rotated.png ADDED

Git LFS Details

  • SHA256: 9bceb2b1a8a46d9eaffb4c77d5c4114ff7af8fcd36e586feb1f91bfe2af0e671
  • Pointer size: 130 Bytes
  • Size of remote file: 56.5 kB
frontend/public/textures/Rotated/spark_06_rotated.png ADDED

Git LFS Details

  • SHA256: 1c47a5d892ac1816c8eb18ebddb1ecf7769a35bbdd5a6671677b565006623936
  • Pointer size: 130 Bytes
  • Size of remote file: 41.4 kB
frontend/public/textures/Rotated/trace_01_rotated.png ADDED

Git LFS Details

  • SHA256: 6832399b81f21dfb9379159e25ea2efab85258f24a03a2eb7a3f9285829f98d4
  • Pointer size: 130 Bytes
  • Size of remote file: 28.4 kB
frontend/public/textures/Rotated/trace_02_rotated.png ADDED

Git LFS Details

  • SHA256: 81c30ba255eacd9ff29b5c0b8028b2714a375d6dc7cd11133f515bc37123f0e3
  • Pointer size: 130 Bytes
  • Size of remote file: 31.2 kB
frontend/public/textures/Rotated/trace_03_rotated.png ADDED

Git LFS Details

  • SHA256: 0037f0c659d2a546b0de452b9ea2d910626c3994154395fb84bf28b9c249c2e1
  • Pointer size: 130 Bytes
  • Size of remote file: 34.9 kB
frontend/public/textures/Rotated/trace_04_rotated.png ADDED

Git LFS Details

  • SHA256: 857d9fe44e1af7123c1d1006caac5a3707588412997bc8f16591f01a2f73f6e9
  • Pointer size: 130 Bytes
  • Size of remote file: 36.9 kB
frontend/public/textures/Rotated/trace_05_rotated.png ADDED

Git LFS Details

  • SHA256: 513aca6608ab2c3a677aacc99ad1055a200cd9a703b247c69fabce9695cce2e0
  • Pointer size: 130 Bytes
  • Size of remote file: 36.7 kB
frontend/public/textures/Rotated/trace_06_rotated.png ADDED

Git LFS Details

  • SHA256: 098bd782fbbac098e30357acdef764184e601ccdf05d109f3dcfb698e5a7ea60
  • Pointer size: 130 Bytes
  • Size of remote file: 30.5 kB
frontend/public/textures/Rotated/trace_07_rotated.png ADDED

Git LFS Details

  • SHA256: fdfcc20aa9a194f8eee4081bf0f8de21fef48abcba4f073c183a406cdeb45f86
  • Pointer size: 130 Bytes
  • Size of remote file: 27.4 kB
frontend/public/textures/circle_01.png ADDED

Git LFS Details

  • SHA256: 4b2d03683bf0fe4a946567adc3bd86b8ba045da84cee58dc2cf8aef63bbfaa06
  • Pointer size: 130 Bytes
  • Size of remote file: 69.8 kB
frontend/public/textures/circle_02.png ADDED

Git LFS Details

  • SHA256: 8a2112a6a5610c7cdcbfe8af0a5162b42dfb3431c63f494743945337b7981e51
  • Pointer size: 130 Bytes
  • Size of remote file: 43.4 kB
frontend/public/textures/circle_03.png ADDED

Git LFS Details

  • SHA256: ed6c6f082e666c16bcaca89f15aefb55df9bf69450090518bcae1d3d61f82936
  • Pointer size: 130 Bytes
  • Size of remote file: 73 kB
frontend/public/textures/circle_04.png ADDED

Git LFS Details

  • SHA256: 742da1a1b96b93ae446700f6085385d1f62844352da870c89427307b7b7cf03b
  • Pointer size: 130 Bytes
  • Size of remote file: 59.6 kB
frontend/public/textures/circle_05.png ADDED

Git LFS Details

  • SHA256: 925b8ac284436f74f9cadf0ecd058da1c08fba65c098e4e34fd220603022f02e
  • Pointer size: 130 Bytes
  • Size of remote file: 65.3 kB
frontend/public/textures/dirt_01.png ADDED

Git LFS Details

  • SHA256: 6827a0a32a293ec9570fb98d63963b8dd6e6aaba9e1096afad932e93a378bea9
  • Pointer size: 130 Bytes
  • Size of remote file: 47.3 kB
frontend/public/textures/dirt_02.png ADDED

Git LFS Details

  • SHA256: fcecc70b561f80568b3391c3dc1180126d86ce4fbd4d1b01518e6045203e10a3
  • Pointer size: 130 Bytes
  • Size of remote file: 54.1 kB
frontend/public/textures/dirt_03.png ADDED

Git LFS Details

  • SHA256: 427d6dcad5a8d16b8143d412a5a48ce859716150a074ea5bb36eca577c0ae541
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB