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()
|