File size: 13,911 Bytes
bdb5570 ae94ee6 |
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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
import os
import io
import gigaam
import gradio as gr
from mistralai import Mistral
from pydub import AudioSegment
import markdown2
from xhtml2pdf import pisa
import torch
import json
import numpy as np
import tempfile
def load_tts_model():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.package.PackageImporter("v4_ru.pt").load_pickle("tts_models", "model")
model.to(device)
return model, device
tts_model, tts_device = load_tts_model()
# === Функция генерации озвучки ===
def synthesize_ssml_parts(parts, speaker="baya", sample_rate=48000):
audio_segments = []
for part in parts:
if isinstance(part, dict):
text_ssml = part.get("part", "")
else:
text_ssml = part
# генерируем аудио
audio_tensor = tts_model.apply_tts(
text=text_ssml,
speaker=speaker, # можно поменять: aidar, baya, kseniya, xenia, eugene
sample_rate=48000,
put_accent=True,
put_yo=True,
)
# конвертируем в numpy float32
audio_np = audio_tensor.cpu().numpy()
# создаём AudioSegment
segment = AudioSegment(
(audio_np * 32767).astype(np.int16).tobytes(),
frame_rate=48000,
sample_width=2,
channels=1
)
audio_segments.append(segment)
# объединяем все сегменты
combined = sum(audio_segments)
# сохраняем в BytesIO
buffer = io.BytesIO()
combined.export(buffer, format="wav")
buffer.seek(0)
return buffer
# === Определение длительности аудио ===
def get_audio_duration(file_path: str) -> float:
audio = AudioSegment.from_file(file_path)
return audio.duration_seconds
# === Транскрибация с прогресс-баром ===
def transcribe_audio(audio_file: str, progress_bar) -> str:
os.environ["HF_TOKEN"] = os.getenv("HF_TOKEN")
model = gigaam.load_model("v2_rnnt")
total_duration = get_audio_duration(audio_file)
recognition_result = model.transcribe_longform(audio_file)
all_text = []
last_progress = 0
for utterance in recognition_result:
transcription = utterance["transcription"]
start, end = utterance["boundaries"]
all_text.append(f"[{gigaam.format_time(start)} - {gigaam.format_time(end)}]: {transcription}")
# обновляем прогресс
current_progress = int((end / total_duration) * 100 * 0.9)
if current_progress > last_progress:
progress_bar.progress(current_progress, text="⏳ Транскрибируем аудио...")
last_progress = current_progress
return "\n".join(all_text)
# === Генерация PDF из Markdown ===
def create_pdf_abstract(markdown_text: str) -> bytes:
html = markdown2.markdown(markdown_text)
buffer = io.BytesIO()
pisa.CreatePDF(io.StringIO(html), dest=buffer)
buffer.seek(0)
return buffer.read()
# === Суммаризация ===
def summarize_text(text: str, style: str, length: str) -> str:
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
client = Mistral(api_key=MISTRAL_API_KEY)
prompt = f"""
Ты — умный помощник.
Сделай {length} {style} конспект по этому тексту (на русском языке):
{text}
"""
response = client.chat.complete(
model="mistral-large-latest",
messages=[
{"role": "system", "content": "Ты создаёшь структурированные конспекты в формате Markdown."},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content
def convert_summarize_text_with_ssml(text: str, style: str, length: str) -> list:
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
client = Mistral(api_key=MISTRAL_API_KEY)
prompt = f"""
Ты — умный помощник.
Разбей текст на части, где каждая часть не больше 1000 символов.
Каждую часть оберни в SSML тег <speak>.
- Оберни текст в тег <speak> ... </speak>.
- **Знаки препинания не должны произноситься словами.** Вместо этого:
- запятая → вставь `<break time="220ms"/>` в месте запятой;
- точка / конец предложения → вставь `<break time="450ms"/>` после предложения;
- двоеточие / точка с запятой → `<break time="350ms"/>`;
- длинная пауза / переход к новому абзацу → `<break time="700ms"/>`.
- **Вопросительные предложения**: в конце вопроса НЕ вставляй слово «вопросительный знак». Вместо этого оберни заключительную часть вопроса в `<prosody pitch="+12%" rate="95%">...</prosody>` чтобы задать подъём интонации, и затем `<break time="450ms"/>`.
- **Восклицательные предложения**: выдели ключевую фразу с помощью `<emphasis level="strong">...</emphasis>` и / или `<prosody pitch="+15%" rate="105%">...</prosody>`, затем `<break time="450ms"/>`.
- **Кавычки / прямые речи**: при открытии цитаты добавь небольшую паузу `<break time="200ms"/>`, затем внутри цитаты можно использовать `<emphasis level="moderate">` или слегка поднять `pitch` для выразительности, после цитаты — пауза `<break time="300ms"/>`. Не произноси слово «кавычки».
- **Числа**: записывай цифры буквенно; если невозможно — используй `<say-as interpret-as="cardinal">...</say-as>` для чисел (но приоритет — слова).
- **Не вставляй** никаких дополнительных SSML-тегов, которые могут быть не поддержаны (например, vendor-specific `<amazon:...>`). Используй только: `<speak>`, `<break>`, `<prosody>`, `<emphasis>`, `<say-as>`.
Цифры запиши буквенно.
Пеши без сокращений слов(не г. а год/года)
Верни результат в JSON формате: список объектов с полем 'part'.
Пример:
[
{{"part": "<speak>Текст части 1...</speak>"}},
{{"part": "<speak>Текст части 2...</speak>"}}
]
Только JSON, никаких объяснений.
{text}
RETURN ONLY JSON
"""
response = client.chat.complete(
model="pixtral-12b-2409",
messages=[
{"role": "system", "content": "Ты создаёшь структурированные конспекты для TTS с SSML."},
{"role": "user", "content": f"{prompt}"},
],
response_format={"type": "json_object"}
)
print(response.choices[0].message.content)
# Получаем JSON
json_text = response.choices[0].message.content
return json.loads(json_text)
# Небольшая "заглушка" прогресс-бара, чтобы можно было вызвать transcribe_audio (он ожидает progress_bar)
class DummyProgress:
def progress(self, *args, **kwargs):
return None
progress_dummy = DummyProgress()
# Обёртки (НЕ меняют логику твоих функций)
def transcribe_wrapper(audio_filepath):
if audio_filepath is None or audio_filepath == "":
return ""
# Gradio даёт путь к временному файлу — передаём напрямую в твою функцию
try:
return transcribe_audio(audio_filepath, progress_dummy)
except Exception as e:
return f"Transcription error: {e}"
def summarize_wrapper(audio_filepath, style, compression):
# получаем транскрипт (если пользователь подаёт текст в дальнейшем — можно расширить)
transcript = transcribe_wrapper(audio_filepath)
if transcript.startswith("Transcription error"):
return transcript
try:
summary = summarize_text(transcript, style, compression)
return summary
except Exception as e:
return f"Summarization error: {e}"
def pdf_wrapper(markdown_text):
try:
pdf_bytes = create_pdf_abstract(markdown_text)
# Gradio принимает bytes для File/Download
return ("abstract.pdf", pdf_bytes)
except Exception as e:
return None
def ssml_and_tts_wrapper(markdown_text, style, compression, speaker):
try:
# Получаем части с SSML (твоя функция)
parts = convert_summarize_text_with_ssml(markdown_text, style, compression)
# Генерируем аудио (твоя функция возвращает BytesIO)
audio_buffer = synthesize_ssml_parts(parts, speaker)
audio_buffer.seek(0)
# Для gr.Audio можно возвращать bytes либо путь к файлу.
return audio_buffer.read()
except Exception as e:
# В случае ошибки возвращаем строку с сообщением (Gradio покажет)
return f"TTS error: {e}"
# Запускаем интерфейс Gradio
with gr.Blocks() as demo:
gr.Markdown("# 🎙️ Аудио-конспекты (Gradio)")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## 1) Загрузка аудио и создание конспекта")
upload = gr.Audio(label="Загрузите аудио (mp3/wav)", type="filepath")
style = gr.Dropdown(["структурированный", "в виде списка", "подробный", "короткий"], value="структурированный", label="Стиль конспекта")
compression = gr.Slider(0, 100, 50, label="Уровень сжатия (0 — подробно, 100 — кратко)")
btn_summarize = gr.Button("✨ Сделать конспект")
transcript_out = gr.Textbox(label="Транскрипт (результат распознавания)", lines=6)
summary_md = gr.Textbox(label="Конспект (Markdown)", lines=15)
# Кнопка создаёт транскрипт и конспект
def on_summarize(audio_fp, stl, cmp):
tr = transcribe_wrapper(audio_fp)
if tr.startswith("Transcription error"):
return tr, ""
summary = summarize_text(tr, stl, cmp)
return tr, summary
btn_summarize.click(
fn=on_summarize,
inputs=[upload, style, compression],
outputs=[transcript_out, summary_md],
)
with gr.Column(scale=1):
gr.Markdown("## 2) Озвучка (SSML → TTS)")
speaker = gr.Dropdown(["aidar", "baya", "kseniya", "xenia", "eugene"], value="baya", label="Выберите голос")
btn_tts = gr.Button("🔊 Сгенерировать озвучку")
audio_out = gr.Audio(label="Озвучка (WAV)", type="numpy")
tts_file = gr.File(label="Скачать WAV")
def on_tts(markdown_text, stl, cmp, sp):
"""
Возвращает:
- путь (str) для gr.Audio (type="filepath")
- путь (str) для gr.File
В случае ошибки возвращает (None, None).
"""
try:
# получаем SSML-части (твоя функция)
parts = convert_summarize_text_with_ssml(markdown_text, stl, cmp)
# генерируем BytesIO с wav (твоя функция)
buf = synthesize_ssml_parts(parts, sp)
buf.seek(0)
data = buf.read()
if not data:
print("on_tts: audio buffer is empty")
return None, None
# записываем во временный файл и возвращаем путь
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
try:
tmp.write(data)
tmp.flush()
finally:
tmp.close()
# Опционально: можно вернуть имя файла без пути для отображения,
# но Gradio требует полный путь чтобы прочитать файл, поэтому возвращаем tmp.name
return tmp.name, tmp.name
except Exception as e:
# полезный лог в консоль для отладки
print("TTS generation error:", repr(e))
return None, None
# Подключаем обработчик к кнопке
btn_tts.click(
fn=on_tts,
inputs=[summary_md, style, compression, speaker],
outputs=[audio_out, tts_file],
)
demo.launch() |