Spaces:
Sleeping
Sleeping
Commit ·
de63014
0
Parent(s):
Initial commit - HOLOKIA-AVATAR v2.2
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +111 -0
- .gitignore +175 -0
- Back-end/README.md +118 -0
- Back-end/app/__init__.py +0 -0
- Back-end/app/main.py +69 -0
- Back-end/requirements.txt +12 -0
- Back-end/services/live_stream_service.py +445 -0
- Back-end/services/llm_service.py +168 -0
- Back-end/services/stt_service.py +180 -0
- Back-end/services/tts_service.py +119 -0
- Back-end/start_services.py +121 -0
- Dockerfile +92 -0
- README.md +45 -0
- app.py +221 -0
- clean_and_restructure.ps1 +278 -0
- clean_and_restructure.sh +259 -0
- debug_websocket.py +48 -0
- deploy_to_hf_final.ps1 +252 -0
- env.example +37 -0
- frontend/index.html +12 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +42 -0
- frontend/public/images/holokia.jpeg +3 -0
- frontend/public/images/wawasensei-white.png +3 -0
- frontend/public/images/wawasensei.png +3 -0
- frontend/public/pcm-worklet.js +108 -0
- frontend/public/textures/Rotated/flame_05_rotated.png +3 -0
- frontend/public/textures/Rotated/flame_06_rotated.png +3 -0
- frontend/public/textures/Rotated/muzzle_01_rotated.png +3 -0
- frontend/public/textures/Rotated/muzzle_02_rotated.png +3 -0
- frontend/public/textures/Rotated/muzzle_03_rotated.png +3 -0
- frontend/public/textures/Rotated/muzzle_04_rotated.png +3 -0
- frontend/public/textures/Rotated/muzzle_05_rotated.png +3 -0
- frontend/public/textures/Rotated/spark_05_rotated.png +3 -0
- frontend/public/textures/Rotated/spark_06_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_01_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_02_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_03_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_04_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_05_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_06_rotated.png +3 -0
- frontend/public/textures/Rotated/trace_07_rotated.png +3 -0
- frontend/public/textures/circle_01.png +3 -0
- frontend/public/textures/circle_02.png +3 -0
- frontend/public/textures/circle_03.png +3 -0
- frontend/public/textures/circle_04.png +3 -0
- frontend/public/textures/circle_05.png +3 -0
- frontend/public/textures/dirt_01.png +3 -0
- frontend/public/textures/dirt_02.png +3 -0
- 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
|
frontend/public/images/wawasensei-white.png
ADDED
|
|
Git LFS Details
|
frontend/public/images/wawasensei.png
ADDED
|
|
Git LFS Details
|
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
|
frontend/public/textures/Rotated/flame_06_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/muzzle_01_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/muzzle_02_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/muzzle_03_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/muzzle_04_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/muzzle_05_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/spark_05_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/spark_06_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_01_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_02_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_03_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_04_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_05_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_06_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/Rotated/trace_07_rotated.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/circle_01.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/circle_02.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/circle_03.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/circle_04.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/circle_05.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/dirt_01.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/dirt_02.png
ADDED
|
|
Git LFS Details
|
frontend/public/textures/dirt_03.png
ADDED
|
|
Git LFS Details
|