NickVerri commited on
Commit
b4c2174
·
verified ·
1 Parent(s): 0ea9bf1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2 -278
app.py CHANGED
@@ -113,285 +113,9 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
113
  return "\n".join(edl_lines)
114
 
115
  def generate_xml(sequence_name, segments, source_name, fps=25):
116
- """Constructs a Final Cut Pro 7 XML (compatible with DaVinci Resolve)."""
117
 
118
- # XML Header
119
  xml_output = [
120
  '<?xml version="1.0" encoding="UTF-8"?>',
121
  '<!DOCTYPE xmeml>',
122
- '<xmeml version="4">',
123
- '<sequence>',
124
- f'\t<name>{sequence_name}</name>',
125
- '\t<rate>',
126
- f'\t\t<timebase>{fps}</timebase>',
127
- '\t</rate>',
128
- '\t<media>',
129
- '\t\t<video>',
130
- '\t\t\t<format>',
131
- '\t\t\t\t<samplecharacteristics>',
132
- f'\t\t\t\t\t<rate><timebase>{fps}</timebase></rate>',
133
- '\t\t\t\t\t<width>1920</width>',
134
- '\t\t\t\t\t<height>1080</height>',
135
- '\t\t\t\t\t<pixelaspectratio>square</pixelaspectratio>',
136
- '\t\t\t\t</samplecharacteristics>',
137
- '\t\t\t</format>',
138
- '\t\t\t<track>'
139
- ]
140
-
141
- # Timeline Tracker
142
- timeline_head_frames = 0
143
-
144
- for i, seg in enumerate(segments, 1):
145
- # Calculate Frame Data
146
- src_in_frames = seconds_to_frames(seg['src_start'], fps)
147
- src_out_frames = seconds_to_frames(seg['src_end'], fps)
148
- duration_frames = src_out_frames - src_in_frames
149
-
150
- # Timeline positions
151
- tl_start = timeline_head_frames
152
- tl_end = tl_start + duration_frames
153
-
154
- clip_note = seg.get('note', 'Junior Editor Selection')
155
-
156
- # XML Clip Item
157
- xml_output.append(f'\t\t\t\t<clipitem id="clipitem-{i}">')
158
- xml_output.append(f'\t\t\t\t\t<name>{clip_note}</name>')
159
- xml_output.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
160
- xml_output.append(f'\t\t\t\t\t<rate><timebase>{fps}</timebase></rate>')
161
- xml_output.append(f'\t\t\t\t\t<start>{tl_start}</start>')
162
- xml_output.append(f'\t\t\t\t\t<end>{tl_end}</end>')
163
- xml_output.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
164
- xml_output.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
165
-
166
- # File Reference (The Multicam Link)
167
- xml_output.append(f'\t\t\t\t\t<file id="multicam_file">')
168
- xml_output.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
169
- xml_output.append(f'\t\t\t\t\t\t<pathurl>file://localhost/placeholder/{source_name}</pathurl>')
170
- xml_output.append(f'\t\t\t\t\t\t<rate><timebase>{fps}</timebase></rate>')
171
- xml_output.append(f'\t\t\t\t\t\t<timecode><string>00:00:00:00</string></timecode>')
172
- xml_output.append(f'\t\t\t\t\t</file>')
173
-
174
- xml_output.append(f'\t\t\t\t</clipitem>')
175
-
176
- # Handle Gaps
177
- gap_seconds = seg.get('gap', 0.0)
178
- gap_frames = seconds_to_frames(gap_seconds, fps)
179
- timeline_head_frames = tl_end + gap_frames
180
-
181
- # XML Footer
182
- xml_output.extend([
183
- '\t\t\t</track>',
184
- '\t\t</video>',
185
- '\t</media>',
186
- '</sequence>',
187
- '</xmeml>'
188
- ])
189
-
190
- return "\n".join(xml_output)
191
-
192
- def call_gemini_for_edl(transcript_data, story_prompt, api_key):
193
- if not api_key:
194
- st.error("Gemini API Key is missing.")
195
- return None
196
-
197
- url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
198
-
199
- system_prompt = (
200
- "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
201
- "(which includes Speaker IDs and word-level timestamps) to create a condensed story. "
202
- "Output ONLY a valid JSON array of segments with 'src_start', 'src_end', 'note', and optionally 'gap'. "
203
- "CRITICAL RULES:\n"
204
- "1. IGNORE ALL INTERVIEWER COMMENTS: Do not include any speech or segments where the interviewer is speaking.\n"
205
- "2. REMOVE FLUFF: Delete 'um', 'ah', repeats, and irrelevant filler.\n"
206
- "3. NARRATIVE FLOW: Focus on the subject's high-energy responses and narrative hooks.\n"
207
- "4. TIMESTAMP INTEGRITY: Use only the exact word-level start and end times from the data.\n"
208
- "5. PACING: Group related clips together. Between distinct ideas, add a 'gap': 1.0 (float seconds) "
209
- "to the segment preceding the break."
210
- )
211
-
212
- prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
213
-
214
- payload = {
215
- "contents": [{"parts": [{"text": prompt_text}]}],
216
- "systemInstruction": {"parts": [{"text": system_prompt}]},
217
- "generationConfig": {"responseMimeType": "application/json"}
218
- }
219
-
220
- try:
221
- res = requests.post(url, json=payload)
222
- res.raise_for_status()
223
- result_json = res.json()
224
- return json.loads(result_json['candidates'][0]['content']['parts'][0]['text'])
225
- except Exception as e:
226
- st.error(f"Senior Editor AI Error: {e}")
227
- return None
228
-
229
- # --- Streamlit UI ---
230
- st.set_page_config(page_title="Junior Editor", layout="wide")
231
- st.title("Junior Editor")
232
-
233
- st.markdown("""
234
- **Instructions**
235
- * **For Multicam Workflows:** Use the **XML** option in the sidebar and enter the **exact name** of your Multicam Sequence.
236
- * Upload your file here (video or audio).
237
- * Set your timeline FPS and transcription quality.
238
- * Junior Editor will transcribe and separate speakers. You can then instruct it to find engaging bits or construct a narrative.
239
- * It will create an EDL or XML to import back into your editing software (Resolve, Premiere).
240
- """)
241
-
242
- st.divider()
243
-
244
- with st.sidebar:
245
- st.header("Project Settings")
246
- fps = st.number_input("Timeline FPS", value=25)
247
-
248
- st.header("Export Settings")
249
- export_format = st.radio("Output Format", ["EDL", "XML (Multicam)"], index=0)
250
-
251
- input_label = "EDL Reel Name"
252
- input_help = "Leave empty to use the uploaded file name."
253
-
254
- if export_format == "XML (Multicam)":
255
- input_label = "Multicam Sequence Name"
256
- input_help = "EXACT name of your Multicam Clip in Resolve."
257
-
258
- st.info("💡 **Conform Helper**")
259
- custom_reel_name = st.text_input(
260
- input_label,
261
- placeholder="e.g. Interview_Day1_Multi",
262
- help=input_help
263
- )
264
-
265
- st.header("Model Settings")
266
- model_size = st.selectbox("Whisper Model", ["large-v2", "medium", "base"], index=0)
267
-
268
- language_map = {
269
- "Auto-Detect": None,
270
- "English": "en",
271
- "Spanish": "es",
272
- "French": "fr",
273
- "German": "de",
274
- "Italian": "it",
275
- "Portuguese": "pt"
276
- }
277
- selected_lang_label = st.selectbox("Audio Language", list(language_map.keys()), index=1)
278
- target_language = language_map[selected_lang_label]
279
-
280
- num_speakers = st.number_input("Speakers (0=Auto)", min_value=0, value=0)
281
-
282
- st.divider()
283
- if ACTIVE_HF_TOKEN == "PASTE_YOUR_HF_TOKEN_HERE":
284
- st.warning("⚠️ HF_TOKEN not set in Secrets!")
285
- else:
286
- st.success("✅ HF_TOKEN Loaded")
287
-
288
- uploaded_file = st.file_uploader("Upload Video/Audio Clip", type=["mp4", "m4a", "wav", "mp3", "mov"])
289
-
290
- if uploaded_file:
291
- # --- Auto-Reset Logic ---
292
- if "last_processed_file" not in st.session_state or st.session_state.last_processed_file != uploaded_file.name:
293
- if "transcript" in st.session_state:
294
- del st.session_state.transcript
295
- st.session_state.last_processed_file = uploaded_file.name
296
-
297
- # --- Auto-Process Logic ---
298
- if "transcript" not in st.session_state:
299
- if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
300
- st.error("Please provide a valid Hugging Face Token in the Sidebar/Secrets.")
301
- else:
302
- progress_container = st.container()
303
- with progress_container:
304
- st.info("🤖 **Junior Editor is processing your file...**")
305
- status_text = st.empty()
306
- progress_bar = st.progress(0)
307
-
308
- try:
309
- # Phase 1
310
- status_text.markdown("**Phase 1/4: Extracting Audio...**")
311
- with open("temp_input", "wb") as f:
312
- f.write(uploaded_file.getbuffer())
313
-
314
- subprocess.run(["ffmpeg", "-i", "temp_input", "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "temp_audio.wav", "-y"])
315
- progress_bar.progress(25)
316
-
317
- device = "cuda" if torch.cuda.is_available() else "cpu"
318
- if device == "cpu": st.warning("⚠️ No GPU detected.")
319
-
320
- # Phase 2
321
- status_text.markdown(f"**Phase 2/4: Transcribing (Whisper {model_size})... This is the longest step.**")
322
- compute_type = "float16" if device == "cuda" else "int8"
323
- model = whisperx.load_model(model_size, device, compute_type=compute_type)
324
- audio = whisperx.load_audio("temp_audio.wav")
325
- result = model.transcribe(audio, batch_size=16, language=target_language)
326
- del model
327
- gc.collect()
328
- torch.cuda.empty_cache()
329
- progress_bar.progress(50)
330
-
331
- # Phase 3
332
- status_text.markdown("**Phase 3/4: Aligning Text...**")
333
- model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=device)
334
- result = whisperx.align(result["segments"], model_a, metadata, audio, device, return_char_alignments=False)
335
- del model_a
336
- gc.collect()
337
- torch.cuda.empty_cache()
338
- progress_bar.progress(75)
339
-
340
- # Phase 4
341
- status_text.markdown("**Phase 4/4: Identifying Speakers...**")
342
- diarize_model = whisperx.DiarizationPipeline(use_auth_token=ACTIVE_HF_TOKEN, device=device)
343
- diarize_kwargs = {"min_speakers": num_speakers, "max_speakers": num_speakers} if num_speakers > 0 else {}
344
- diarize_segments = diarize_model(audio, **diarize_kwargs)
345
-
346
- # Final Merge
347
- status_text.markdown("**Finalizing...**")
348
- final_result = whisperx.assign_word_speakers(diarize_segments, result)
349
-
350
- processed_segments = []
351
- for segment in final_result["segments"]:
352
- processed_segments.append({
353
- "speaker": segment.get("speaker", "Unknown"),
354
- "text": segment["text"].strip(),
355
- "start": segment["start"],
356
- "end": segment["end"]
357
- })
358
-
359
- st.session_state.transcript = processed_segments
360
- if os.path.exists("temp_input"): os.remove("temp_input")
361
- if os.path.exists("temp_audio.wav"): os.remove("temp_audio.wav")
362
- progress_bar.progress(100)
363
- status_text.success(f"Done! Processed {len(processed_segments)} segments.")
364
-
365
- except Exception as e:
366
- status_text.error(f"Error: {e}")
367
- if os.path.exists("temp_input"): os.remove("temp_input")
368
- st.stop()
369
-
370
- if "transcript" in st.session_state:
371
- st.divider()
372
- with st.expander("Transcript Preview", expanded=True):
373
- for seg in st.session_state.transcript:
374
- st.markdown(f"**{seg['speaker']}:** {seg['text']}")
375
-
376
- st.subheader("Your Instruction")
377
- brief = st.text_area("What should the Junior Editor do?", placeholder="e.g. Find the most engaging bits and put them together from Speaker 1.")
378
-
379
- if st.button("Generate Edit"):
380
- if not ACTIVE_GEMINI_KEY:
381
- st.error("Gemini API Key required.")
382
- else:
383
- with st.spinner("Junior Editor is thinking..."):
384
- final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
385
-
386
- edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
387
- if edl_segments:
388
- if export_format == "EDL":
389
- final_output = generate_cmx_edl("Junior_Editor_Cut", edl_segments, final_source_name, fps)
390
- ext = "edl"
391
- else:
392
- final_output = generate_xml("Junior_Editor_Cut", edl_segments, final_source_name, fps)
393
- ext = "xml"
394
-
395
- st.subheader("Ready for Import")
396
- st.code(final_output, language="xml" if ext == "xml" else "text")
397
- st.download_button(f"Download .{ext.upper()}", data=final_output, file_name=f"junior_editor_cut.{ext}")
 
113
  return "\n".join(edl_lines)
114
 
115
  def generate_xml(sequence_name, segments, source_name, fps=25):
116
+ """Constructs a Final Cut Pro 7 XML with Video AND Audio Track 1."""
117
 
 
118
  xml_output = [
119
  '<?xml version="1.0" encoding="UTF-8"?>',
120
  '<!DOCTYPE xmeml>',
121
+ '<x