File size: 5,897 Bytes
075e8ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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()