File size: 6,576 Bytes
7eecd1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import os
import io
import re
import wave
import torch
import numpy as np
import tempfile
import sys
import supertonic

# Добавяме BgTTS към sys.path, за да може вътрешните му импорти да работят
sys.path.append(os.path.join(os.path.dirname(__file__), 'BgTTS'))
from inference import synthesize
from normalizer import normalize_text

class TTSEngine:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        print(f"Зареждам TTS Engine на устройство: {self.device}")
        
        # Supertonic (Референтно аудио)
        from supertonic import TTS
        self.engine = TTS(auto_download=True)
        
        # BgTTS (Основен модел)
        self.bgtts_checkpoint = os.path.join(os.path.dirname(__file__), "BgTTS", "checkpoint_inference.pt")
        # BgTTS inference.synthesize зарежда модела всеки път, ако не му подадем модела. 
        # В текущия BgTTS/inference.py synthesize() вика load_for_inference(), ако се подаде път.
        # За сега ще ползваме пътя, тъй като така е написан BgTTS. 
        # Ако искаме пълно кеширане, може да се наложи леко пренаписване на BgTTS/inference.py.
        # Но засега ще ползваме оригиналната synthesize функция.
        
        print("TTS Engine зареден успешно.")

    def split_text_for_tts(self, text: str) -> list[str]:
        text = text.strip()
        if not text:
            return []
        raw = re.split(r'(?<=[\.\!\?…])\s+|\n+', text)
        chunks = []
        buf = ""
        for part in raw:
            part = part.strip()
            if not part: continue
            
            if not buf or len(buf) < 80 or len(buf) + len(part) + 1 <= 200:
                buf = (buf + " " + part).strip()
            else:
                chunks.append(buf)
                buf = part
        if buf: chunks.append(buf)
        return chunks

    def generate_chunk(self, chunk_text: str, voice_style: str = "F5", speed: float = 1.6) -> bytes:
        """
        Генерира аудио за едно изречение (chunk) и го връща като WAV байтове.
        """
        clean_text = chunk_text.replace('"', '').replace('„', '').replace('“', '') \
                               .replace("’", "'").replace("–", "-").replace("—", "-") \
                               .replace("*", "")
                               
        if not clean_text.strip():
            return b""
            
        # 1. Генериране на референтно аудио
        # Ако voice_style е стринг (напр. "F5"), взимаме съответния обект
        if isinstance(voice_style, str):
            v_style = self.engine.get_voice_style(voice_name=voice_style)
        else:
            v_style = voice_style
            
        wav_array, _ = self.engine.synthesize(clean_text, voice_style=v_style, lang="bg", speed=speed)
        wav_data = np.asarray(wav_array).flatten()
        wav_max = np.max(np.abs(wav_data))
        if wav_max > 0:
            wav_data = wav_data / wav_max
        pcm_data = (wav_data * 32767).astype(np.int16)
        
        # Записваме временно референтното аудио (тъй като BgTTS изисква файл)
        fd, ref_path = tempfile.mkstemp(suffix=".wav")
        os.close(fd)
        with wave.open(ref_path, "wb") as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(44100)
            wf.writeframes(pcm_data.tobytes())
            
        # 2. Генериране на крайното аудио
        fd, final_path = tempfile.mkstemp(suffix=".wav")
        os.close(fd)
        
        try:
            synthesize(checkpoint=self.bgtts_checkpoint, 
                       text=clean_text, 
                       output=final_path, 
                       speaker_wav=ref_path,
                       device=self.device)
                       
            # Прочитане на резултата
            with open(final_path, "rb") as f:
                audio_bytes = f.read()
                
            return audio_bytes
            
        finally:
            try:
                os.remove(ref_path)
                os.remove(final_path)
            except OSError:
                pass

    def synthesize_stream(self, text: str, voice_style: str = "F5", speed: float = 1.6):
        """
        Генератор, който нормализира текста, цепи го на парчета и връща WAV байтове за всяко парче.
        """
        normalized_text = normalize_text(text)
        chunks = self.split_text_for_tts(normalized_text)
        
        for chunk in chunks:
            audio_bytes = self.generate_chunk(chunk, voice_style, speed)
            if audio_bytes:
                yield audio_bytes

    def synthesize_full(self, text: str, voice_style: str = "F5", speed: float = 1.6) -> bytes:
        """
        Нормализира текста, цепи го, генерира всички парчета и ги слепва в един общ WAV файл.
        """
        normalized_text = normalize_text(text)
        chunks = self.split_text_for_tts(normalized_text)
        
        all_frames = b""
        params = None
        
        for chunk in chunks:
            audio_bytes = self.generate_chunk(chunk, voice_style, speed)
            if not audio_bytes:
                continue
                
            # Парсване на WAV данните, за да можем да ги слеем без да дублираме хедъри
            with wave.open(io.BytesIO(audio_bytes), "rb") as wf:
                if not params:
                    params = wf.getparams()
                all_frames += wf.readframes(wf.getnframes())
                
        if not params:
            return b""
            
        # Създаване на крайния WAV
        out_io = io.BytesIO()
        with wave.open(out_io, "wb") as wf:
            wf.setparams(params)
            wf.writeframes(all_frames)
            
        return out_io.getvalue()

# Глобална инстанция за по-лесно преизползване
engine = TTSEngine()