|
|
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, |
|
|
sample_rate=48000, |
|
|
put_accent=True, |
|
|
put_yo=True, |
|
|
) |
|
|
|
|
|
|
|
|
audio_np = audio_tensor.cpu().numpy() |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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_text = response.choices[0].message.content |
|
|
return json.loads(json_text) |
|
|
|
|
|
|
|
|
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 "" |
|
|
|
|
|
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) |
|
|
|
|
|
return ("abstract.pdf", pdf_bytes) |
|
|
except Exception as e: |
|
|
return None |
|
|
|
|
|
def ssml_and_tts_wrapper(markdown_text, style, compression, speaker): |
|
|
try: |
|
|
|
|
|
parts = convert_summarize_text_with_ssml(markdown_text, style, compression) |
|
|
|
|
|
audio_buffer = synthesize_ssml_parts(parts, speaker) |
|
|
audio_buffer.seek(0) |
|
|
|
|
|
return audio_buffer.read() |
|
|
except Exception as e: |
|
|
|
|
|
return f"TTS error: {e}" |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
parts = convert_summarize_text_with_ssml(markdown_text, stl, cmp) |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
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() |