Spaces:
Sleeping
Sleeping
File size: 10,286 Bytes
3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 2bbbfa5 c15924f 2bbbfa5 3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 2bbbfa5 3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 2bbbfa5 3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 3fb1f38 c15924f 2bbbfa5 c15924f 2bbbfa5 c15924f 2bbbfa5 c15924f 2bbbfa5 c15924f 2bbbfa5 c15924f 2bbbfa5 3fb1f38 3c39789 3fb1f38 2bbbfa5 3fb1f38 3c39789 3fb1f38 2bbbfa5 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 c15924f 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 3c39789 3fb1f38 3c39789 | 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 | """
Voice -> Clean Text (Wispr Flow-style) β Gradio app.
Loads the two fine-tuned models pushed to the Hub by the training
notebook:
- WHISPER_REPO_ID: verbatim, disfluency-robust ASR
- LLM_REPO_ID: cleanup + tone adaptation
ZeroGPU note: no CUDA context exists in the main process β a GPU is attached
only for the duration of a call to a function decorated with @spaces.GPU, then
released. So both models are loaded onto CPU at startup; the only functions
that ever touch CUDA are _transcribe_on_gpu and _clean_up_on_gpu below, which
move the (already-loaded) models onto the GPU the first time they run and
reuse that placement on later calls within the same worker. On a plain CPU
Space (no @spaces available), everything falls back to running on CPU as-is.
"""
import os
import time
import gradio as gr
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
WhisperForConditionalGeneration,
WhisperProcessor,
pipeline,
)
try:
import spaces
ZEROGPU_AVAILABLE = True
except ImportError:
ZEROGPU_AVAILABLE = False
class _NoOpSpaces:
@staticmethod
def GPU(fn=None, **kwargs):
# No-op decorator so @spaces.GPU still works on a plain CPU Space
# (no `spaces` package installed / not running on HF infra).
if fn is not None:
return fn
return lambda f: f
spaces = _NoOpSpaces()
# ---------------------------------------------------------------------------
# Config β point these at the repos the notebook pushed to
# ---------------------------------------------------------------------------
WHISPER_REPO_ID = os.environ.get("WHISPER_REPO_ID", "aijadugar/wisprflow-clone-whisper")
LLM_REPO_ID = os.environ.get("LLM_REPO_ID", "aijadugar/wisprflow-clone-llm")
# CPU-only at import time β deliberately never assume CUDA exists here.
# Under ZeroGPU, torch.cuda.is_available() is unreliable/False in the main
# process anyway; actual device placement happens lazily inside the
# @spaces.GPU-decorated functions below.
#
# bfloat16 (not float32) on CPU: halves RAM for both models. Modern PyTorch
# has native CPU kernels for bf16 matmul/linear (unlike float16, which is
# poorly supported on CPU), so this is safe and meaningfully cuts memory
# pressure β important since the LLM checkpoint here is ~15GB.
LOAD_DEVICE = "cpu"
LOAD_DTYPE = torch.bfloat16
MODE_PROMPTS = {
"Email": (
"You are a transcription cleanup assistant. Rewrite the raw speech "
"transcript into clear, well-punctuated, professional email prose. "
"Remove filler words and false starts, fix grammar, use complete "
"sentences. Do not add information that was not said. Output ONLY "
"the cleaned text."
),
"Chat / Slack": (
"You are a transcription cleanup assistant. Rewrite the raw speech "
"transcript into clear, casual chat-message text. Remove filler "
"words and false starts, fix grammar, but keep it brief and "
"conversational -- do not over-formalize. Do not add information "
"that was not said. Output ONLY the cleaned text."
),
"Notes": (
"You are a transcription cleanup assistant. Rewrite the raw speech "
"transcript into clear, concise note form. Remove filler words and "
"false starts, fix grammar, tighten wordy phrasing. Do not add "
"information that was not said. Output ONLY the cleaned text."
),
"Plain cleanup": (
"You are a transcription cleanup assistant. Rewrite the raw speech "
"transcript into clear, well-punctuated text. Remove filler words "
"and false starts, fix grammar. Do not add information that was "
"not said. Output ONLY the cleaned text."
),
}
# ---------------------------------------------------------------------------
# Model loading (once, at startup) β CPU only. GPU placement is deferred to
# the @spaces.GPU-decorated inference functions below.
# ---------------------------------------------------------------------------
print(f"Loading ASR model from {WHISPER_REPO_ID} (CPU) ...")
whisper_processor = WhisperProcessor.from_pretrained(WHISPER_REPO_ID)
whisper_model = WhisperForConditionalGeneration.from_pretrained(
WHISPER_REPO_ID,
torch_dtype=LOAD_DTYPE,
low_cpu_mem_usage=True,
)
whisper_model.eval()
print(f"Loading cleanup LLM from {LLM_REPO_ID} (CPU) ...")
llm_tokenizer = AutoTokenizer.from_pretrained(LLM_REPO_ID)
llm_model = AutoModelForCausalLM.from_pretrained(
LLM_REPO_ID,
torch_dtype=LOAD_DTYPE,
low_cpu_mem_usage=True,
)
llm_model.eval()
print("Models loaded on CPU. GPU (if any) is attached lazily per-call.")
# Tracks whether each model currently lives on cuda, so repeat calls don't
# re-transfer weights every time within the same ZeroGPU worker.
_whisper_gpu_resident = False
_llm_gpu_resident = False
_asr_pipe = None # built lazily once we know the device/dtype for this call
# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------
@spaces.GPU
def _transcribe_on_gpu(audio_path):
"""The only function that touches CUDA for ASR. ZeroGPU attaches a GPU for
the duration of this call; whisper_model is moved here once (a no-op on
later calls once resident) and released back to the caller as plain text,
since nothing outside this function is guaranteed a live CUDA context."""
global _whisper_gpu_resident, _asr_pipe
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
if not _whisper_gpu_resident and device == "cuda":
whisper_model.to(device=device, dtype=dtype)
_whisper_gpu_resident = True
_asr_pipe = None # rebuild pipe against the now-GPU model
if _asr_pipe is None:
_asr_pipe = pipeline(
"automatic-speech-recognition",
model=whisper_model,
tokenizer=whisper_processor.tokenizer,
feature_extractor=whisper_processor.feature_extractor,
torch_dtype=whisper_model.dtype,
device=device,
)
result = _asr_pipe(audio_path)
return result["text"].strip()
@spaces.GPU
def _clean_up_on_gpu(raw_text, mode):
"""The only function that touches CUDA for the cleanup LLM. Same lazy
move-once pattern as _transcribe_on_gpu."""
global _llm_gpu_resident
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
if not _llm_gpu_resident and device == "cuda":
llm_model.to(device=device, dtype=dtype)
_llm_gpu_resident = True
system_prompt = MODE_PROMPTS.get(mode, MODE_PROMPTS["Plain cleanup"])
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": raw_text},
]
prompt = llm_tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = llm_tokenizer(prompt, return_tensors="pt").to(llm_model.device)
with torch.no_grad():
output_ids = llm_model.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
repetition_penalty=1.1,
pad_token_id=llm_tokenizer.pad_token_id or llm_tokenizer.eos_token_id,
)
new_tokens = output_ids[0][inputs["input_ids"].shape[1]:]
cleaned = llm_tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
return cleaned
def transcribe(audio_path):
if audio_path is None:
return "", 0.0, None
start = time.time()
try:
text = _transcribe_on_gpu(audio_path)
except Exception as e:
return "", time.time() - start, f"Transcription failed: {e}"
elapsed = time.time() - start
return text, elapsed, None
def clean_up(raw_text, mode):
if not raw_text.strip():
return "", 0.0, None
start = time.time()
try:
cleaned = _clean_up_on_gpu(raw_text, mode)
except Exception as e:
return "", time.time() - start, f"Cleanup failed: {e}"
elapsed = time.time() - start
return cleaned, elapsed, None
def run_pipeline(audio_path, mode):
if audio_path is None:
return "", "", "Record or upload audio first."
raw_text, asr_seconds, asr_error = transcribe(audio_path)
if asr_error:
return "", "", asr_error
if not raw_text:
return "", "", "Couldn't transcribe that clip β try again."
cleaned_text, llm_seconds, llm_error = clean_up(raw_text, mode)
if llm_error:
return raw_text, "", llm_error
total = asr_seconds + llm_seconds
stats = (
f"Transcription: {asr_seconds:.1f}s Β· "
f"Cleanup: {llm_seconds:.1f}s Β· "
f"Total: {total:.1f}s"
)
return raw_text, cleaned_text, stats
# ---------------------------------------------------------------------------
# UI β a plain, single-screen layout: record/upload, pick a style, run.
# ---------------------------------------------------------------------------
with gr.Blocks(title="Voice β Clean Text") as demo:
gr.Markdown(
"# ποΈ Voice β Clean Text\n"
"Record or upload audio, pick a style, and get a cleaned-up transcript."
)
with gr.Row():
with gr.Column(scale=1):
audio_input = gr.Audio(
sources=["microphone", "upload"],
type="filepath",
label="Speak or upload audio",
)
mode = gr.Radio(
choices=list(MODE_PROMPTS.keys()),
value="Plain cleanup",
label="Cleanup style",
)
run_btn = gr.Button("Transcribe & Clean Up", variant="primary")
with gr.Column(scale=1):
raw_output = gr.Textbox(label="Raw transcript (verbatim)", lines=5)
clean_output = gr.Textbox(label="Cleaned text", lines=5)
stats_output = gr.Markdown()
run_btn.click(
fn=run_pipeline,
inputs=[audio_input, mode],
outputs=[raw_output, clean_output, stats_output],
)
if __name__ == "__main__":
demo.queue().launch()
|