NickVerri commited on
Commit
73f85e4
·
verified ·
1 Parent(s): 43ae3ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -28
app.py CHANGED
@@ -119,16 +119,19 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25, start_offset_fram
119
  reel_id = source_name.replace(" ", "_")[:8] # EDL reel IDs are traditionally short max 8 chars
120
 
121
  for i, seg in enumerate(segments, 1):
122
- is_vo = seg.get('type') == 'vo' or 'text' in seg
 
 
123
 
124
- if is_vo:
125
- vo_text = seg.get('text', 'VO')
126
- duration_sec = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
 
127
  duration_frames = seconds_to_frames(duration_sec, fps)
128
  src_in_frames = 0
129
  src_out_frames = duration_frames
130
- current_source = "GEN_VO"
131
- note_text = f"VO SCRIPT: {vo_text}"
132
  else:
133
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
134
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
@@ -145,7 +148,7 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25, start_offset_fram
145
  rec_out = frames_to_timecode(rec_start_frames + duration_frames, fps)
146
 
147
  edl_lines.append(f"{i:03} {current_source:8} V C {src_in} {src_out} {rec_in} {rec_out}")
148
- if not is_vo:
149
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
150
  edl_lines.append(f"* {note_text}\n")
151
 
@@ -192,26 +195,31 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset_fram
192
 
193
  file_defined = False
194
  vo_file_defined = False
 
195
  timeline_head_frames = 0
196
 
197
  for i, seg in enumerate(segments, 1):
198
- is_vo = seg.get('type') == 'vo' or 'text' in seg
 
 
199
 
200
- if is_vo:
201
- vo_text = seg.get('text', 'VO')
202
- duration_sec = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
 
203
  duration_frames = seconds_to_frames(duration_sec, fps)
204
  src_in_frames = 0
205
  src_out_frames = duration_frames
206
- master_id_to_use = "masterfile-vo"
207
- source_name_to_use = "Generated_VO_Placeholder"
208
- clip_note = f"VO SCRIPT: {vo_text}"
 
209
  clip_name_disp = (clip_note[:75] + '..') if len(clip_note) > 75 else clip_note
210
  else:
211
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
212
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
213
- src_in_frames = seconds_to_frames(src_start_val, fps)
214
- src_out_frames = seconds_to_frames(src_end_val, fps)
215
  duration_frames = src_out_frames - src_in_frames
216
  master_id_to_use = master_id
217
  source_name_to_use = source_name
@@ -254,6 +262,27 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset_fram
254
  vo_file_defined = True
255
  else:
256
  lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}"/>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  else:
258
  if not file_defined:
259
  lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}">')
@@ -279,7 +308,13 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset_fram
279
  lines.append('\t\t\t\t\t<marker>')
280
  lines.append(f'\t\t\t\t\t\t<name>{clip_note}</name>')
281
  lines.append(f'\t\t\t\t\t\t<in>{src_in_frames}</in>')
282
- lines.append(f'\t\t\t\t\t\t<out>{src_in_frames}</out>')
 
 
 
 
 
 
283
  lines.append('\t\t\t\t\t</marker>')
284
 
285
  lines.append('\t\t\t\t</clipitem>')
@@ -296,8 +331,18 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset_fram
296
 
297
  timeline_head_frames = 0
298
  for i, seg in enumerate(segments, 1):
299
- is_vo = seg.get('type') == 'vo' or 'text' in seg
 
 
300
 
 
 
 
 
 
 
 
 
301
  if is_vo:
302
  vo_text = seg.get('text', 'VO')
303
  duration_sec = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
@@ -345,13 +390,20 @@ def generate_transcript_txt(sequence_name, segments, fps=25, start_offset_frames
345
  lines = [f"Sequence Transcript: {sequence_name}", "=" * 50, ""]
346
 
347
  for i, seg in enumerate(segments, 1):
348
- is_vo = seg.get('type') == 'vo' or 'text' in seg
 
 
349
 
350
  if is_vo:
351
  vo_text = seg.get('text', 'VO')
352
  duration = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
353
  lines.append(f"Clip {i:02} | [GENERATED VOICE-OVER] | Est. Duration: {duration:.1f}s")
354
  lines.append(f"Script: \"{vo_text}\"")
 
 
 
 
 
355
  else:
356
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
357
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
@@ -388,9 +440,11 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key, previous_edit=No
388
  system_prompt = (
389
  "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
390
  "(which includes Speaker IDs and word-level timestamps) to create a condensed story.\n\n"
391
- "NEW CAPABILITY: You can now act as a Writer/Director and generate Voice-Over (VO) to bridge gaps, introduce topics, or summarize. "
392
- "CRITICAL: ONLY generate 'vo' segments if the user explicitly requests voice over suggestions or narration in their creative brief. Otherwise, ONLY use 'clip' segments.\n\n"
393
- "Output ONLY a valid JSON array of segments. Every segment MUST be either a 'clip' or a 'vo'.\n\n"
 
 
394
  "For 'clip' segments (extracting from the subject):\n"
395
  "{\"type\": \"clip\", \"src_start\": 12.5, \"src_end\": 25.0, \"note\": \"Subject talks about X\", \"gap\": 0.0}\n"
396
  "- IGNORE ALL INTERVIEWER COMMENTS.\n"
@@ -400,6 +454,8 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key, previous_edit=No
400
  "{\"type\": \"vo\", \"text\": \"The journey began years ago...\", \"duration\": 3.5, \"gap\": 0.0}\n"
401
  "- DURATION: Estimate duration accurately based on reading speed (approx 2.5 words per second).\n"
402
  "- Keep VO concise and in the style of a documentary narrator.\n\n"
 
 
403
  "PACING: Group related clips. Add a 'gap' (in seconds) between distinct ideas."
404
  )
405
 
@@ -499,7 +555,7 @@ st.markdown("""
499
  * Upload your file here (video or audio).
500
  * Set your timeline FPS, Source Timecode, and transcription quality in the sidebar.
501
  * Junior Editor will transcribe and separate speakers. You can then instruct it to find engaging bits or construct a narrative.
502
- * **Note: If you want me to write voice over suggestions, please request it.**
503
  * It will create an EDL or XML to import back into your editing software (Resolve, Premiere).
504
  * **For Multicam Workflows:** Use the **XML** option in the sidebar and enter the **exact name** of your Multicam Sequence.
505
  * **Resolve Users:** Uncheck "Automatically import source clips into media pool" during import.
@@ -641,7 +697,7 @@ if uploaded_file:
641
  st.markdown(f"**{seg['speaker']}:** {seg['text']}")
642
 
643
  st.subheader("Your Instruction")
644
- brief = st.text_area("What should the Junior Editor do?", placeholder="e.g. Find the most engaging bits. If you want me to write voice over suggestions, please request it.")
645
 
646
  if st.button("Generate Edit"):
647
  if not ACTIVE_GEMINI_KEY:
@@ -672,6 +728,8 @@ if uploaded_file:
672
 
673
  # Save to session state so buttons don't disappear on click
674
  st.session_state.edit_generated = True
 
 
675
  st.session_state.final_output = final_output
676
  st.session_state.ext = ext
677
  st.session_state.transcript_txt = transcript_txt
@@ -694,7 +752,7 @@ if uploaded_file:
694
 
695
  st.divider()
696
  st.subheader("Revise Edit")
697
- revision_brief = st.text_area("Want changes? Tell Junior Editor:", placeholder="e.g. Make it twice as long, add more details about X, or write a VO intro.")
698
 
699
  if st.button("Apply Revisions"):
700
  with st.spinner("Junior Editor is revising the edit..."):
@@ -709,14 +767,18 @@ if uploaded_file:
709
  )
710
 
711
  if new_edl_segments:
 
 
 
 
712
  if export_format == "EDL":
713
- final_output = generate_cmx_edl(final_source_name, new_edl_segments, final_source_name, fps, offset_frames)
714
  ext = "edl"
715
  else:
716
- final_output = generate_xml(final_source_name, new_edl_segments, final_source_name, fps, offset_frames)
717
  ext = "xml"
718
 
719
- transcript_txt = generate_transcript_txt(final_source_name, new_edl_segments, fps, offset_frames, st.session_state.transcript)
720
 
721
  st.session_state.final_output = final_output
722
  st.session_state.ext = ext
@@ -724,6 +786,11 @@ if uploaded_file:
724
  st.session_state.used_model = used_model
725
  st.session_state.used_endpoint = used_endpoint
726
  st.session_state.edl_segments = new_edl_segments
 
 
 
 
727
  st.rerun()
728
 
729
 
 
 
119
  reel_id = source_name.replace(" ", "_")[:8] # EDL reel IDs are traditionally short max 8 chars
120
 
121
  for i, seg in enumerate(segments, 1):
122
+ seg_type = seg.get('type', 'clip')
123
+ is_vo = seg_type == 'vo' or ('text' in seg and seg_type != 'graphic')
124
+ is_graphic = seg_type == 'graphic'
125
 
126
+ if is_vo or is_graphic:
127
+ text_content = seg.get('text', 'Placeholder')
128
+ default_dur = max(1.0, len(text_content.split()) / 2.5) if is_vo else 4.0
129
+ duration_sec = float(seg.get('duration', default_dur))
130
  duration_frames = seconds_to_frames(duration_sec, fps)
131
  src_in_frames = 0
132
  src_out_frames = duration_frames
133
+ current_source = "GEN_VO" if is_vo else "GEN_GFX"
134
+ note_text = f"{'VO SCRIPT' if is_vo else 'GRAPHIC'}: {text_content}"
135
  else:
136
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
137
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
 
148
  rec_out = frames_to_timecode(rec_start_frames + duration_frames, fps)
149
 
150
  edl_lines.append(f"{i:03} {current_source:8} V C {src_in} {src_out} {rec_in} {rec_out}")
151
+ if not is_vo and not is_graphic:
152
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
153
  edl_lines.append(f"* {note_text}\n")
154
 
 
195
 
196
  file_defined = False
197
  vo_file_defined = False
198
+ gfx_file_defined = False
199
  timeline_head_frames = 0
200
 
201
  for i, seg in enumerate(segments, 1):
202
+ seg_type = seg.get('type', 'clip')
203
+ is_vo = seg_type == 'vo' or ('text' in seg and seg_type != 'graphic')
204
+ is_graphic = seg_type == 'graphic'
205
 
206
+ if is_vo or is_graphic:
207
+ text_content = seg.get('text', 'Placeholder')
208
+ default_dur = max(1.0, len(text_content.split()) / 2.5) if is_vo else 4.0
209
+ duration_sec = float(seg.get('duration', default_dur))
210
  duration_frames = seconds_to_frames(duration_sec, fps)
211
  src_in_frames = 0
212
  src_out_frames = duration_frames
213
+ master_id_to_use = "masterfile-vo" if is_vo else "masterfile-gfx"
214
+ source_name_to_use = "Generated_VO_Placeholder" if is_vo else "Generated_Graphic_Placeholder"
215
+ prefix = "VO SCRIPT" if is_vo else "GRAPHIC CARD"
216
+ clip_note = f"{prefix}: {text_content}"
217
  clip_name_disp = (clip_note[:75] + '..') if len(clip_note) > 75 else clip_note
218
  else:
219
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
220
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
221
+ src_in_frames = seconds_to_frames(src_start_val, fps) + start_offset_frames
222
+ src_out_frames = seconds_to_frames(src_end_val, fps) + start_offset_frames
223
  duration_frames = src_out_frames - src_in_frames
224
  master_id_to_use = master_id
225
  source_name_to_use = source_name
 
262
  vo_file_defined = True
263
  else:
264
  lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}"/>')
265
+ elif is_graphic:
266
+ if not gfx_file_defined:
267
+ lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}">')
268
+ lines.append(f'\t\t\t\t\t\t<name>{source_name_to_use}</name>')
269
+ lines.append(f'\t\t\t\t\t\t<pathurl>file://localhost/{source_name_to_use}.mov</pathurl>')
270
+ lines.append(f'\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
271
+ lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
272
+ lines.append(f'\t\t\t\t\t\t<timecode>')
273
+ lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
274
+ lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
275
+ lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
276
+ lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
277
+ lines.append(f'\t\t\t\t\t\t</timecode>')
278
+ lines.append('\t\t\t\t\t\t<media>')
279
+ lines.append('\t\t\t\t\t\t\t<video><samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics></video>')
280
+ # Intentionally omitting audio block for graphics placeholder to keep NLE track structure clean
281
+ lines.append('\t\t\t\t\t\t</media>')
282
+ lines.append('\t\t\t\t\t</file>')
283
+ gfx_file_defined = True
284
+ else:
285
+ lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}"/>')
286
  else:
287
  if not file_defined:
288
  lines.append(f'\t\t\t\t\t<file id="{master_id_to_use}">')
 
308
  lines.append('\t\t\t\t\t<marker>')
309
  lines.append(f'\t\t\t\t\t\t<name>{clip_note}</name>')
310
  lines.append(f'\t\t\t\t\t\t<in>{src_in_frames}</in>')
311
+
312
+ # Extends the marker duration to span the entire clip for VO and Graphics
313
+ if is_vo or is_graphic:
314
+ lines.append(f'\t\t\t\t\t\t<out>{src_out_frames}</out>')
315
+ else:
316
+ lines.append(f'\t\t\t\t\t\t<out>{src_in_frames}</out>')
317
+
318
  lines.append('\t\t\t\t\t</marker>')
319
 
320
  lines.append('\t\t\t\t</clipitem>')
 
331
 
332
  timeline_head_frames = 0
333
  for i, seg in enumerate(segments, 1):
334
+ seg_type = seg.get('type', 'clip')
335
+ is_vo = seg_type == 'vo' or ('text' in seg and seg_type != 'graphic')
336
+ is_graphic = seg_type == 'graphic'
337
 
338
+ if is_graphic:
339
+ # Graphics have no audio. We just advance the playhead so the timeline stays perfectly in sync!
340
+ duration_sec = float(seg.get('duration', 4.0))
341
+ duration_frames = seconds_to_frames(duration_sec, fps)
342
+ gap_frames = seconds_to_frames(float(seg.get('gap', 0.0)), fps)
343
+ timeline_head_frames += duration_frames + gap_frames
344
+ continue
345
+
346
  if is_vo:
347
  vo_text = seg.get('text', 'VO')
348
  duration_sec = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
 
390
  lines = [f"Sequence Transcript: {sequence_name}", "=" * 50, ""]
391
 
392
  for i, seg in enumerate(segments, 1):
393
+ seg_type = seg.get('type', 'clip')
394
+ is_vo = seg_type == 'vo' or ('text' in seg and seg_type != 'graphic')
395
+ is_graphic = seg_type == 'graphic'
396
 
397
  if is_vo:
398
  vo_text = seg.get('text', 'VO')
399
  duration = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
400
  lines.append(f"Clip {i:02} | [GENERATED VOICE-OVER] | Est. Duration: {duration:.1f}s")
401
  lines.append(f"Script: \"{vo_text}\"")
402
+ elif is_graphic:
403
+ gfx_text = seg.get('text', 'Graphic Text')
404
+ duration = float(seg.get('duration', 4.0))
405
+ lines.append(f"Clip {i:02} | [GRAPHIC CARD] | Est. Duration: {duration:.1f}s")
406
+ lines.append(f"Text: \"{gfx_text}\"")
407
  else:
408
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
409
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
 
440
  system_prompt = (
441
  "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
442
  "(which includes Speaker IDs and word-level timestamps) to create a condensed story.\n\n"
443
+ "NEW CAPABILITIES:\n"
444
+ "1. You can generate Voice-Over (VO) to bridge gaps, introduce topics, or summarize.\n"
445
+ "2. You can generate Graphic Cards (Title Cards) to display text on screen (e.g., location, date, or chapter title).\n"
446
+ "CRITICAL: ONLY generate 'vo' or 'graphic' segments if the user explicitly requests voice over, narration, or graphic/title cards in their creative brief. Otherwise, ONLY use 'clip' segments.\n\n"
447
+ "Output ONLY a valid JSON array of segments. Every segment MUST be a 'clip', a 'vo', or a 'graphic'.\n\n"
448
  "For 'clip' segments (extracting from the subject):\n"
449
  "{\"type\": \"clip\", \"src_start\": 12.5, \"src_end\": 25.0, \"note\": \"Subject talks about X\", \"gap\": 0.0}\n"
450
  "- IGNORE ALL INTERVIEWER COMMENTS.\n"
 
454
  "{\"type\": \"vo\", \"text\": \"The journey began years ago...\", \"duration\": 3.5, \"gap\": 0.0}\n"
455
  "- DURATION: Estimate duration accurately based on reading speed (approx 2.5 words per second).\n"
456
  "- Keep VO concise and in the style of a documentary narrator.\n\n"
457
+ "For 'graphic' segments (generating text on screen):\n"
458
+ "{\"type\": \"graphic\", \"text\": \"Chapter 1: The Beginning\", \"duration\": 4.0, \"gap\": 0.0}\n\n"
459
  "PACING: Group related clips. Add a 'gap' (in seconds) between distinct ideas."
460
  )
461
 
 
555
  * Upload your file here (video or audio).
556
  * Set your timeline FPS, Source Timecode, and transcription quality in the sidebar.
557
  * Junior Editor will transcribe and separate speakers. You can then instruct it to find engaging bits or construct a narrative.
558
+ * **Note: If you want me to write voice over suggestions or title cards, please request it.**
559
  * It will create an EDL or XML to import back into your editing software (Resolve, Premiere).
560
  * **For Multicam Workflows:** Use the **XML** option in the sidebar and enter the **exact name** of your Multicam Sequence.
561
  * **Resolve Users:** Uncheck "Automatically import source clips into media pool" during import.
 
697
  st.markdown(f"**{seg['speaker']}:** {seg['text']}")
698
 
699
  st.subheader("Your Instruction")
700
+ brief = st.text_area("What should the Junior Editor do?", placeholder="e.g. Find the most engaging bits. If you want me to write voice over suggestions or title cards, please request it.")
701
 
702
  if st.button("Generate Edit"):
703
  if not ACTIVE_GEMINI_KEY:
 
728
 
729
  # Save to session state so buttons don't disappear on click
730
  st.session_state.edit_generated = True
731
+ st.session_state.revision_count = 1
732
+ st.session_state.base_filename = safe_filename
733
  st.session_state.final_output = final_output
734
  st.session_state.ext = ext
735
  st.session_state.transcript_txt = transcript_txt
 
752
 
753
  st.divider()
754
  st.subheader("Revise Edit")
755
+ revision_brief = st.text_area("Want changes? Tell Junior Editor:", placeholder="e.g. Make it twice as long, add a graphic card for location, or write a VO intro.")
756
 
757
  if st.button("Apply Revisions"):
758
  with st.spinner("Junior Editor is revising the edit..."):
 
767
  )
768
 
769
  if new_edl_segments:
770
+ # Increment version count
771
+ st.session_state.revision_count = st.session_state.get("revision_count", 1) + 1
772
+ rev_seq_name = f"{final_source_name} V{st.session_state.revision_count}"
773
+
774
  if export_format == "EDL":
775
+ final_output = generate_cmx_edl(rev_seq_name, new_edl_segments, final_source_name, fps, offset_frames)
776
  ext = "edl"
777
  else:
778
+ final_output = generate_xml(rev_seq_name, new_edl_segments, final_source_name, fps, offset_frames)
779
  ext = "xml"
780
 
781
+ transcript_txt = generate_transcript_txt(rev_seq_name, new_edl_segments, fps, offset_frames, st.session_state.transcript)
782
 
783
  st.session_state.final_output = final_output
784
  st.session_state.ext = ext
 
786
  st.session_state.used_model = used_model
787
  st.session_state.used_endpoint = used_endpoint
788
  st.session_state.edl_segments = new_edl_segments
789
+
790
+ # Update safe filename for the download buttons to show _V2, _V3, etc.
791
+ st.session_state.safe_filename = f"{st.session_state.base_filename}_V{st.session_state.revision_count}"
792
+
793
  st.rerun()
794
 
795
 
796
+