Spaces:
Sleeping
Sleeping
Add application file
Browse files
README.md
CHANGED
|
@@ -87,6 +87,38 @@ Le Space fonctionne **sur CPU comme sur ZeroGPU**, sans modification :
|
|
| 87 |
démarrage échoue avec `No @spaces.GPU function detected during startup`. `app.py` décore
|
| 88 |
donc `synthese()` dès que le module `spaces` est disponible.
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
## Notes techniques
|
| 91 |
|
| 92 |
- **Gradio 6** (comme le Space officiel BlueTTS) : Gradio 4.x importait
|
|
|
|
| 87 |
démarrage échoue avec `No @spaces.GPU function detected during startup`. `app.py` décore
|
| 88 |
donc `synthese()` dès que le module `spaces` est disponible.
|
| 89 |
|
| 90 |
+
## Statistiques d'usage (facultatif)
|
| 91 |
+
|
| 92 |
+
Le disque d'un Space est **éphémère** : un fichier écrit localement disparaît au moindre
|
| 93 |
+
redémarrage. Les compteurs sont donc poussés dans un **dépôt HF (dataset), de préférence
|
| 94 |
+
privé**. Fonctionnalité **désactivée** tant que `STATS_REPO` n'est pas renseigné.
|
| 95 |
+
|
| 96 |
+
| Variable | Défaut | Rôle |
|
| 97 |
+
|---|---|---|
|
| 98 |
+
| `STATS_REPO` | *(vide)* | Dataset où écrire `stats.json`, ex. `mimba/malagasy-tts-stats`. Vide = désactivé. |
|
| 99 |
+
| `STATS_SALT` | `mimba-space` | Sel du hachage des empreintes visiteurs |
|
| 100 |
+
| `STATS_FLUSH_EVERY` | `10` | Écriture toutes les N synthèses |
|
| 101 |
+
| `STATS_FLUSH_SECONDS` | `600` | …ou au bout de N secondes |
|
| 102 |
+
|
| 103 |
+
> Le `HF_TOKEN` doit alors avoir le droit d'**écrire** sur ce dataset (et lui seul, en plus
|
| 104 |
+
> de la lecture du modèle).
|
| 105 |
+
|
| 106 |
+
Contenu de `stats.json` : total, compte par jour, compte par voix utilisée, et nombre de
|
| 107 |
+
**visiteurs distincts** (empreintes `sha256(sel + IP + user-agent)` tronquées, irréversibles).
|
| 108 |
+
|
| 109 |
+
**Jamais enregistré** : IP en clair, texte saisi, audio envoyé.
|
| 110 |
+
|
| 111 |
+
**Écriture par lots** : une écriture par synthèse dépasserait la limite HF de
|
| 112 |
+
**128 commits/heure**. Les compteurs sont donc accumulés en mémoire et poussés toutes les
|
| 113 |
+
`STATS_FLUSH_EVERY` synthèses (ou `STATS_FLUSH_SECONDS`). Conséquence assumée : les
|
| 114 |
+
dernières synthèses avant un redémarrage peuvent être perdues.
|
| 115 |
+
|
| 116 |
+
### Connaître l'identité des visiteurs
|
| 117 |
+
|
| 118 |
+
Impossible pour un visiteur anonyme. Il faut une **connexion HF explicite** : ajoute
|
| 119 |
+
`hf_oauth: true` dans l'en-tête YAML et un `gr.LoginButton()` dans l'interface — tu obtiens
|
| 120 |
+
alors le nom d'utilisateur de ceux qui acceptent de se connecter, et uniquement d'eux.
|
| 121 |
+
|
| 122 |
## Notes techniques
|
| 123 |
|
| 124 |
- **Gradio 6** (comme le Space officiel BlueTTS) : Gradio 4.x importait
|
app.py
CHANGED
|
@@ -63,6 +63,15 @@ LATENT_DIM = int(_env("BLUETTS_LATENT_DIM", "24"))
|
|
| 63 |
SAMPLE_RATE = int(_env("BLUETTS_SAMPLE_RATE", "44100"))
|
| 64 |
MAX_CHARS = int(_env("MAX_CHARS", "400"))
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
# Voix de demonstration embarquees : le visiteur peut essayer sans fournir d'extrait.
|
| 67 |
# Ce sont de VRAIES voix malgaches natives (les memes que celles du bootstrap Mimba).
|
| 68 |
_DOSSIER_VOIX = os.path.join(os.path.dirname(__file__), "voix_demo")
|
|
@@ -88,6 +97,104 @@ _char_to_id, _pad_id = None, 0
|
|
| 88 |
_ref_encoder = None
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# ============================================================
|
| 92 |
# Chargement du modèle (dépôt privé -> disque temporaire)
|
| 93 |
# ============================================================
|
|
@@ -141,6 +248,7 @@ def charger():
|
|
| 141 |
_ref_encoder = EncodeurReference(dossier, sample_rate=SAMPLE_RATE,
|
| 142 |
chunk_compress_factor=CHUNK_COMPRESS_FACTOR)
|
| 143 |
print(f"[space] modele charge ({len(_char_to_id)} phonemes)", flush=True)
|
|
|
|
| 144 |
|
| 145 |
|
| 146 |
# ============================================================
|
|
@@ -239,11 +347,13 @@ def apercu_voix_demo(voix_demo):
|
|
| 239 |
|
| 240 |
|
| 241 |
@zero_gpu
|
| 242 |
-
def synthese(texte, audio_ref, voix_demo, total_step, speed, seed,
|
|
|
|
| 243 |
if not texte or not texte.strip():
|
| 244 |
raise gr.Error("Saisissez un texte en malgache.")
|
| 245 |
if len(texte) > MAX_CHARS:
|
| 246 |
raise gr.Error(f"Texte trop long ({len(texte)} caractères, maximum {MAX_CHARS}).")
|
|
|
|
| 247 |
audio_ref = _resoudre_reference(audio_ref, voix_demo)
|
| 248 |
|
| 249 |
progress(0.15, desc="Phonémisation…")
|
|
@@ -259,6 +369,7 @@ def synthese(texte, audio_ref, voix_demo, total_step, speed, seed, progress=gr.P
|
|
| 259 |
|
| 260 |
sortie = os.path.join(tempfile.mkdtemp(), "mimba_bluetts.wav")
|
| 261 |
sf.write(sortie, wav, SAMPLE_RATE)
|
|
|
|
| 262 |
duree = len(wav) / SAMPLE_RATE
|
| 263 |
return sortie, f"{duree:.2f} s · {len(phonemes)} phonèmes · {int(total_step)} pas"
|
| 264 |
|
|
|
|
| 63 |
SAMPLE_RATE = int(_env("BLUETTS_SAMPLE_RATE", "44100"))
|
| 64 |
MAX_CHARS = int(_env("MAX_CHARS", "400"))
|
| 65 |
|
| 66 |
+
# --- Statistiques d'usage (facultatif) ---
|
| 67 |
+
# Le disque d'un Space est EPHEMERE : tout fichier ecrit est perdu au redemarrage.
|
| 68 |
+
# Les compteurs sont donc pousses dans un depot HF (dataset), de preference PRIVE.
|
| 69 |
+
# Laisser STATS_REPO vide desactive completement la fonctionnalite.
|
| 70 |
+
STATS_REPO = _env("STATS_REPO") # ex. "mimba/malagasy-tts-stats"
|
| 71 |
+
STATS_SALT = _env("STATS_SALT", "mimba-space") # sel du hachage des IP
|
| 72 |
+
STATS_FLUSH_EVERY = int(_env("STATS_FLUSH_EVERY", "10")) # ecriture tous les N usages
|
| 73 |
+
STATS_FLUSH_SECONDS = int(_env("STATS_FLUSH_SECONDS", "600"))
|
| 74 |
+
|
| 75 |
# Voix de demonstration embarquees : le visiteur peut essayer sans fournir d'extrait.
|
| 76 |
# Ce sont de VRAIES voix malgaches natives (les memes que celles du bootstrap Mimba).
|
| 77 |
_DOSSIER_VOIX = os.path.join(os.path.dirname(__file__), "voix_demo")
|
|
|
|
| 97 |
_ref_encoder = None
|
| 98 |
|
| 99 |
|
| 100 |
+
# ============================================================
|
| 101 |
+
# Statistiques d'usage — RGPD : aucune donnée directement identifiante
|
| 102 |
+
# ============================================================
|
| 103 |
+
# Ce qui est conserve : compteurs par jour, nombre de visiteurs DISTINCTS (empreintes
|
| 104 |
+
# hachees + salees, irreversibles), voix utilisee, pays si l'hebergeur le fournit.
|
| 105 |
+
# Ce qui n'est JAMAIS conserve : IP en clair, texte saisi, audio envoye.
|
| 106 |
+
_stats = {"total": 0, "par_jour": {}, "par_voix": {}, "empreintes": [], "maj": None}
|
| 107 |
+
_stats_depuis_flush = 0
|
| 108 |
+
_stats_dernier_flush = 0.0
|
| 109 |
+
FICHIER_STATS = "stats.json"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _empreinte(requete):
|
| 113 |
+
"""Empreinte anonyme et stable d'un visiteur (IP + navigateur, hachees + salees).
|
| 114 |
+
Irreversible : sert uniquement a compter des visiteurs distincts."""
|
| 115 |
+
import hashlib
|
| 116 |
+
ip = navigateur = ""
|
| 117 |
+
try:
|
| 118 |
+
entetes = dict(getattr(requete, "headers", {}) or {})
|
| 119 |
+
ip = (entetes.get("x-forwarded-for") or "").split(",")[0].strip()
|
| 120 |
+
navigateur = entetes.get("user-agent", "")
|
| 121 |
+
except Exception:
|
| 122 |
+
pass
|
| 123 |
+
if not ip and not navigateur:
|
| 124 |
+
return None
|
| 125 |
+
brut = f"{STATS_SALT}|{ip}|{navigateur}"
|
| 126 |
+
return hashlib.sha256(brut.encode("utf-8")).hexdigest()[:16]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _charger_stats():
|
| 130 |
+
"""Recupere les compteurs existants : le disque du Space etant efface a chaque
|
| 131 |
+
redemarrage, l'historique ne survit que dans le depot HF."""
|
| 132 |
+
global _stats
|
| 133 |
+
if not STATS_REPO:
|
| 134 |
+
return
|
| 135 |
+
try:
|
| 136 |
+
import json
|
| 137 |
+
from huggingface_hub import hf_hub_download
|
| 138 |
+
chemin = hf_hub_download(STATS_REPO, FICHIER_STATS, repo_type="dataset",
|
| 139 |
+
token=HF_TOKEN)
|
| 140 |
+
with open(chemin, encoding="utf-8") as f:
|
| 141 |
+
precedent = json.load(f)
|
| 142 |
+
for cle in ("total", "par_jour", "par_voix", "empreintes"):
|
| 143 |
+
if cle in precedent:
|
| 144 |
+
_stats[cle] = precedent[cle]
|
| 145 |
+
print(f"[stats] historique repris : {_stats['total']} usages, "
|
| 146 |
+
f"{len(_stats['empreintes'])} visiteurs distincts", flush=True)
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print(f"[stats] pas d'historique ({type(e).__name__}) -- demarrage a zero", flush=True)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _pousser_stats():
|
| 152 |
+
global _stats_depuis_flush, _stats_dernier_flush
|
| 153 |
+
if not STATS_REPO:
|
| 154 |
+
return
|
| 155 |
+
try:
|
| 156 |
+
import json
|
| 157 |
+
import time as _t
|
| 158 |
+
from huggingface_hub import HfApi
|
| 159 |
+
_stats["maj"] = _t.strftime("%Y-%m-%dT%H:%M:%SZ", _t.gmtime())
|
| 160 |
+
local = os.path.join(tempfile.gettempdir(), FICHIER_STATS)
|
| 161 |
+
with open(local, "w", encoding="utf-8") as f:
|
| 162 |
+
json.dump(_stats, f, ensure_ascii=False, indent=2)
|
| 163 |
+
HfApi().upload_file(
|
| 164 |
+
path_or_fileobj=local, path_in_repo=FICHIER_STATS, repo_id=STATS_REPO,
|
| 165 |
+
repo_type="dataset", token=HF_TOKEN,
|
| 166 |
+
commit_message=f"{_stats['total']} usages / {len(_stats['empreintes'])} visiteurs")
|
| 167 |
+
_stats_depuis_flush = 0
|
| 168 |
+
_stats_dernier_flush = _t.time()
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f"[stats] ecriture impossible : {repr(e)[:120]}", flush=True)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _enregistrer_usage(requete, nom_voix, propre_extrait):
|
| 174 |
+
"""Incremente les compteurs, et n'ecrit sur HF que par LOTS : une ecriture par
|
| 175 |
+
synthese depasserait vite la limite HF de 128 commits/heure."""
|
| 176 |
+
global _stats_depuis_flush
|
| 177 |
+
if not STATS_REPO:
|
| 178 |
+
return
|
| 179 |
+
try:
|
| 180 |
+
import time as _t
|
| 181 |
+
jour = _t.strftime("%Y-%m-%d", _t.gmtime())
|
| 182 |
+
_stats["total"] += 1
|
| 183 |
+
_stats["par_jour"][jour] = _stats["par_jour"].get(jour, 0) + 1
|
| 184 |
+
cle_voix = "voix fournie par le visiteur" if propre_extrait else (nom_voix or "?")
|
| 185 |
+
_stats["par_voix"][cle_voix] = _stats["par_voix"].get(cle_voix, 0) + 1
|
| 186 |
+
emp = _empreinte(requete)
|
| 187 |
+
if emp and emp not in _stats["empreintes"]:
|
| 188 |
+
_stats["empreintes"].append(emp)
|
| 189 |
+
_stats_depuis_flush += 1
|
| 190 |
+
assez_d_usages = _stats_depuis_flush >= STATS_FLUSH_EVERY
|
| 191 |
+
assez_de_temps = (_t.time() - _stats_dernier_flush) >= STATS_FLUSH_SECONDS
|
| 192 |
+
if assez_d_usages or assez_de_temps:
|
| 193 |
+
_pousser_stats()
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"[stats] non enregistre : {repr(e)[:120]}", flush=True)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
# ============================================================
|
| 199 |
# Chargement du modèle (dépôt privé -> disque temporaire)
|
| 200 |
# ============================================================
|
|
|
|
| 248 |
_ref_encoder = EncodeurReference(dossier, sample_rate=SAMPLE_RATE,
|
| 249 |
chunk_compress_factor=CHUNK_COMPRESS_FACTOR)
|
| 250 |
print(f"[space] modele charge ({len(_char_to_id)} phonemes)", flush=True)
|
| 251 |
+
_charger_stats()
|
| 252 |
|
| 253 |
|
| 254 |
# ============================================================
|
|
|
|
| 347 |
|
| 348 |
|
| 349 |
@zero_gpu
|
| 350 |
+
def synthese(texte, audio_ref, voix_demo, total_step, speed, seed,
|
| 351 |
+
progress=gr.Progress(), request: gr.Request = None):
|
| 352 |
if not texte or not texte.strip():
|
| 353 |
raise gr.Error("Saisissez un texte en malgache.")
|
| 354 |
if len(texte) > MAX_CHARS:
|
| 355 |
raise gr.Error(f"Texte trop long ({len(texte)} caractères, maximum {MAX_CHARS}).")
|
| 356 |
+
audio_fourni = bool(audio_ref) # distingue extrait du visiteur / voix fournie
|
| 357 |
audio_ref = _resoudre_reference(audio_ref, voix_demo)
|
| 358 |
|
| 359 |
progress(0.15, desc="Phonémisation…")
|
|
|
|
| 369 |
|
| 370 |
sortie = os.path.join(tempfile.mkdtemp(), "mimba_bluetts.wav")
|
| 371 |
sf.write(sortie, wav, SAMPLE_RATE)
|
| 372 |
+
_enregistrer_usage(request, voix_demo, propre_extrait=bool(audio_fourni))
|
| 373 |
duree = len(wav) / SAMPLE_RATE
|
| 374 |
return sortie, f"{duree:.2f} s · {len(phonemes)} phonèmes · {int(total_step)} pas"
|
| 375 |
|