SolimanBa commited on
Commit
075e8ac
·
verified ·
1 Parent(s): 270b9dc

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -0
app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom captioning tool: transcribes audio/video with Whisper (large-v3) and chunks
3
+ captions by a user-specified number of words per caption, outputting an SRT file
4
+ ready to import into CapCut or any other editor.
5
+
6
+ Designed for Hugging Face Spaces ZeroGPU (dynamic H200 access).
7
+ Uses openai-whisper (PyTorch-based) rather than faster-whisper/CTranslate2,
8
+ since ZeroGPU's GPU-attach mechanism is built around torch.cuda and is most
9
+ reliable with PyTorch-native models.
10
+ """
11
+
12
+ import os
13
+ import tempfile
14
+
15
+ import gradio as gr
16
+ import spaces
17
+ import torch
18
+ import whisper
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Model loads once at startup on CPU. It's moved to GPU only inside the
22
+ # @spaces.GPU-decorated function below, since real CUDA access on ZeroGPU
23
+ # only exists during that call.
24
+ # ---------------------------------------------------------------------------
25
+ MODEL_SIZE = "large-v3"
26
+ model = whisper.load_model(MODEL_SIZE, device="cpu")
27
+
28
+
29
+ def format_timestamp(seconds: float) -> str:
30
+ """Convert seconds (float) to SRT timestamp format: HH:MM:SS,mmm"""
31
+ ms_total = int(round(seconds * 1000))
32
+ hours, rem = divmod(ms_total, 3600_000)
33
+ minutes, rem = divmod(rem, 60_000)
34
+ secs, ms = divmod(rem, 1000)
35
+ return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}"
36
+
37
+
38
+ def chunk_words(words, words_per_caption, max_chars=None):
39
+ """
40
+ Group a flat list of {"word","start","end"} dicts into caption chunks
41
+ of `words_per_caption` words each. Optionally caps chunk length by
42
+ max_chars so long words don't overflow a caption line.
43
+ """
44
+ captions = []
45
+ current = []
46
+
47
+ def flush():
48
+ if current:
49
+ captions.append({
50
+ "text": " ".join(w["word"] for w in current).strip(),
51
+ "start": current[0]["start"],
52
+ "end": current[-1]["end"],
53
+ })
54
+
55
+ for w in words:
56
+ current.append(w)
57
+ text_len = len(" ".join(x["word"] for x in current))
58
+ hit_word_limit = len(current) >= words_per_caption
59
+ hit_char_limit = max_chars is not None and text_len >= max_chars
60
+ if hit_word_limit or hit_char_limit:
61
+ flush()
62
+ current = []
63
+ flush()
64
+
65
+ return captions
66
+
67
+
68
+ def write_srt(captions, path):
69
+ with open(path, "w", encoding="utf-8") as f:
70
+ for i, cap in enumerate(captions, start=1):
71
+ f.write(f"{i}\n")
72
+ f.write(f"{format_timestamp(cap['start'])} --> {format_timestamp(cap['end'])}\n")
73
+ f.write(f"{cap['text']}\n\n")
74
+
75
+
76
+ @spaces.GPU(duration=120) # per-call GPU time budget; counts against your daily quota
77
+ def run_transcription(media_path):
78
+ """The only part that touches CUDA -- kept as small as possible to conserve quota."""
79
+ global model
80
+ model = model.to("cuda")
81
+ result = model.transcribe(media_path, word_timestamps=True, fp16=True)
82
+ model = model.to("cpu") # release VRAM before the GPU is handed back
83
+ torch.cuda.empty_cache()
84
+ return result
85
+
86
+
87
+ def transcribe_and_caption(media_file, words_per_caption, max_chars, progress=gr.Progress()):
88
+ if media_file is None:
89
+ return None, "Upload an audio or video file first."
90
+
91
+ words_per_caption = max(1, int(words_per_caption))
92
+ max_chars_val = int(max_chars) if max_chars and max_chars > 0 else None
93
+
94
+ progress(0.2, desc="Requesting GPU and transcribing (uses your ZeroGPU quota)...")
95
+ result = run_transcription(media_file)
96
+
97
+ words = []
98
+ for segment in result.get("segments", []):
99
+ for w in segment.get("words", []):
100
+ words.append({
101
+ "word": w["word"].strip(),
102
+ "start": w["start"],
103
+ "end": w["end"],
104
+ })
105
+
106
+ if not words:
107
+ return None, "No speech detected in the file."
108
+
109
+ progress(0.8, desc="Building captions...")
110
+ captions = chunk_words(words, words_per_caption, max_chars_val)
111
+
112
+ out_path = os.path.join(tempfile.gettempdir(), "captions.srt")
113
+ write_srt(captions, out_path)
114
+
115
+ preview_lines = [f"[{format_timestamp(c['start'])}] {c['text']}" for c in captions[:15]]
116
+ preview = "\n".join(preview_lines)
117
+ if len(captions) > 15:
118
+ preview += f"\n... ({len(captions) - 15} more captions)"
119
+
120
+ detected_lang = result.get("language", "unknown")
121
+ summary = (
122
+ f"Detected language: {detected_lang}\n"
123
+ f"Total captions: {len(captions)}\n\n"
124
+ f"Preview:\n{preview}"
125
+ )
126
+
127
+ progress(1.0, desc="Done")
128
+ return out_path, summary
129
+
130
+
131
+ with gr.Blocks(title="Word-Count Caption Generator") as demo:
132
+ gr.Markdown(
133
+ "# Word-Count Caption Generator\n"
134
+ "Upload a video or audio file, choose how many words should appear per caption, "
135
+ "and get back an `.srt` file to import into CapCut (or any editor).\n\n"
136
+ f"Running **{MODEL_SIZE}** on ZeroGPU (H200). Each run uses part of your daily "
137
+ "GPU quota, so batch your clips rather than testing repeatedly."
138
+ )
139
+
140
+ with gr.Row():
141
+ with gr.Column():
142
+ media_input = gr.File(
143
+ label="Audio or video file",
144
+ file_types=["audio", "video"],
145
+ )
146
+ words_slider = gr.Slider(
147
+ minimum=1, maximum=10, value=3, step=1,
148
+ label="Words per caption",
149
+ )
150
+ chars_slider = gr.Slider(
151
+ minimum=0, maximum=60, value=0, step=1,
152
+ label="Max characters per caption (0 = no limit)",
153
+ )
154
+ run_btn = gr.Button("Generate captions", variant="primary")
155
+
156
+ with gr.Column():
157
+ srt_output = gr.File(label="Download .srt")
158
+ summary_output = gr.Textbox(label="Summary / preview", lines=18)
159
+
160
+ run_btn.click(
161
+ fn=transcribe_and_caption,
162
+ inputs=[media_input, words_slider, chars_slider],
163
+ outputs=[srt_output, summary_output],
164
+ )
165
+
166
+ if __name__ == "__main__":
167
+ demo.launch()