Update app.py
Browse files
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="
|
| 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()
|