Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +55 -18
- requirements.txt +0 -1
app.py
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import subprocess
|
| 3 |
import tempfile
|
| 4 |
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
-
from faster_gigaam import FastGigaAM
|
| 7 |
|
| 8 |
-
# Модель грузим ОДИН раз при старте контейнера, не на каждый запрос
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
# нигде не используем
|
| 13 |
-
#
|
| 14 |
-
#
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def _to_wav(src_path: str) -> str:
|
| 19 |
-
"""Конвертирует присланный файл (у Telegram голосовые — .ogg/opus
|
| 20 |
-
|
| 21 |
-
предсказуем
|
| 22 |
-
разберётся с любым форматом на входе."""
|
| 23 |
dst_path = tempfile.mktemp(suffix=".wav")
|
| 24 |
subprocess.run(
|
| 25 |
["ffmpeg", "-y", "-i", src_path, "-ar", "16000", "-ac", "1", dst_path],
|
|
@@ -29,18 +37,47 @@ def _to_wav(src_path: str) -> str:
|
|
| 29 |
return dst_path
|
| 30 |
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def transcribe(audio_path: str) -> str:
|
| 33 |
if not audio_path:
|
| 34 |
return ""
|
| 35 |
wav_path = _to_wav(audio_path)
|
|
|
|
| 36 |
try:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
segments, _info = MODEL.transcribe(wav_path)
|
| 41 |
-
return " ".join(seg.text for seg in segments).strip()
|
| 42 |
finally:
|
| 43 |
os.remove(wav_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
# api_name фиксирует имя эндпоинта — так со стороны бота адрес вызова
|
|
|
|
| 1 |
+
import glob
|
| 2 |
import os
|
| 3 |
import subprocess
|
| 4 |
import tempfile
|
| 5 |
|
| 6 |
+
import gigaam
|
| 7 |
import gradio as gr
|
|
|
|
| 8 |
|
| 9 |
+
# Модель грузим ОДИН раз при старте контейнера, не на каждый запрос.
|
| 10 |
+
# v3_e2e_ctc — версия с пунктуацией и нормализацией текста (не голый поток
|
| 11 |
+
# слов без знаков препинания) и CTC-декодер (быстрее RNNT, для транскрибации
|
| 12 |
+
# голосовых в чате этого достаточно).
|
| 13 |
+
# Никакого @spaces.GPU нигде не используем — Space создан на тарифе ZeroGPU
|
| 14 |
+
# (сейчас у бесплатных аккаунтов HF нет варианта CPU Basic при создании),
|
| 15 |
+
# но раз GPU нигде не запрашивается, приложение работает на обычном CPU
|
| 16 |
+
# этого же контейнера и не трогает дневную квоту GPU-секунд. GigaAM
|
| 17 |
+
# компактная (220-240М параметров), GPU ей и не нужен.
|
| 18 |
+
MODEL = gigaam.load_model("v3_e2e_ctc")
|
| 19 |
+
|
| 20 |
+
# Нативный model.transcribe() ограничен 25 секундами аудио за вызов.
|
| 21 |
+
# Официальный обход (.transcribe_longform) тянет за собой pyannote.audio
|
| 22 |
+
# и токен Hugging Face — ради простой последовательной нарезки заводить
|
| 23 |
+
# это не стоит, поэтому режем длинные голосовые сами через ffmpeg.
|
| 24 |
+
CHUNK_SECONDS = 20
|
| 25 |
|
| 26 |
|
| 27 |
def _to_wav(src_path: str) -> str:
|
| 28 |
+
"""Конвертирует присланный файл (у Telegram голосовые — .ogg/opus, но
|
| 29 |
+
подойдёт любой формат, который понимает ffmpeg — mp3, flac и т.д.) в
|
| 30 |
+
16kHz mono WAV. Модели нужен предсказуемый формат на входе."""
|
|
|
|
| 31 |
dst_path = tempfile.mktemp(suffix=".wav")
|
| 32 |
subprocess.run(
|
| 33 |
["ffmpeg", "-y", "-i", src_path, "-ar", "16000", "-ac", "1", dst_path],
|
|
|
|
| 37 |
return dst_path
|
| 38 |
|
| 39 |
|
| 40 |
+
def _split_into_chunks(wav_path: str) -> list[str]:
|
| 41 |
+
"""Режет WAV на куски по CHUNK_SECONDS через ffmpeg segment muxer.
|
| 42 |
+
Простая последовательная нарезка без перехлёста — изредка может
|
| 43 |
+
срезать слово ровно на границе куска, но для голосовых в чате это
|
| 44 |
+
приемлемый компромисс против того, чтобы тянуть pyannote+HF-токен
|
| 45 |
+
только ради длинных сообщений."""
|
| 46 |
+
out_dir = tempfile.mkdtemp()
|
| 47 |
+
pattern = os.path.join(out_dir, "chunk_%03d.wav")
|
| 48 |
+
subprocess.run(
|
| 49 |
+
[
|
| 50 |
+
"ffmpeg", "-y", "-i", wav_path,
|
| 51 |
+
"-f", "segment", "-segment_time", str(CHUNK_SECONDS),
|
| 52 |
+
"-c", "copy", pattern,
|
| 53 |
+
],
|
| 54 |
+
check=True,
|
| 55 |
+
capture_output=True,
|
| 56 |
+
)
|
| 57 |
+
return sorted(glob.glob(os.path.join(out_dir, "chunk_*.wav")))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
def transcribe(audio_path: str) -> str:
|
| 61 |
if not audio_path:
|
| 62 |
return ""
|
| 63 |
wav_path = _to_wav(audio_path)
|
| 64 |
+
chunks: list[str] = []
|
| 65 |
try:
|
| 66 |
+
chunks = _split_into_chunks(wav_path)
|
| 67 |
+
parts = [MODEL.transcribe(chunk).strip() for chunk in chunks]
|
| 68 |
+
return " ".join(p for p in parts if p).strip()
|
|
|
|
|
|
|
| 69 |
finally:
|
| 70 |
os.remove(wav_path)
|
| 71 |
+
for c in chunks:
|
| 72 |
+
try:
|
| 73 |
+
os.remove(c)
|
| 74 |
+
except OSError:
|
| 75 |
+
pass
|
| 76 |
+
if chunks:
|
| 77 |
+
try:
|
| 78 |
+
os.rmdir(os.path.dirname(chunks[0]))
|
| 79 |
+
except OSError:
|
| 80 |
+
pass
|
| 81 |
|
| 82 |
|
| 83 |
# api_name фиксирует имя эндпоинта — так со стороны бота адрес вызова
|
requirements.txt
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
torch
|
| 3 |
git+https://github.com/salute-developers/GigaAM.git
|
| 4 |
-
git+https://github.com/MishaNyaCopilot/faster-gigaam.git
|
| 5 |
gradio
|
|
|
|
| 1 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
torch
|
| 3 |
git+https://github.com/salute-developers/GigaAM.git
|
|
|
|
| 4 |
gradio
|