mimba commited on
Commit
a847875
·
1 Parent(s): 7cf6220

Add application file

Browse files
Files changed (2) hide show
  1. README.md +32 -0
  2. app.py +112 -1
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, progress=gr.Progress()):
 
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