Belemort commited on
Commit
bdb5570
·
verified ·
1 Parent(s): 5a05e6b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -302
app.py CHANGED
@@ -1,303 +1,303 @@
1
- import os
2
- import io
3
- import gigaam
4
- import gradio as gr
5
- from mistralai import Mistral
6
- from pydub import AudioSegment
7
- import markdown2
8
- from xhtml2pdf import pisa
9
- import torch
10
- import json
11
- import numpy as np
12
- import tempfile
13
-
14
-
15
- def load_tts_model():
16
- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
17
- model = torch.package.PackageImporter("v4_ru.pt").load_pickle("tts_models", "model")
18
- model.to(device)
19
- return model, device
20
-
21
- tts_model, tts_device = load_tts_model()
22
-
23
- # === Функция генерации озвучки ===
24
- def synthesize_ssml_parts(parts, speaker="baya", sample_rate=48000):
25
- audio_segments = []
26
- for part in parts:
27
- if isinstance(part, dict):
28
- text_ssml = part.get("part", "")
29
- else:
30
- text_ssml = part
31
-
32
-
33
- # генерируем аудио
34
- audio_tensor = tts_model.apply_tts(
35
- text=text_ssml,
36
- speaker=speaker, # можно поменять: aidar, baya, kseniya, xenia, eugene
37
- sample_rate=48000,
38
- put_accent=True,
39
- put_yo=True,
40
- )
41
-
42
- # конвертируем в numpy float32
43
- audio_np = audio_tensor.cpu().numpy()
44
-
45
- # создаём AudioSegment
46
- segment = AudioSegment(
47
- (audio_np * 32767).astype(np.int16).tobytes(),
48
- frame_rate=48000,
49
- sample_width=2,
50
- channels=1
51
- )
52
- audio_segments.append(segment)
53
-
54
- # объединяем все сегменты
55
- combined = sum(audio_segments)
56
-
57
- # сохраняем в BytesIO
58
- buffer = io.BytesIO()
59
- combined.export(buffer, format="wav")
60
- buffer.seek(0)
61
- return buffer
62
-
63
-
64
- # === Определение длительности аудио ===
65
- def get_audio_duration(file_path: str) -> float:
66
- audio = AudioSegment.from_file(file_path)
67
- return audio.duration_seconds
68
-
69
-
70
- # === Транскрибация с прогресс-баром ===
71
- def transcribe_audio(audio_file: str, progress_bar) -> str:
72
- os.environ["HF_TOKEN"] = os.getenv("HF_TOKEN")
73
- model = gigaam.load_model("v2_rnnt")
74
-
75
- total_duration = get_audio_duration(audio_file)
76
- recognition_result = model.transcribe_longform(audio_file)
77
-
78
- all_text = []
79
- last_progress = 0
80
-
81
- for utterance in recognition_result:
82
- transcription = utterance["transcription"]
83
- start, end = utterance["boundaries"]
84
-
85
- all_text.append(f"[{gigaam.format_time(start)} - {gigaam.format_time(end)}]: {transcription}")
86
-
87
- # обновляем прогресс
88
- current_progress = int((end / total_duration) * 100 * 0.9)
89
- if current_progress > last_progress:
90
- progress_bar.progress(current_progress, text="⏳ Транскрибируем аудио...")
91
- last_progress = current_progress
92
-
93
- return "\n".join(all_text)
94
-
95
-
96
- # === Генерация PDF из Markdown ===
97
- def create_pdf_abstract(markdown_text: str) -> bytes:
98
- html = markdown2.markdown(markdown_text)
99
-
100
- buffer = io.BytesIO()
101
- pisa.CreatePDF(io.StringIO(html), dest=buffer)
102
- buffer.seek(0)
103
- return buffer.read()
104
-
105
-
106
- # === Суммаризация ===
107
- def summarize_text(text: str, style: str, length: str) -> str:
108
- MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
109
- client = Mistral(api_key=MISTRAL_API_KEY)
110
-
111
- prompt = f"""
112
- Ты — умный помощник.
113
- Сделай {length} {style} конспект по этому тексту (на русском языке):
114
-
115
- {text}
116
- """
117
-
118
- response = client.chat.complete(
119
- model="mistral-large-latest",
120
- messages=[
121
- {"role": "system", "content": "Ты создаёшь структурированные конспекты в формате Markdown."},
122
- {"role": "user", "content": prompt},
123
- ],
124
- )
125
- return response.choices[0].message.content
126
-
127
-
128
- def convert_summarize_text_with_ssml(text: str, style: str, length: str) -> list:
129
- MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
130
- client = Mistral(api_key=MISTRAL_API_KEY)
131
-
132
- prompt = f"""
133
- Ты — умный помощник.
134
- Разбей текст на части, где каждая часть не больше 1000 символов.
135
- Каждую часть оберни в SSML тег <speak>.
136
- - Оберни текст в тег <speak> ... </speak>.
137
- - **Знаки препинания не должны произноситься словами.** Вместо этого:
138
- - запятая → вставь `<break time="220ms"/>` в месте запятой;
139
- - точка / конец предложения → вставь `<break time="450ms"/>` после предложения;
140
- - двоеточие / точка с запятой → `<break time="350ms"/>`;
141
- - длинная пауза / переход к новому абзацу → `<break time="700ms"/>`.
142
- - **Вопросительные предложения**: в конце вопроса НЕ вставляй слово «вопросительный знак». Вместо этого оберни заключительную часть вопроса в `<prosody pitch="+12%" rate="95%">...</prosody>` чтобы задать подъём интонации, и затем `<break time="450ms"/>`.
143
- - **Восклицательные предложения**: выдели ключевую фразу с помощью `<emphasis level="strong">...</emphasis>` и / или `<prosody pitch="+15%" rate="105%">...</prosody>`, затем `<break time="450ms"/>`.
144
- - **Кавычки / прямые речи**: при открытии цитаты добавь небольшую паузу `<break time="200ms"/>`, затем внутри цитаты можно использовать `<emphasis level="moderate">` или слегка поднять `pitch` для выразительности, после цитаты — пауза `<break time="300ms"/>`. Не произноси слово «кавычки».
145
- - **Числа**: записывай цифры буквенно; если невозможно — используй `<say-as interpret-as="cardinal">...</say-as>` для чисел (но приоритет — слова).
146
- - **Не вставляй** никаких дополнительных SSML-тегов, которые могут быть не поддержаны (например, vendor-specific `<amazon:...>`). Используй только: `<speak>`, `<break>`, `<prosody>`, `<emphasis>`, `<say-as>`.
147
- Цифры запиши буквенно.
148
- Пеши без сокращений слов(не г. а год/года)
149
- Верни результат в JSON формате: список объектов с полем 'part'.
150
- Пример:
151
- [
152
- {{"part": "<speak>Текст части 1...</speak>"}},
153
- {{"part": "<speak>Текст части 2...</speak>"}}
154
- ]
155
- Только JSON, никаких объяснений.
156
- {text}
157
-
158
- RETURN ONLY JSON
159
- """
160
-
161
- response = client.chat.complete(
162
- model="mistral-large-latest",
163
- messages=[
164
- {"role": "system", "content": "Ты создаёшь структурированные конспекты для TTS с SSML."},
165
- {"role": "user", "content": f"{prompt}"},
166
- ],
167
- response_format={"type": "json_object"}
168
- )
169
-
170
- print(response.choices[0].message.content)
171
- # Получаем JSON
172
- json_text = response.choices[0].message.content
173
- return json.loads(json_text)
174
-
175
- # Небольшая "заглушка" прогресс-бара, чтобы можно было вызвать transcribe_audio (он ожидает progress_bar)
176
- class DummyProgress:
177
- def progress(self, *args, **kwargs):
178
- return None
179
-
180
- progress_dummy = DummyProgress()
181
-
182
- # Обёртки (НЕ меняют логику твоих функций)
183
- def transcribe_wrapper(audio_filepath):
184
- if audio_filepath is None or audio_filepath == "":
185
- return ""
186
- # Gradio даёт путь к временному файлу — передаём напрямую в твою функцию
187
- try:
188
- return transcribe_audio(audio_filepath, progress_dummy)
189
- except Exception as e:
190
- return f"Transcription error: {e}"
191
-
192
- def summarize_wrapper(audio_filepath, style, compression):
193
- # получаем транскрипт (если пользователь подаёт текст в дальнейшем — можно расширить)
194
- transcript = transcribe_wrapper(audio_filepath)
195
- if transcript.startswith("Transcription error"):
196
- return transcript
197
- try:
198
- summary = summarize_text(transcript, style, compression)
199
- return summary
200
- except Exception as e:
201
- return f"Summarization error: {e}"
202
-
203
- def pdf_wrapper(markdown_text):
204
- try:
205
- pdf_bytes = create_pdf_abstract(markdown_text)
206
- # Gradio принимает bytes для File/Download
207
- return ("abstract.pdf", pdf_bytes)
208
- except Exception as e:
209
- return None
210
-
211
- def ssml_and_tts_wrapper(markdown_text, style, compression, speaker):
212
- try:
213
- # Получаем части с SSML (твоя функция)
214
- parts = convert_summarize_text_with_ssml(markdown_text, style, compression)
215
- # Генерируем аудио (твоя функция возвращает BytesIO)
216
- audio_buffer = synthesize_ssml_parts(parts, speaker)
217
- audio_buffer.seek(0)
218
- # Для gr.Audio можно возвращать bytes либо путь к файлу.
219
- return audio_buffer.read()
220
- except Exception as e:
221
- # В случае ошибки возвращаем строку с сообщением (Gradio пока��ет)
222
- return f"TTS error: {e}"
223
-
224
- # Запускаем интерфейс Gradio
225
- with gr.Blocks() as demo:
226
- gr.Markdown("# 🎙️ Аудио-конспекты (Gradio)")
227
-
228
- with gr.Row():
229
- with gr.Column(scale=1):
230
- gr.Markdown("## 1) Загрузка аудио и создание конспекта")
231
- upload = gr.Audio(label="Загрузите аудио (mp3/wav)", type="filepath")
232
- style = gr.Dropdown(["структурированный", "в виде списка", "подробный", "короткий"], value="структурированный", label="Стиль конспекта")
233
- compression = gr.Slider(0, 100, 50, label="Уровень сжатия (0 — подробно, 100 — кратко)")
234
- btn_summarize = gr.Button("✨ Сделать конспект")
235
- transcript_out = gr.Textbox(label="Транскрипт (результат распознавания)", lines=6)
236
- summary_md = gr.Textbox(label="Конспект (Markdown)", lines=15)
237
-
238
- # Кнопка создаёт транскрипт и конспект
239
- def on_summarize(audio_fp, stl, cmp):
240
- tr = transcribe_wrapper(audio_fp)
241
- if tr.startswith("Transcription error"):
242
- return tr, ""
243
- summary = summarize_text(tr, stl, cmp)
244
- return tr, summary
245
-
246
- btn_summarize.click(
247
- fn=on_summarize,
248
- inputs=[upload, style, compression],
249
- outputs=[transcript_out, summary_md],
250
- )
251
-
252
- with gr.Column(scale=1):
253
- gr.Markdown("## 2) Озвучка (SSML → TTS)")
254
- speaker = gr.Dropdown(["aidar", "baya", "kseniya", "xenia", "eugene"], value="baya", label="Выберите голос")
255
- btn_tts = gr.Button("🔊 Сгенерировать озвучку")
256
- audio_out = gr.Audio(label="Озвучка (WAV)", type="numpy")
257
- tts_file = gr.File(label="Скачать WAV")
258
-
259
- def on_tts(markdown_text, stl, cmp, sp):
260
- """
261
- Возвращает:
262
- - путь (str) для gr.Audio (type="filepath")
263
- - путь (str) для gr.File
264
- В случае ошибки возвращает (None, None).
265
- """
266
- try:
267
- # получаем SSML-части (твоя функция)
268
- parts = convert_summarize_text_with_ssml(markdown_text, stl, cmp)
269
-
270
- # генерируем BytesIO с wav (твоя функция)
271
- buf = synthesize_ssml_parts(parts, sp)
272
- buf.seek(0)
273
- data = buf.read()
274
- if not data:
275
- print("on_tts: audio buffer is empty")
276
- return None, None
277
-
278
- # записываем во временный файл и возвращаем путь
279
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
280
- try:
281
- tmp.write(data)
282
- tmp.flush()
283
- finally:
284
- tmp.close()
285
-
286
- # Опционально: можно вернуть имя файла без пути для отображения,
287
- # но Gradio требует полный путь чтобы прочитать файл, поэтому возвращаем tmp.name
288
- return tmp.name, tmp.name
289
-
290
- except Exception as e:
291
- # полезный лог в консоль для отладки
292
- print("TTS generation error:", repr(e))
293
- return None, None
294
-
295
- # Подключаем обработчик к кнопке
296
- btn_tts.click(
297
- fn=on_tts,
298
- inputs=[summary_md, style, compression, speaker],
299
- outputs=[audio_out, tts_file],
300
- )
301
-
302
-
303
  demo.launch()
 
1
+ import os
2
+ import io
3
+ import gigaam
4
+ import gradio as gr
5
+ from mistralai import Mistral
6
+ from pydub import AudioSegment
7
+ import markdown2
8
+ from xhtml2pdf import pisa
9
+ import torch
10
+ import json
11
+ import numpy as np
12
+ import tempfile
13
+
14
+
15
+ def load_tts_model():
16
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
17
+ model = torch.package.PackageImporter("v4_ru.pt").load_pickle("tts_models", "model")
18
+ model.to(device)
19
+ return model, device
20
+
21
+ tts_model, tts_device = load_tts_model()
22
+
23
+ # === Функция генерации озвучки ===
24
+ def synthesize_ssml_parts(parts, speaker="baya", sample_rate=48000):
25
+ audio_segments = []
26
+ for part in parts:
27
+ if isinstance(part, dict):
28
+ text_ssml = part.get("part", "")
29
+ else:
30
+ text_ssml = part
31
+
32
+
33
+ # генерируем аудио
34
+ audio_tensor = tts_model.apply_tts(
35
+ text=text_ssml,
36
+ speaker=speaker, # можно поменять: aidar, baya, kseniya, xenia, eugene
37
+ sample_rate=48000,
38
+ put_accent=True,
39
+ put_yo=True,
40
+ )
41
+
42
+ # конвертируем в numpy float32
43
+ audio_np = audio_tensor.cpu().numpy()
44
+
45
+ # создаём AudioSegment
46
+ segment = AudioSegment(
47
+ (audio_np * 32767).astype(np.int16).tobytes(),
48
+ frame_rate=48000,
49
+ sample_width=2,
50
+ channels=1
51
+ )
52
+ audio_segments.append(segment)
53
+
54
+ # объединяем все сегменты
55
+ combined = sum(audio_segments)
56
+
57
+ # сохраняем в BytesIO
58
+ buffer = io.BytesIO()
59
+ combined.export(buffer, format="wav")
60
+ buffer.seek(0)
61
+ return buffer
62
+
63
+
64
+ # === Определение длительности аудио ===
65
+ def get_audio_duration(file_path: str) -> float:
66
+ audio = AudioSegment.from_file(file_path)
67
+ return audio.duration_seconds
68
+
69
+
70
+ # === Транскрибация с прогресс-баром ===
71
+ def transcribe_audio(audio_file: str, progress_bar) -> str:
72
+ os.environ["HF_TOKEN"] = os.getenv("HF_TOKEN")
73
+ model = gigaam.load_model("v2_rnnt")
74
+
75
+ total_duration = get_audio_duration(audio_file)
76
+ recognition_result = model.transcribe_longform(audio_file)
77
+
78
+ all_text = []
79
+ last_progress = 0
80
+
81
+ for utterance in recognition_result:
82
+ transcription = utterance["transcription"]
83
+ start, end = utterance["boundaries"]
84
+
85
+ all_text.append(f"[{gigaam.format_time(start)} - {gigaam.format_time(end)}]: {transcription}")
86
+
87
+ # обновляем прогресс
88
+ current_progress = int((end / total_duration) * 100 * 0.9)
89
+ if current_progress > last_progress:
90
+ progress_bar.progress(current_progress, text="⏳ Транскрибируем аудио...")
91
+ last_progress = current_progress
92
+
93
+ return "\n".join(all_text)
94
+
95
+
96
+ # === Генерация PDF из Markdown ===
97
+ def create_pdf_abstract(markdown_text: str) -> bytes:
98
+ html = markdown2.markdown(markdown_text)
99
+
100
+ buffer = io.BytesIO()
101
+ pisa.CreatePDF(io.StringIO(html), dest=buffer)
102
+ buffer.seek(0)
103
+ return buffer.read()
104
+
105
+
106
+ # === Суммаризация ===
107
+ def summarize_text(text: str, style: str, length: str) -> str:
108
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
109
+ client = Mistral(api_key=MISTRAL_API_KEY)
110
+
111
+ prompt = f"""
112
+ Ты — умный помощник.
113
+ Сделай {length} {style} конспект по этому тексту (на русском языке):
114
+
115
+ {text}
116
+ """
117
+
118
+ response = client.chat.complete(
119
+ model="mistral-large-latest",
120
+ messages=[
121
+ {"role": "system", "content": "Ты создаёшь структурированные конспекты в формате Markdown."},
122
+ {"role": "user", "content": prompt},
123
+ ],
124
+ )
125
+ return response.choices[0].message.content
126
+
127
+
128
+ def convert_summarize_text_with_ssml(text: str, style: str, length: str) -> list:
129
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
130
+ client = Mistral(api_key=MISTRAL_API_KEY)
131
+
132
+ prompt = f"""
133
+ Ты — умный помощник.
134
+ Разбей текст на части, где каждая часть не больше 1000 символов.
135
+ Каждую часть оберни в SSML тег <speak>.
136
+ - Оберни текст в тег <speak> ... </speak>.
137
+ - **Знаки препинания не должны произноситься словами.** Вместо этого:
138
+ - запятая → вставь `<break time="220ms"/>` в месте запятой;
139
+ - точка / конец предложения → вставь `<break time="450ms"/>` после предложения;
140
+ - двоеточие / точка с запятой → `<break time="350ms"/>`;
141
+ - длинная пауза / переход к новому абзацу → `<break time="700ms"/>`.
142
+ - **Вопросительные предложения**: в конце вопроса НЕ вставляй слово «вопросительный знак». Вместо этого оберни заключительную часть вопроса в `<prosody pitch="+12%" rate="95%">...</prosody>` чтобы задать подъём интонации, и затем `<break time="450ms"/>`.
143
+ - **Восклицательные предложения**: выдели ключевую фразу с помощью `<emphasis level="strong">...</emphasis>` и / или `<prosody pitch="+15%" rate="105%">...</prosody>`, затем `<break time="450ms"/>`.
144
+ - **Кавычки / прямые речи**: при открытии цитаты добавь небольшую паузу `<break time="200ms"/>`, затем внутри цитаты можно использовать `<emphasis level="moderate">` или слегка поднять `pitch` для выразительности, после цитаты — пауза `<break time="300ms"/>`. Не произноси слово «кавычки».
145
+ - **Числа**: записывай цифры буквенно; если невозможно — используй `<say-as interpret-as="cardinal">...</say-as>` для чисел (но приоритет — слова).
146
+ - **Не вставляй** никаких дополнительных SSML-тегов, которые могут быть не поддержаны (например, vendor-specific `<amazon:...>`). Используй только: `<speak>`, `<break>`, `<prosody>`, `<emphasis>`, `<say-as>`.
147
+ Цифры запиши буквенно.
148
+ Пеши без сокращений слов(не г. а год/года)
149
+ Верни результат в JSON формате: список объектов с полем 'part'.
150
+ Пример:
151
+ [
152
+ {{"part": "<speak>Текст части 1...</speak>"}},
153
+ {{"part": "<speak>Текст части 2...</speak>"}}
154
+ ]
155
+ Только JSON, никаких объяснений.
156
+ {text}
157
+
158
+ RETURN ONLY JSON
159
+ """
160
+
161
+ response = client.chat.complete(
162
+ model="pixtral-12b-2409",
163
+ messages=[
164
+ {"role": "system", "content": "Ты создаёшь структурированные конспекты для TTS с SSML."},
165
+ {"role": "user", "content": f"{prompt}"},
166
+ ],
167
+ response_format={"type": "json_object"}
168
+ )
169
+
170
+ print(response.choices[0].message.content)
171
+ # Получаем JSON
172
+ json_text = response.choices[0].message.content
173
+ return json.loads(json_text)
174
+
175
+ # Небольшая "заглушка" прогресс-бара, чтобы можно было вызвать transcribe_audio (он ожидает progress_bar)
176
+ class DummyProgress:
177
+ def progress(self, *args, **kwargs):
178
+ return None
179
+
180
+ progress_dummy = DummyProgress()
181
+
182
+ # Обёртки (НЕ меняют логику твоих функций)
183
+ def transcribe_wrapper(audio_filepath):
184
+ if audio_filepath is None or audio_filepath == "":
185
+ return ""
186
+ # Gradio даёт путь к временному файлу — передаём напрямую в твою функцию
187
+ try:
188
+ return transcribe_audio(audio_filepath, progress_dummy)
189
+ except Exception as e:
190
+ return f"Transcription error: {e}"
191
+
192
+ def summarize_wrapper(audio_filepath, style, compression):
193
+ # получаем транскрипт (если пользователь подаёт текст в дальнейшем — можно расширить)
194
+ transcript = transcribe_wrapper(audio_filepath)
195
+ if transcript.startswith("Transcription error"):
196
+ return transcript
197
+ try:
198
+ summary = summarize_text(transcript, style, compression)
199
+ return summary
200
+ except Exception as e:
201
+ return f"Summarization error: {e}"
202
+
203
+ def pdf_wrapper(markdown_text):
204
+ try:
205
+ pdf_bytes = create_pdf_abstract(markdown_text)
206
+ # Gradio принимает bytes для File/Download
207
+ return ("abstract.pdf", pdf_bytes)
208
+ except Exception as e:
209
+ return None
210
+
211
+ def ssml_and_tts_wrapper(markdown_text, style, compression, speaker):
212
+ try:
213
+ # Получаем части с SSML (твоя функция)
214
+ parts = convert_summarize_text_with_ssml(markdown_text, style, compression)
215
+ # Генерируем аудио (твоя функция возвращает BytesIO)
216
+ audio_buffer = synthesize_ssml_parts(parts, speaker)
217
+ audio_buffer.seek(0)
218
+ # Для gr.Audio можно возвращать bytes либо путь к файлу.
219
+ return audio_buffer.read()
220
+ except Exception as e:
221
+ # В случае ошибки возвращаем строку с сообщением (Gradio покажет)
222
+ return f"TTS error: {e}"
223
+
224
+ # Запускаем интерфейс Gradio
225
+ with gr.Blocks() as demo:
226
+ gr.Markdown("# 🎙️ Аудио-конспекты (Gradio)")
227
+
228
+ with gr.Row():
229
+ with gr.Column(scale=1):
230
+ gr.Markdown("## 1) Загрузка аудио и создание конспекта")
231
+ upload = gr.Audio(label="Загрузите аудио (mp3/wav)", type="filepath")
232
+ style = gr.Dropdown(["структурированный", "в виде списка", "подробный", "короткий"], value="структурированный", label="Стиль конспекта")
233
+ compression = gr.Slider(0, 100, 50, label="Уровень сжатия (0 — подробно, 100 — кратко)")
234
+ btn_summarize = gr.Button("✨ Сделать конспект")
235
+ transcript_out = gr.Textbox(label="Транскрипт (результат распознавания)", lines=6)
236
+ summary_md = gr.Textbox(label="Конспект (Markdown)", lines=15)
237
+
238
+ # Кнопка создаёт транскрипт и конспект
239
+ def on_summarize(audio_fp, stl, cmp):
240
+ tr = transcribe_wrapper(audio_fp)
241
+ if tr.startswith("Transcription error"):
242
+ return tr, ""
243
+ summary = summarize_text(tr, stl, cmp)
244
+ return tr, summary
245
+
246
+ btn_summarize.click(
247
+ fn=on_summarize,
248
+ inputs=[upload, style, compression],
249
+ outputs=[transcript_out, summary_md],
250
+ )
251
+
252
+ with gr.Column(scale=1):
253
+ gr.Markdown("## 2) Озвучка (SSML → TTS)")
254
+ speaker = gr.Dropdown(["aidar", "baya", "kseniya", "xenia", "eugene"], value="baya", label="Выберите голос")
255
+ btn_tts = gr.Button("🔊 Сгенерировать озвучку")
256
+ audio_out = gr.Audio(label="Озвучка (WAV)", type="numpy")
257
+ tts_file = gr.File(label="Скачать WAV")
258
+
259
+ def on_tts(markdown_text, stl, cmp, sp):
260
+ """
261
+ Возвращает:
262
+ - путь (str) для gr.Audio (type="filepath")
263
+ - путь (str) для gr.File
264
+ В случае ошибки возвращает (None, None).
265
+ """
266
+ try:
267
+ # получаем SSML-части (твоя функция)
268
+ parts = convert_summarize_text_with_ssml(markdown_text, stl, cmp)
269
+
270
+ # генерируем BytesIO с wav (твоя функция)
271
+ buf = synthesize_ssml_parts(parts, sp)
272
+ buf.seek(0)
273
+ data = buf.read()
274
+ if not data:
275
+ print("on_tts: audio buffer is empty")
276
+ return None, None
277
+
278
+ # записываем во временный файл и возвращаем путь
279
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
280
+ try:
281
+ tmp.write(data)
282
+ tmp.flush()
283
+ finally:
284
+ tmp.close()
285
+
286
+ # Опционально: можно вернуть имя файла без пути для отображения,
287
+ # но Gradio требует полный путь чтобы прочитать файл, поэтому возвращаем tmp.name
288
+ return tmp.name, tmp.name
289
+
290
+ except Exception as e:
291
+ # полезный лог в консоль для отладки
292
+ print("TTS generation error:", repr(e))
293
+ return None, None
294
+
295
+ # Подключаем обработчик к кнопке
296
+ btn_tts.click(
297
+ fn=on_tts,
298
+ inputs=[summary_md, style, compression, speaker],
299
+ outputs=[audio_out, tts_file],
300
+ )
301
+
302
+
303
  demo.launch()