NickVerri commited on
Commit
43ae3ee
·
verified ·
1 Parent(s): 6e44088

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -46
app.py CHANGED
@@ -90,32 +90,32 @@ def clean_json_response(text):
90
  except Exception:
91
  return text
92
 
93
- def timecode_to_seconds(tc, fps=25):
94
- """Converts HH:MM:SS:FF or HH:MM:SS.SS to total seconds."""
95
  if not tc or not re.match(r"\d{2}:\d{2}:\d{2}[:\.]\d{2}", tc):
96
- return 0.0
97
-
98
  parts = re.split(r'[:\.]', tc)
99
  h, m, s, f = map(int, parts)
100
- return h * 3600 + m * 60 + s + (f / fps)
 
101
 
102
- def format_timecode(seconds, fps=25):
103
- """Converts seconds to HH:MM:SS:FF (for EDL)."""
104
- total_frames = int(round(seconds * fps))
105
- hours = total_frames // (3600 * int(fps))
106
- minutes = (total_frames // (60 * int(fps))) % 60
107
- secs = (total_frames // int(fps)) % 60
108
- frames = total_frames % int(fps)
109
- return f"{hours:02}:{minutes:02}:{secs:02}:{frames:02}"
110
 
111
  def seconds_to_frames(seconds, fps):
112
- """Converts seconds to integer frames (for XML)."""
113
  return int(round(seconds * fps))
114
 
115
- def generate_cmx_edl(edl_title, segments, source_name, fps=25, start_offset=0.0):
116
  """Constructs a CMX 3600 formatted EDL with source offset."""
117
  edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
118
- rec_start = 0.0
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):
@@ -123,41 +123,44 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25, start_offset=0.0)
123
 
124
  if is_vo:
125
  vo_text = seg.get('text', 'VO')
126
- duration = float(seg.get('duration', max(1.0, len(vo_text.split()) / 2.5)))
127
- src_in = format_timecode(0, fps)
128
- src_out = format_timecode(duration, fps)
 
129
  current_source = "GEN_VO"
130
  note_text = f"VO SCRIPT: {vo_text}"
131
  else:
132
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
133
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
134
- src_in = format_timecode(src_start_val + start_offset, fps)
135
- src_out = format_timecode(src_end_val + start_offset, fps)
136
- duration = src_end_val - src_start_val
137
  current_source = reel_id
138
  note_text = seg.get('note', 'Clip')
 
 
 
139
 
140
- rec_in = format_timecode(rec_start, fps)
141
- rec_out = format_timecode(rec_start + duration, fps)
142
 
143
  edl_lines.append(f"{i:03} {current_source:8} V C {src_in} {src_out} {rec_in} {rec_out}")
144
  if not is_vo:
145
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
146
  edl_lines.append(f"* {note_text}\n")
147
 
148
- gap = float(seg.get('gap', 0.0))
149
- rec_start += duration + gap
150
 
151
  return "\n".join(edl_lines)
152
 
153
- def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0):
154
  """Constructs a Robust XML with source timecode offset for Premiere AND Resolve."""
155
  master_id = "masterfile-1"
156
  timebase = int(round(fps))
157
  is_ntsc = "TRUE" if fps % 1 != 0 else "FALSE"
158
 
159
- start_offset_frames = seconds_to_frames(start_offset, fps)
160
- start_tc_string = format_timecode(start_offset, fps)
161
 
162
  encoded_name = quote(source_name)
163
  path_url = f"file://localhost/{encoded_name}"
@@ -337,7 +340,7 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0)
337
 
338
  return "\n".join(lines)
339
 
340
- def generate_transcript_txt(sequence_name, segments, fps=25, start_offset=0.0, transcript_data=None):
341
  """Generates a human-readable text document of the edit decisions and exact transcription."""
342
  lines = [f"Sequence Transcript: {sequence_name}", "=" * 50, ""]
343
 
@@ -352,16 +355,17 @@ def generate_transcript_txt(sequence_name, segments, fps=25, start_offset=0.0, t
352
  else:
353
  src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
354
  src_end_val = float(seg.get('src_end', seg.get('end', 0.0)))
355
- src_in = format_timecode(src_start_val + start_offset, fps)
356
- src_out = format_timecode(src_end_val + start_offset, fps)
 
 
 
357
  note = seg.get('note', 'No note provided.')
358
 
359
- # Cross-reference the exact times with the original Whisper transcript to get the exact words
360
  actual_text = ""
361
  if transcript_data:
362
  clip_words = []
363
  for t_seg in transcript_data:
364
- # Check if the transcript segment overlaps with our clip's time window
365
  if max(src_start_val, t_seg['start']) < min(src_end_val, t_seg['end']):
366
  clip_words.append(t_seg['text'])
367
  actual_text = " ".join(clip_words).strip()
@@ -371,12 +375,12 @@ def generate_transcript_txt(sequence_name, segments, fps=25, start_offset=0.0, t
371
  if actual_text:
372
  lines.append(f"Exact Audio Text: \"{actual_text}\"")
373
 
374
- lines.append("") # Blank line to separate clips visually
375
 
376
  return "\n".join(lines)
377
 
378
 
379
- def call_gemini_for_edl(transcript_data, story_prompt, api_key):
380
  if not api_key:
381
  st.error("Gemini API Key is missing.")
382
  return None
@@ -399,7 +403,15 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
399
  "PACING: Group related clips. Add a 'gap' (in seconds) between distinct ideas."
400
  )
401
 
402
- prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
 
 
 
 
 
 
 
 
403
 
404
  payload = {
405
  "contents": [
@@ -637,7 +649,7 @@ if uploaded_file:
637
  else:
638
  with st.spinner("Junior Editor is thinking..."):
639
  final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
640
- offset_sec = timecode_to_seconds(source_start_tc, fps)
641
 
642
  edl_segments, used_model, used_endpoint = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
643
 
@@ -649,14 +661,14 @@ if uploaded_file:
649
 
650
  # Generate the selected edit format (EDL/XML)
651
  if export_format == "EDL":
652
- final_output = generate_cmx_edl(final_source_name, edl_segments, final_source_name, fps, offset_sec)
653
  ext = "edl"
654
  else:
655
- final_output = generate_xml(final_source_name, edl_segments, final_source_name, fps, offset_sec)
656
  ext = "xml"
657
 
658
- # Generate the companion human-readable transcript, passing the raw transcript data for exact word matching
659
- transcript_txt = generate_transcript_txt(final_source_name, edl_segments, fps, offset_sec, st.session_state.transcript)
660
 
661
  # Save to session state so buttons don't disappear on click
662
  st.session_state.edit_generated = True
@@ -666,6 +678,7 @@ if uploaded_file:
666
  st.session_state.safe_filename = safe_filename
667
  st.session_state.used_model = used_model
668
  st.session_state.used_endpoint = used_endpoint
 
669
 
670
  # Render the download buttons outside the Generate block using session state
671
  if st.session_state.get("edit_generated"):
@@ -679,9 +692,38 @@ if uploaded_file:
679
  with col2:
680
  st.download_button(f"Download Reference Transcript (.TXT)", data=st.session_state.transcript_txt, file_name=f"{st.session_state.safe_filename}-Transcript.txt")
681
 
682
- with st.expander(f"Show {st.session_state.ext.upper()} Code", expanded=False):
683
- st.code(st.session_state.final_output, language="xml" if st.session_state.ext == "xml" else "text")
 
684
 
685
- with st.expander("Show Transcript Reference", expanded=False):
686
- st.text(st.session_state.transcript_txt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
 
90
  except Exception:
91
  return text
92
 
93
+ def timecode_to_frames(tc, fps):
94
+ """Converts HH:MM:SS:FF to absolute integer frames based on timebase."""
95
  if not tc or not re.match(r"\d{2}:\d{2}:\d{2}[:\.]\d{2}", tc):
96
+ return 0
 
97
  parts = re.split(r'[:\.]', tc)
98
  h, m, s, f = map(int, parts)
99
+ timebase = int(round(fps)) # 29.97 becomes 30, 23.98 becomes 24
100
+ return (h * 3600 * timebase) + (m * 60 * timebase) + (s * timebase) + f
101
 
102
+ def frames_to_timecode(frames, fps):
103
+ """Converts absolute integer frames back to HH:MM:SS:FF (NDF)."""
104
+ timebase = int(round(fps))
105
+ hours = frames // (3600 * timebase)
106
+ minutes = (frames // (60 * timebase)) % 60
107
+ secs = (frames // timebase) % 60
108
+ f = frames % timebase
109
+ return f"{hours:02}:{minutes:02}:{secs:02}:{f:02}"
110
 
111
  def seconds_to_frames(seconds, fps):
112
+ """Converts real seconds to integer frames."""
113
  return int(round(seconds * fps))
114
 
115
+ def generate_cmx_edl(edl_title, segments, source_name, fps=25, start_offset_frames=0):
116
  """Constructs a CMX 3600 formatted EDL with source offset."""
117
  edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
118
+ rec_start_frames = 0
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):
 
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)))
135
+ src_in_frames = seconds_to_frames(src_start_val, fps) + start_offset_frames
136
+ src_out_frames = seconds_to_frames(src_end_val, fps) + start_offset_frames
137
+ duration_frames = src_out_frames - src_in_frames
138
  current_source = reel_id
139
  note_text = seg.get('note', 'Clip')
140
+
141
+ src_in = frames_to_timecode(src_in_frames, fps)
142
+ src_out = frames_to_timecode(src_out_frames, fps)
143
 
144
+ rec_in = frames_to_timecode(rec_start_frames, fps)
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
 
152
+ gap_frames = seconds_to_frames(float(seg.get('gap', 0.0)), fps)
153
+ rec_start_frames += duration_frames + gap_frames
154
 
155
  return "\n".join(edl_lines)
156
 
157
+ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset_frames=0):
158
  """Constructs a Robust XML with source timecode offset for Premiere AND Resolve."""
159
  master_id = "masterfile-1"
160
  timebase = int(round(fps))
161
  is_ntsc = "TRUE" if fps % 1 != 0 else "FALSE"
162
 
163
+ start_tc_string = frames_to_timecode(start_offset_frames, fps)
 
164
 
165
  encoded_name = quote(source_name)
166
  path_url = f"file://localhost/{encoded_name}"
 
340
 
341
  return "\n".join(lines)
342
 
343
+ def generate_transcript_txt(sequence_name, segments, fps=25, start_offset_frames=0, transcript_data=None):
344
  """Generates a human-readable text document of the edit decisions and exact transcription."""
345
  lines = [f"Sequence Transcript: {sequence_name}", "=" * 50, ""]
346
 
 
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)))
358
+ src_in_frames = seconds_to_frames(src_start_val, fps) + start_offset_frames
359
+ src_out_frames = seconds_to_frames(src_end_val, fps) + start_offset_frames
360
+
361
+ src_in = frames_to_timecode(src_in_frames, fps)
362
+ src_out = frames_to_timecode(src_out_frames, fps)
363
  note = seg.get('note', 'No note provided.')
364
 
 
365
  actual_text = ""
366
  if transcript_data:
367
  clip_words = []
368
  for t_seg in transcript_data:
 
369
  if max(src_start_val, t_seg['start']) < min(src_end_val, t_seg['end']):
370
  clip_words.append(t_seg['text'])
371
  actual_text = " ".join(clip_words).strip()
 
375
  if actual_text:
376
  lines.append(f"Exact Audio Text: \"{actual_text}\"")
377
 
378
+ lines.append("")
379
 
380
  return "\n".join(lines)
381
 
382
 
383
+ def call_gemini_for_edl(transcript_data, story_prompt, api_key, previous_edit=None):
384
  if not api_key:
385
  st.error("Gemini API Key is missing.")
386
  return None
 
403
  "PACING: Group related clips. Add a 'gap' (in seconds) between distinct ideas."
404
  )
405
 
406
+ if previous_edit:
407
+ prompt_text = (
408
+ f"Transcript Data:\n{json.dumps(transcript_data)}\n\n"
409
+ f"Previous Edit You Generated:\n{json.dumps(previous_edit)}\n\n"
410
+ f"Director's Note for Revision: {story_prompt}\n\n"
411
+ "Please output a NEW JSON array of segments applying these requested changes to the previous edit."
412
+ )
413
+ else:
414
+ prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
415
 
416
  payload = {
417
  "contents": [
 
649
  else:
650
  with st.spinner("Junior Editor is thinking..."):
651
  final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
652
+ offset_frames = timecode_to_frames(source_start_tc, fps)
653
 
654
  edl_segments, used_model, used_endpoint = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
655
 
 
661
 
662
  # Generate the selected edit format (EDL/XML)
663
  if export_format == "EDL":
664
+ final_output = generate_cmx_edl(final_source_name, edl_segments, final_source_name, fps, offset_frames)
665
  ext = "edl"
666
  else:
667
+ final_output = generate_xml(final_source_name, edl_segments, final_source_name, fps, offset_frames)
668
  ext = "xml"
669
 
670
+ # Generate the companion human-readable transcript
671
+ transcript_txt = generate_transcript_txt(final_source_name, edl_segments, fps, offset_frames, st.session_state.transcript)
672
 
673
  # Save to session state so buttons don't disappear on click
674
  st.session_state.edit_generated = True
 
678
  st.session_state.safe_filename = safe_filename
679
  st.session_state.used_model = used_model
680
  st.session_state.used_endpoint = used_endpoint
681
+ st.session_state.edl_segments = edl_segments
682
 
683
  # Render the download buttons outside the Generate block using session state
684
  if st.session_state.get("edit_generated"):
 
692
  with col2:
693
  st.download_button(f"Download Reference Transcript (.TXT)", data=st.session_state.transcript_txt, file_name=f"{st.session_state.safe_filename}-Transcript.txt")
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..."):
701
+ final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
702
+ offset_frames = timecode_to_frames(source_start_tc, fps)
703
+
704
+ new_edl_segments, used_model, used_endpoint = call_gemini_for_edl(
705
+ st.session_state.transcript,
706
+ revision_brief,
707
+ ACTIVE_GEMINI_KEY,
708
+ previous_edit=st.session_state.edl_segments
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
723
+ st.session_state.transcript_txt = transcript_txt
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