Captioner / app.py
SolimanBa's picture
Create app.py
075e8ac verified
Raw
History Blame Contribute Delete
5.9 kB
"""
Custom captioning tool: transcribes audio/video with Whisper (large-v3) and chunks
captions by a user-specified number of words per caption, outputting an SRT file
ready to import into CapCut or any other editor.
Designed for Hugging Face Spaces ZeroGPU (dynamic H200 access).
Uses openai-whisper (PyTorch-based) rather than faster-whisper/CTranslate2,
since ZeroGPU's GPU-attach mechanism is built around torch.cuda and is most
reliable with PyTorch-native models.
"""
import os
import tempfile
import gradio as gr
import spaces
import torch
import whisper
# ---------------------------------------------------------------------------
# Model loads once at startup on CPU. It's moved to GPU only inside the
# @spaces.GPU-decorated function below, since real CUDA access on ZeroGPU
# only exists during that call.
# ---------------------------------------------------------------------------
MODEL_SIZE = "large-v3"
model = whisper.load_model(MODEL_SIZE, device="cpu")
def format_timestamp(seconds: float) -> str:
"""Convert seconds (float) to SRT timestamp format: HH:MM:SS,mmm"""
ms_total = int(round(seconds * 1000))
hours, rem = divmod(ms_total, 3600_000)
minutes, rem = divmod(rem, 60_000)
secs, ms = divmod(rem, 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}"
def chunk_words(words, words_per_caption, max_chars=None):
"""
Group a flat list of {"word","start","end"} dicts into caption chunks
of `words_per_caption` words each. Optionally caps chunk length by
max_chars so long words don't overflow a caption line.
"""
captions = []
current = []
def flush():
if current:
captions.append({
"text": " ".join(w["word"] for w in current).strip(),
"start": current[0]["start"],
"end": current[-1]["end"],
})
for w in words:
current.append(w)
text_len = len(" ".join(x["word"] for x in current))
hit_word_limit = len(current) >= words_per_caption
hit_char_limit = max_chars is not None and text_len >= max_chars
if hit_word_limit or hit_char_limit:
flush()
current = []
flush()
return captions
def write_srt(captions, path):
with open(path, "w", encoding="utf-8") as f:
for i, cap in enumerate(captions, start=1):
f.write(f"{i}\n")
f.write(f"{format_timestamp(cap['start'])} --> {format_timestamp(cap['end'])}\n")
f.write(f"{cap['text']}\n\n")
@spaces.GPU(duration=120) # per-call GPU time budget; counts against your daily quota
def run_transcription(media_path):
"""The only part that touches CUDA -- kept as small as possible to conserve quota."""
global model
model = model.to("cuda")
result = model.transcribe(media_path, word_timestamps=True, fp16=True)
model = model.to("cpu") # release VRAM before the GPU is handed back
torch.cuda.empty_cache()
return result
def transcribe_and_caption(media_file, words_per_caption, max_chars, progress=gr.Progress()):
if media_file is None:
return None, "Upload an audio or video file first."
words_per_caption = max(1, int(words_per_caption))
max_chars_val = int(max_chars) if max_chars and max_chars > 0 else None
progress(0.2, desc="Requesting GPU and transcribing (uses your ZeroGPU quota)...")
result = run_transcription(media_file)
words = []
for segment in result.get("segments", []):
for w in segment.get("words", []):
words.append({
"word": w["word"].strip(),
"start": w["start"],
"end": w["end"],
})
if not words:
return None, "No speech detected in the file."
progress(0.8, desc="Building captions...")
captions = chunk_words(words, words_per_caption, max_chars_val)
out_path = os.path.join(tempfile.gettempdir(), "captions.srt")
write_srt(captions, out_path)
preview_lines = [f"[{format_timestamp(c['start'])}] {c['text']}" for c in captions[:15]]
preview = "\n".join(preview_lines)
if len(captions) > 15:
preview += f"\n... ({len(captions) - 15} more captions)"
detected_lang = result.get("language", "unknown")
summary = (
f"Detected language: {detected_lang}\n"
f"Total captions: {len(captions)}\n\n"
f"Preview:\n{preview}"
)
progress(1.0, desc="Done")
return out_path, summary
with gr.Blocks(title="Word-Count Caption Generator") as demo:
gr.Markdown(
"# Word-Count Caption Generator\n"
"Upload a video or audio file, choose how many words should appear per caption, "
"and get back an `.srt` file to import into CapCut (or any editor).\n\n"
f"Running **{MODEL_SIZE}** on ZeroGPU (H200). Each run uses part of your daily "
"GPU quota, so batch your clips rather than testing repeatedly."
)
with gr.Row():
with gr.Column():
media_input = gr.File(
label="Audio or video file",
file_types=["audio", "video"],
)
words_slider = gr.Slider(
minimum=1, maximum=10, value=3, step=1,
label="Words per caption",
)
chars_slider = gr.Slider(
minimum=0, maximum=60, value=0, step=1,
label="Max characters per caption (0 = no limit)",
)
run_btn = gr.Button("Generate captions", variant="primary")
with gr.Column():
srt_output = gr.File(label="Download .srt")
summary_output = gr.Textbox(label="Summary / preview", lines=18)
run_btn.click(
fn=transcribe_and_caption,
inputs=[media_input, words_slider, chars_slider],
outputs=[srt_output, summary_output],
)
if __name__ == "__main__":
demo.launch()