apppy / app.py
Hosdroid's picture
Update app.py
88f806d verified
Raw
History Blame Contribute Delete
18.4 kB
# -*- coding: utf-8 -*-
# app.py — Human-grade Multilingual STT for Hugging Face Spaces
# خود-بهینه‌ساز + مود مخصوص فارسی + سازگاری خودکار با نسخه‌های مختلف Gradio
import os, sys, subprocess, importlib, io, time, tempfile, math, re, inspect
import numpy as np
def ensure(pkg, import_name=None, extra=None, quiet=False):
try:
return importlib.import_module(import_name or pkg)
except ImportError:
print(f"[setup] installing {pkg} ...")
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", pkg]
if extra: cmd += extra
if quiet: cmd += ["-q"]
subprocess.check_call(cmd)
return importlib.import_module(import_name or pkg)
gradio = ensure("gradio", "gradio")
fw = ensure("faster-whisper", "faster_whisper")
soundfile = ensure("soundfile", "soundfile")
# اختیاری برای کاهش نویز دقیق‌تر و ریسَمپل دقیق
try:
librosa = ensure("librosa", "librosa"); noisereduce = ensure("noisereduce", "noisereduce", quiet=True)
except Exception:
librosa = None; noisereduce = None
try:
psutil = ensure("psutil", "psutil", quiet=True)
except Exception:
psutil = None
import gradio as gr
from faster_whisper import WhisperModel
import soundfile as sf
# ------------------------- تشخیص منابع -------------------------
def detect_ram_gb():
try:
if psutil:
return max(1, int(psutil.virtual_memory().total / (1024**3)))
except Exception:
pass
return 8
def detect_cpu_threads():
try:
return max(1, min(8, os.cpu_count() or 4))
except Exception:
return 4
RAM_GB = detect_ram_gb()
CPU_THREADS = detect_cpu_threads()
def default_model_by_ram(ram_gb:int) -> str:
if ram_gb < 4: return "tiny"
if ram_gb < 8: return "base"
if ram_gb < 12: return "small"
return "medium"
DEFAULT_MODEL = default_model_by_ram(RAM_GB)
DEFAULT_COMPUTE = "int8_float16"
DEFAULT_CHUNK = 30
DEFAULT_BEAM = 3
DEFAULT_VAD = True
# ------------------------- تقویت صوت -------------------------
def limiter_soft(x: np.ndarray, gain: float = 10.0) -> np.ndarray:
if x.size == 0: return x
peak = max(np.max(np.abs(x)), 1e-6)
y = (x / peak) * gain
return np.tanh(y)
def enhance_audio(wav: np.ndarray, sr: int, boost: float = 10.0, precise: bool = False):
if precise and (librosa is not None) and (noisereduce is not None):
pad = min(len(wav), sr // 2)
try:
den = noisereduce.reduce_noise(y=wav, sr=sr, y_noise=wav[:pad] if pad > 0 else None, stationary=False)
except Exception:
den = wav
return limiter_soft(den, gain=boost)
else:
return limiter_soft(wav, gain=boost)
def load_audio_to_float32(file_path: str):
data, sr = sf.read(file_path, dtype="float32", always_2d=False)
if data.ndim == 2:
data = data.mean(axis=1)
return data.astype(np.float32), sr
def to_pcm16_wav_file(wav: np.ndarray, sr: int) -> str:
if (librosa is not None) and (sr != 16000):
try:
wav = librosa.resample(wav, orig_sr=sr, target_sr=16000); sr = 16000
except Exception:
pass
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
sf.write(tmp.name, wav, sr, subtype="PCM_16")
tmp.close()
return tmp.name
# ------------------------- مدل -------------------------
def build_model(size: str, compute_type: str, threads: int):
return WhisperModel(
size,
device="auto",
compute_type=compute_type,
cpu_threads=int(threads),
download_root=os.getenv("HF_HOME", None)
)
# ------------------------- مود مخصوص فارسی -------------------------
FA_COMMON_FIXES = {
"می باشد": "است",
"میشود": "می‌شود",
"میشه": "می‌شود",
"ها ی": "های",
" می خواهم": " می‌خواهم",
" نمی توان": " نمی‌توان",
" نمیخواهم": " نمی‌خواهم",
" میشود": " می‌شود",
" نمی شود": " نمی‌شود",
" های ": "‌های ",
}
FA_COMMA_HINTS = [
"البته","اما","ولی","یعنی","مثلا","در نتیجه","در نهایت","در کل","از طرفی","به علاوه","در واقع"
]
FA_HALFSPACE_PATTERNS = [
(r"\b(می)\s+(شود|کنم|کنی|کند|کنیم|کنید|کنند)\b", r"\1‌\2"),
(r"\b(نمی)\s+(شود|خواهم|خواهی|خواهد|خواهیم|خواهید|خواهند|کنم|کنی|کند|کنیم|کنید|کنند)\b", r"\1‌\2"),
(r"\b(تر|ترین)\b", r"‌\1"),
]
def fa_normalize_spaces(text: str) -> str:
s = text
s = re.sub(r"\s+", " ", s).strip()
s = s.replace(" ,", ",").replace(" .", ".").replace(" !", "!").replace(" ؟", "؟").replace(" ؛", "؛").replace(" :", ":")
s = re.sub(r"\s+،", "،", s)
s = re.sub(r"\s+%", "%", s)
return s
def fa_insert_commas(text: str) -> str:
s = text
for hint in FA_COMMA_HINTS:
s = re.sub(rf"(\s{hint})\s", rf"\1، ", s)
return s
def fa_sentenceize(text: str) -> str:
s = text.strip()
if not s: return s
if len(s) > 40 and not s.endswith((".", "؟", "!", "؛")):
s += "."
parts = re.split(r"([\.!\؟؛])", s)
out = []
buf = ""
for p in parts:
if p in [".","!","؟","؛"]:
buf += p
out.append(buf.strip())
buf = ""
else:
if buf:
buf += " " + p.strip()
else:
buf = p.strip()
if buf.strip():
out.append(buf.strip())
return " ".join(out)
def fa_apply_halfspaces(text: str) -> str:
s = " " + text + " "
for pat, rep in FA_HALFSPACE_PATTERNS:
s = re.sub(pat, rep, s)
return s.strip()
def fa_apply_common_fixes(text: str) -> str:
s = text
for k, v in FA_COMMON_FIXES.items():
s = s.replace(k, v)
return s
def fa_polish(text: str) -> str:
s = text
s = fa_apply_common_fixes(s)
s = fa_insert_commas(s)
s = fa_sentenceize(s)
s = fa_apply_halfspaces(s)
s = fa_normalize_spaces(s)
return s
def maybe_persian_polish(text: str, language: str) -> str:
if (language or "").lower() in ["fa", "auto", ""]:
return fa_polish(text)
return text
# ------------------------- خود-بهینه‌سازی -------------------------
def autotune_params(ram_gb:int, base_model:str, compute_type:str, threads:int,
chunk_len:int, beam:int, target_rt:float=1.0):
model = base_model; comp = compute_type; ch = int(chunk_len); bm = int(beam); th = int(threads)
if ram_gb < 4:
model = "tiny"; comp = "int8"; ch = min(ch, 20); bm = min(bm, 2); th = max(1, min(th, 4))
elif ram_gb < 8:
model = "base"; comp = "int8"; ch = min(ch, 25); bm = min(bm, 3); th = max(2, min(th, 6))
elif ram_gb < 12:
model = "small"; comp = "int8_float16"; ch = min(ch, 30); bm = min(bm, 3); th = max(3, min(th, 8))
else:
model = "small" if base_model in ["tiny","base"] else base_model
comp = "int8_float16"; ch = min(max(ch, 30), 45); bm = min(max(bm, 3), 5); th = max(4, min(th, 8))
return dict(model=model, compute=comp, chunk=ch, beam=bm, threads=th, target_rt=target_rt)
def quick_benchmark(model: WhisperModel, wav_path: str, language: str,
chunk_len:int, beam:int, vad:bool) -> float:
data, sr = sf.read(wav_path, dtype="float32", always_2d=False)
if data.ndim == 2: data = data.mean(axis=1)
max_len = int(sr * 12)
data = data[:max_len] if len(data) > max_len else data
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
sf.write(tmp.name, data, sr, subtype="PCM_16"); tmp.close()
t0 = time.time()
segs, info = model.transcribe(
tmp.name,
language=None if language=="auto" else language,
vad_filter=vad,
chunk_length=int(chunk_len),
beam_size=int(beam),
vad_parameters=dict(min_silence_duration_ms=300),
no_speech_threshold=0.6,
compression_ratio_threshold=2.8
)
t1 = time.time()
os.unlink(tmp.name)
audio_sec = len(data)/sr if sr>0 else 12.0
proc_sec = max(t1 - t0, 1e-6)
return proc_sec / max(audio_sec, 1e-6)
def refine_params_by_rt(params:dict, rt:float):
if rt > 1.2:
params["beam"] = max(1, params["beam"] - 1)
params["chunk"] = max(15, params["chunk"] - 5)
if params["model"] == "medium": params["model"] = "small"
elif params["model"] == "small": params["model"] = "base"
elif params["model"] == "base": params["model"] = "tiny"
elif rt < 0.7:
params["beam"] = min(5, params["beam"] + 1)
params["chunk"] = min(45, params["chunk"] + 5)
return params
# ------------------------- SRT -------------------------
def make_srt(segments):
def fmt_time(t):
h = int(t // 3600); t -= 3600*h
m = int(t // 60); s = t - 60*m
return f"{h:02d}:{m:02d}:{s:06.3f}".replace(".", ",")
lines = []
for i, s in enumerate(segments, 1):
txt = s.text.strip()
lines.append(str(i))
lines.append(f"{fmt_time(s.start)} --> {fmt_time(s.end)}")
lines.append(txt)
lines.append("")
return "\n".join(lines)
# ------------------------- پردازش اصلی (استریم) -------------------------
def transcribe_stream(
audio_file,
lang="auto",
auto_tune=True,
model_size=DEFAULT_MODEL,
compute_type=DEFAULT_COMPUTE,
threads=CPU_THREADS,
boost=10,
precise_enhance=False,
chunk_len=DEFAULT_CHUNK,
beam_size=DEFAULT_BEAM,
vad_filter=DEFAULT_VAD,
timestamps=False
):
if audio_file is None:
yield "لطفاً یک فایل صوتی انتخاب کن."
return
wav_fp = audio_file.name
wav, sr = load_audio_to_float32(wav_fp)
wav = enhance_audio(wav, sr, boost=float(boost), precise=bool(precise_enhance))
wav_path = to_pcm16_wav_file(wav, sr)
try:
p = autotune_params(RAM_GB, model_size, compute_type, int(threads), int(chunk_len), int(beam_size))
if not auto_tune:
p = dict(model=model_size, compute=compute_type, chunk=int(chunk_len), beam=int(beam_size),
threads=int(threads), target_rt=1.0)
yield f"آماده‌سازی مدل ({p['model']} | {p['compute']} | threads={p['threads']})..."
model = build_model(p["model"], p["compute"], p["threads"])
if auto_tune:
yield "بنچمارک کوتاه برای تنظیم سرعت/دقت..."
rt = quick_benchmark(model, wav_path, lang, p["chunk"], p["beam"], bool(vad_filter))
p = refine_params_by_rt(p, rt)
yield f"تنظیمات نهایی: model={p['model']} compute={p['compute']} chunk={p['chunk']} beam={p['beam']} threads={p['threads']} (RT≈{rt:.2f})"
if p["model"] != model_size:
yield f"تعویض مدل به {p['model']} ..."
model = build_model(p["model"], p["compute"], p["threads"])
yield "شروع پردازش..."
language = None if lang == "auto" else lang
segs, info = model.transcribe(
wav_path,
language=language,
vad_filter=bool(vad_filter),
chunk_length=int(p["chunk"]),
beam_size=int(p["beam"]),
vad_parameters=dict(min_silence_duration_ms=300),
no_speech_threshold=0.6,
compression_ratio_threshold=2.8
)
segments_collected = []
full_text = []
for seg in segs:
segments_collected.append(seg)
piece = seg.text.strip()
if piece:
full_text.append(piece)
current = " ".join(full_text)
if timestamps:
yield f"[{seg.start:.2f}{seg.end:.2f}] {piece}\n\n---\n{current}"
else:
yield current
final_text = " ".join(full_text).strip()
final_text = maybe_persian_polish(final_text, (lang or "auto"))
srt_text_raw = make_srt(segments_collected)
if (lang or "").lower() in ["fa","auto"]:
srt_text_raw = fa_normalize_spaces(srt_text_raw)
txt_fp = tempfile.NamedTemporaryFile(suffix=".txt", delete=False).name
with open(txt_fp, "w", encoding="utf-8") as f:
f.write(final_text or "")
srt_fp = tempfile.NamedTemporaryFile(suffix=".srt", delete=False).name
with open(srt_fp, "w", encoding="utf-8") as f:
f.write(srt_text_raw or "")
yield f"\n---\nپایان. از تبِ خروجی فایل‌ها را دانلود کن."
finally:
if os.path.exists(wav_path):
try: os.unlink(wav_path)
except: pass
def transcribe_and_return_files(
audio_file, lang, auto_tune, model_size, compute_type, threads, boost, precise_enhance, chunk_len, beam_size, vad_filter
):
if audio_file is None:
return None, None, "فایل انتخاب نشده."
wav_fp = audio_file.name
wav, sr = load_audio_to_float32(wav_fp)
wav = enhance_audio(wav, sr, boost=float(boost), precise=bool(precise_enhance))
wav_path = to_pcm16_wav_file(wav, sr)
try:
p = autotune_params(RAM_GB, model_size, compute_type, int(threads), int(chunk_len), int(beam_size))
if not auto_tune:
p = dict(model=model_size, compute=compute_type, chunk=int(chunk_len), beam=int(beam_size),
threads=int(threads), target_rt=1.0)
model = build_model(p["model"], p["compute"], p["threads"])
segs, info = model.transcribe(
wav_path,
language=None if lang=="auto" else lang,
vad_filter=bool(vad_filter),
chunk_length=int(p["chunk"]),
beam_size=int(p["beam"]),
vad_parameters=dict(min_silence_duration_ms=300),
no_speech_threshold=0.6,
compression_ratio_threshold=2.8
)
texts = [s.text.strip() for s in segs if s.text.strip()]
final_text = " ".join(texts).strip()
final_text = maybe_persian_polish(final_text, lang)
srt_text = make_srt(segs)
if (lang or "").lower() in ["fa","auto"]:
srt_text = fa_normalize_spaces(srt_text)
txt_fp = tempfile.NamedTemporaryFile(suffix=".txt", delete=False).name
with open(txt_fp, "w", encoding="utf-8") as f:
f.write(final_text or "")
srt_fp = tempfile.NamedTemporaryFile(suffix=".srt", delete=False).name
with open(srt_fp, "w", encoding="utf-8") as f:
f.write(srt_text or "")
return txt_fp, srt_fp, "آماده دانلود."
except Exception as e:
return None, None, f"خطا: {e}"
finally:
if os.path.exists(wav_path):
try: os.unlink(wav_path)
except: pass
# ------------------------- رابط Gradio -------------------------
LANGS = [
("Automatic (detect)", "auto"),
("فارسی", "fa"),
("English", "en"),
("العربية", "ar"),
("Türkçe", "tr"),
("Français", "fr"),
("Deutsch", "de"),
("Español", "es"),
("Русский", "ru"),
("中文", "zh")
]
MODELS = ["tiny", "base", "small", "medium"]
COMPUTE_TYPES = ["int8_float16", "int8", "float16", "float32"]
with gr.Blocks(title="Human-grade STT — Fast & Self-Tuning", css="#stream{white-space:pre-wrap;}") as demo:
gr.Markdown("### تبدیل گفتار به متن چندزبانه — سریع، خود-بهینه‌ساز، با مود مخصوص فارسی")
with gr.Row():
with gr.Column(scale=2):
audio = gr.Audio(label="فایل صوتی را آپلود کن یا ضبط کن", type="filepath", sources=["upload", "microphone"])
lang = gr.Dropdown(choices=[l[1] for l in LANGS], value="auto", label="Language")
auto_tune = gr.Checkbox(value=True, label="خود-بهینه‌سازی (پیشنهادی)")
model_size = gr.Dropdown(choices=MODELS, value=DEFAULT_MODEL, label="Model size")
compute_type = gr.Dropdown(choices=COMPUTE_TYPES, value=DEFAULT_COMPUTE, label="Compute type")
threads = gr.Slider(1, 8, step=1, value=CPU_THREADS, label="CPU threads")
chunk_len = gr.Slider(10, 60, step=5, value=DEFAULT_CHUNK, label="Chunk length (sec)")
beam_size = gr.Slider(1, 5, step=1, value=DEFAULT_BEAM, label="Beam size")
vad_filter = gr.Checkbox(value=True, label="Voice activity detection")
boost = gr.Slider(1, 12, step=1, value=10, label="Boost / Limiter")
precise_enhance = gr.Checkbox(value=False, label="Precise denoise (CPU/RAM بیشتر)")
run_btn = gr.Button("شروع استریم")
build_btn = gr.Button("تولید خروجی‌های نهایی (TXT/SRT)")
with gr.Column(scale=3):
stream_out = gr.Textbox(label="استریم زنده متن", lines=18, elem_id="stream")
txt_file = gr.File(label="دانلود متن (TXT)")
srt_file = gr.File(label="دانلود زیرنویس (SRT)")
status = gr.Markdown(f"RAM≈{RAM_GB}GB • اگر کند است: model=tiny/base، compute=int8، chunk=20–30، beam=1–2")
run_btn.click(
fn=transcribe_stream,
inputs=[audio, lang, auto_tune, model_size, compute_type, threads, boost, precise_enhance, chunk_len, beam_size, vad_filter, gr.Checkbox(False, visible=False)],
outputs=stream_out
)
build_btn.click(
fn=transcribe_and_return_files,
inputs=[audio, lang, auto_tune, model_size, compute_type, threads, boost, precise_enhance, chunk_len, beam_size, vad_filter],
outputs=[txt_file, srt_file, status]
)
if __name__ == "__main__":
# سازگاری خودکار با نسخه‌های مختلف Gradio (رفع خطای concurrency_count)
queue_sig = inspect.signature(gr.Blocks.queue)
kwargs = {}
if "concurrency_count" in queue_sig.parameters:
kwargs["concurrency_count"] = 1
if "max_size" in queue_sig.parameters:
kwargs["max_size"] = 8
demo.queue(**kwargs).launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", 7860))
)