File size: 4,528 Bytes
5364899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""
Custom handler pour Hugging Face Inference Endpoints — assistant vocal
WAGADU, synthèse wolof (bilalfaye/speecht5_tts-wolof).

À déployer : ce fichier doit être uploadé sous le nom EXACT "handler.py"
à la racine d'une copie du modèle sur votre compte Hugging Face (le
modèle original n'appartenant pas à Wagadu, on ne peut pas y ajouter de
fichier directement — voir les instructions de déploiement fournies par
Claude).

Reproduit exactement la logique déjà validée dans
backend/apps/communication/voice_assistant.py (même modèle, même
embedding de voix — speaker index 7306, "slt", voix féminine neutre) —
seule différence : tourne sur GPU (endpoint dédié) au lieu du CPU du
worker Render, ce qui doit réduire le temps de synthèse de 49-188s à
quelques secondes (décodeur autoregressif, largement accéléré par un
GPU par rapport à un seul cœur CPU).
"""

import base64
import io
from typing import Any, Dict


class EndpointHandler:
    def __init__(self, path: str = ""):
        import torch
        from transformers import SpeechT5ForTextToSpeech, SpeechT5HifiGan, SpeechT5Processor

        self._device = "cuda" if torch.cuda.is_available() else "cpu"

        self.processor = SpeechT5Processor.from_pretrained(path)
        self.model = SpeechT5ForTextToSpeech.from_pretrained(path).to(self._device)
        self.model.eval()
        self.vocoder = SpeechT5HifiGan.from_pretrained("microsoft/speecht5_hifigan").to(self._device)
        self.speaker_embedding = self._load_speaker_embedding().to(self._device)

    def _load_speaker_embedding(self):
        """Même méthode que _load_speaker_embedding() dans
        voice_assistant.py : contourne datasets.load_dataset (cassé pour
        ce dataset — format "script" non supporté par datasets>=4) en
        téléchargeant directement l'archive et en extrayant le fichier
        voulu, indexé exactement comme le script de chargement d'origine."""
        import zipfile

        import numpy as np
        import torch
        from huggingface_hub import hf_hub_download

        zip_path = hf_hub_download("Matthijs/cmu-arctic-xvectors", "spkrec-xvect.zip", repo_type="dataset")
        with zipfile.ZipFile(zip_path) as zf:
            names = sorted(n for n in zf.namelist() if n.endswith(".npy"))
            with zf.open(names[7306]) as f:
                return torch.tensor(np.load(f)).unsqueeze(0)

    def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
        import numpy as np
        import scipy.io.wavfile
        import torch

        text = (data.get("inputs") or "").strip()
        if not text:
            return {"error": "champ 'inputs' requis (texte wolof à synthétiser)"}

        inputs = self.processor(
            text=text,
            return_tensors="pt",
            padding=True,
            truncation=True,
            max_length=self.model.config.max_text_positions,
        )
        inputs = {k: v.to(self._device) for k, v in inputs.items()}

        # Réglages recommandés par l'auteur du modèle (fiche Hugging Face,
        # section "Model Loading and Speech Generation Code") : generate()
        # avec recherche en faisceau plutôt que generate_speech() (glouton,
        # utilisé côté Render CPU où la vitesse prime) — meilleure qualité,
        # mais nettement plus lent. Se justifie uniquement ici, sur GPU, où
        # on a la marge de calcul pour se le permettre.
        max_length = int(self.model.config.max_length * 1.2)
        with torch.no_grad():
            waveform = self.model.generate(
                inputs["input_ids"],
                speaker_embeddings=self.speaker_embedding,
                vocoder=self.vocoder,
                max_length=max_length,
                min_length=max_length // 3,
                num_beams=7,
                temperature=0.6,
                no_repeat_ngram_size=3,
                repetition_penalty=1.5,
                eos_token_id=None,
                use_cache=True,
            )
        waveform = waveform.detach().cpu().numpy()

        # PCM 16 bits — même format que voice_assistant.py côté Render,
        # pour que le reste du pipeline (Celery, polling, lecture client)
        # n'ait besoin d'aucun changement.
        pcm = np.clip(waveform, -1.0, 1.0)
        pcm = (pcm * 32767).astype(np.int16)

        buf = io.BytesIO()
        scipy.io.wavfile.write(buf, rate=16000, data=pcm)

        return {"audio_base64": base64.b64encode(buf.getvalue()).decode("ascii")}