NickVerri commited on
Commit
2eee88b
·
verified ·
1 Parent(s): e9228af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -94
app.py CHANGED
@@ -25,7 +25,6 @@ except Exception:
25
  pass
26
 
27
  if not hasattr(torch.load, "_is_patched"):
28
- print("DEBUG: Applying Monkeypatch to torch.load")
29
  _original_torch_load = torch.load
30
 
31
  def patched_torch_load(*args, **kwargs):
@@ -82,7 +81,8 @@ ACTIVE_GEMINI_KEY = ENV_GEMINI_KEY if ENV_GEMINI_KEY else HARDCODED_GEMINI_KEY
82
  def clean_json_response(text):
83
  """Strips markdown code fences from Gemini output to prevent JSON errors."""
84
  try:
85
- pattern = r"```(?:json)?\s*(.*?)```"
 
86
  match = re.search(pattern, text, re.DOTALL)
87
  if match:
88
  return match.group(1).strip()
@@ -90,34 +90,44 @@ def clean_json_response(text):
90
  except Exception:
91
  return text
92
 
 
 
 
 
 
 
 
 
 
93
  def format_timecode(seconds, fps=25):
94
  """Converts seconds to HH:MM:SS:FF (for EDL)."""
95
- td = timedelta(seconds=seconds)
96
- total_seconds = int(td.total_seconds())
97
- hours = total_seconds // 3600
98
- minutes = (total_seconds % 3600) // 60
99
- secs = total_seconds % 60
100
- frames = int((seconds - total_seconds) * fps)
101
  return f"{hours:02}:{minutes:02}:{secs:02}:{frames:02}"
102
 
103
  def seconds_to_frames(seconds, fps):
104
  """Converts seconds to integer frames (for XML)."""
105
  return int(round(seconds * fps))
106
 
107
- def generate_cmx_edl(edl_title, segments, source_name, fps=25):
108
- """Constructs a CMX 3600 formatted EDL."""
109
  edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
110
  rec_start = 0.0
111
- reel_id = source_name.replace(" ", "_")
112
 
113
  for i, seg in enumerate(segments, 1):
114
- src_in = format_timecode(seg['src_start'], fps)
115
- src_out = format_timecode(seg['src_end'], fps)
 
 
116
  duration = seg['src_end'] - seg['src_start']
117
  rec_in = format_timecode(rec_start, fps)
118
  rec_out = format_timecode(rec_start + duration, fps)
119
 
120
- edl_lines.append(f"{i:03} {reel_id} V C {src_in} {src_out} {rec_in} {rec_out}")
121
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
122
  edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
123
 
@@ -126,19 +136,16 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
126
 
127
  return "\n".join(edl_lines)
128
 
129
- def generate_xml(sequence_name, segments, source_name, fps=25):
130
- """Constructs a Robust XML for Premiere AND Resolve."""
131
-
132
- # Static UUID ensures all cuts link to the SAME file object (Clean Bin)
133
  master_id = "masterfile-1"
134
-
135
- # FPS Logic
136
  timebase = int(round(fps))
137
  is_ntsc = "TRUE" if fps % 1 != 0 else "FALSE"
138
 
139
- # --- NAME & PATH LOGIC ---
140
- # We use URL encoding but NO .mov extension.
141
- # This allows Resolve to find it by name, and Premiere to import it as "Offline".
 
142
  encoded_name = quote(source_name)
143
  path_url = f"file://localhost/{encoded_name}"
144
 
@@ -169,8 +176,8 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
169
 
170
  timeline_head_frames = 0
171
  for i, seg in enumerate(segments, 1):
172
- src_in_frames = seconds_to_frames(seg['src_start'], fps)
173
- src_out_frames = seconds_to_frames(seg['src_end'], fps)
174
  duration_frames = src_out_frames - src_in_frames
175
 
176
  tl_start = timeline_head_frames
@@ -181,7 +188,7 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
181
 
182
  lines.append(f'\t\t\t\t<clipitem id="clipitem-v-{i}">')
183
  lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
184
- lines.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
185
  lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
186
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
187
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
@@ -197,26 +204,18 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
197
  lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
198
  lines.append(f'\t\t\t\t\t\t<timecode>')
199
  lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
200
- lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
201
- lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
202
  lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
203
  lines.append(f'\t\t\t\t\t\t</timecode>')
204
- lines.append(f'\t\t\t\t\t\t<media>')
205
- lines.append(f'\t\t\t\t\t\t\t<video>')
206
- lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics>')
207
- lines.append(f'\t\t\t\t\t\t\t</video>')
208
- lines.append(f'\t\t\t\t\t\t\t<audio>')
209
- lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><depth>16</depth><samplerate>48000</samplerate></samplecharacteristics>')
210
- lines.append(f'\t\t\t\t\t\t\t\t<channelcount>2</channelcount>')
211
- lines.append(f'\t\t\t\t\t\t\t</audio>')
212
- lines.append(f'\t\t\t\t\t\t</media>')
213
- lines.append(f'\t\t\t\t\t</file>')
214
- # -----------------------------------
215
-
216
- lines.append(f'\t\t\t\t</clipitem>')
217
 
218
- gap_seconds = seg.get('gap', 0.0)
219
- gap_frames = seconds_to_frames(gap_seconds, fps)
220
  timeline_head_frames = tl_end + gap_frames
221
 
222
  lines.append('\t\t\t</track>')
@@ -224,64 +223,26 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
224
 
225
  # --- AUDIO TRACK ---
226
  lines.append('\t\t<audio>')
227
- lines.append('\t\t\t<track>')
228
-
229
  timeline_head_frames = 0
230
  for i, seg in enumerate(segments, 1):
231
- src_in_frames = seconds_to_frames(seg['src_start'], fps)
232
- src_out_frames = seconds_to_frames(seg['src_end'], fps)
233
  duration_frames = src_out_frames - src_in_frames
234
-
235
  tl_start = timeline_head_frames
236
  tl_end = tl_start + duration_frames
237
-
238
- raw_note = seg.get('note', 'Junior Editor Selection')
239
- clip_note = (raw_note[:75] + '..') if len(raw_note) > 75 else raw_note
240
 
241
  lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
242
- lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
243
- lines.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
244
  lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
245
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
246
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
247
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
248
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
249
- lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
250
-
251
- # Reuse same file definition (Verbose)
252
- lines.append(f'\t\t\t\t\t<file id="{master_id}">')
253
- lines.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
254
- lines.append(f'\t\t\t\t\t\t<pathurl>{path_url}</pathurl>')
255
- lines.append(f'\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
256
- lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
257
- lines.append(f'\t\t\t\t\t\t<timecode>')
258
- lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
259
- lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
260
- lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
261
- lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
262
- lines.append(f'\t\t\t\t\t\t</timecode>')
263
- lines.append(f'\t\t\t\t\t\t<media>')
264
- lines.append(f'\t\t\t\t\t\t\t<video>')
265
- lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics>')
266
- lines.append(f'\t\t\t\t\t\t\t</video>')
267
- lines.append(f'\t\t\t\t\t\t\t<audio>')
268
- lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><depth>16</depth><samplerate>48000</samplerate></samplecharacteristics>')
269
- lines.append(f'\t\t\t\t\t\t\t\t<channelcount>2</channelcount>')
270
- lines.append(f'\t\t\t\t\t\t\t</audio>')
271
- lines.append(f'\t\t\t\t\t\t</media>')
272
- lines.append(f'\t\t\t\t\t</file>')
273
-
274
- # SOURCE TRACK MAPPING (Simple Mono)
275
- lines.append('\t\t\t\t\t<sourcetrack>')
276
- lines.append('\t\t\t\t\t\t<mediatype>audio</mediatype>')
277
- lines.append('\t\t\t\t\t\t<trackindex>1</trackindex>')
278
- lines.append('\t\t\t\t\t</sourcetrack>')
279
-
280
- lines.append(f'\t\t\t\t</clipitem>')
281
-
282
- gap_seconds = seg.get('gap', 0.0)
283
- gap_frames = seconds_to_frames(gap_seconds, fps)
284
- timeline_head_frames = tl_end + gap_frames
285
 
286
  lines.append('\t\t\t</track>')
287
  lines.append('\t\t</audio>')
@@ -337,7 +298,7 @@ st.title("Junior Editor")
337
  st.markdown("""
338
  **Instructions**
339
  * Upload your file here (video or audio).
340
- * Set your timeline FPS and transcription quality.
341
  * Junior Editor will transcribe and separate speakers. You can then instruct it to find engaging bits or construct a narrative.
342
  * It will create an EDL or XML to import back into your editing software (Resolve, Premiere).
343
  * **For Multicam Workflows:** Use the **XML** option in the sidebar and enter the **exact name** of your Multicam Sequence.
@@ -352,12 +313,15 @@ with st.sidebar:
352
  fps_options = [23.98, 24, 25, 29.97, 30, 50, 59.94, 60]
353
  fps = st.selectbox("Timeline FPS", fps_options, index=2)
354
 
 
 
 
 
355
  st.header("Export Settings")
356
  export_format = st.radio("Output Format", ["EDL", "XML (Multicam)"], index=0)
357
 
358
  input_label = "EDL Reel Name"
359
  input_help = "Leave empty to use the uploaded file name."
360
-
361
  if export_format == "XML (Multicam)":
362
  input_label = "Multicam Sequence Name"
363
  input_help = "EXACT name of your Multicam Clip in Resolve."
@@ -490,18 +454,19 @@ if uploaded_file:
490
  else:
491
  with st.spinner("Junior Editor is thinking..."):
492
  final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
 
 
493
 
494
  edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
495
  if edl_segments:
496
  if export_format == "EDL":
497
- final_output = generate_cmx_edl("Junior_Editor_Cut", edl_segments, final_source_name, fps)
498
  ext = "edl"
499
  else:
500
- final_output = generate_xml("Junior_Editor_Cut", edl_segments, final_source_name, fps)
501
  ext = "xml"
502
 
503
  st.subheader("Ready for Import")
504
- # --- COLLAPSED DEFAULT ---
505
  with st.expander("Show XML/EDL Code", expanded=False):
506
  st.code(final_output, language="xml" if ext == "xml" else "text")
507
 
 
25
  pass
26
 
27
  if not hasattr(torch.load, "_is_patched"):
 
28
  _original_torch_load = torch.load
29
 
30
  def patched_torch_load(*args, **kwargs):
 
81
  def clean_json_response(text):
82
  """Strips markdown code fences from Gemini output to prevent JSON errors."""
83
  try:
84
+ # Avoid backtick parsing issues by splitting the string definition
85
+ pattern = r"`{3}(?:json)?\s*(.*?)`{3}"
86
  match = re.search(pattern, text, re.DOTALL)
87
  if match:
88
  return match.group(1).strip()
 
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):
122
+ # Add offset to source time
123
+ src_in = format_timecode(seg['src_start'] + start_offset, fps)
124
+ src_out = format_timecode(seg['src_end'] + start_offset, fps)
125
+
126
  duration = seg['src_end'] - seg['src_start']
127
  rec_in = format_timecode(rec_start, fps)
128
  rec_out = format_timecode(rec_start + duration, fps)
129
 
130
+ edl_lines.append(f"{i:03} {reel_id:8} V C {src_in} {src_out} {rec_in} {rec_out}")
131
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
132
  edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
133
 
 
136
 
137
  return "\n".join(edl_lines)
138
 
139
+ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0):
140
+ """Constructs a Robust XML with source timecode offset for Premiere AND Resolve."""
 
 
141
  master_id = "masterfile-1"
 
 
142
  timebase = int(round(fps))
143
  is_ntsc = "TRUE" if fps % 1 != 0 else "FALSE"
144
 
145
+ start_offset_frames = seconds_to_frames(start_offset, fps)
146
+ start_tc_string = format_timecode(start_offset, fps)
147
+
148
+ # We use URL encoding but NO .mov extension to allow Resolve to find it by name.
149
  encoded_name = quote(source_name)
150
  path_url = f"file://localhost/{encoded_name}"
151
 
 
176
 
177
  timeline_head_frames = 0
178
  for i, seg in enumerate(segments, 1):
179
+ src_in_frames = seconds_to_frames(seg['src_start'], fps) + start_offset_frames
180
+ src_out_frames = seconds_to_frames(seg['src_end'], fps) + start_offset_frames
181
  duration_frames = src_out_frames - src_in_frames
182
 
183
  tl_start = timeline_head_frames
 
188
 
189
  lines.append(f'\t\t\t\t<clipitem id="clipitem-v-{i}">')
190
  lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
191
+ lines.append(f'\t\t\t\t\t<duration>{duration_frames + src_in_frames}</duration>')
192
  lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
193
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
194
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
 
204
  lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
205
  lines.append(f'\t\t\t\t\t\t<timecode>')
206
  lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
207
+ lines.append(f'\t\t\t\t\t\t\t<string>{start_tc_string}</string>')
208
+ lines.append(f'\t\t\t\t\t\t\t<frame>{start_offset_frames}</frame>')
209
  lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
210
  lines.append(f'\t\t\t\t\t\t</timecode>')
211
+ lines.append('\t\t\t\t\t\t<media>')
212
+ lines.append('\t\t\t\t\t\t\t<video><samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics></video>')
213
+ lines.append('\t\t\t\t\t\t\t<audio><samplecharacteristics><depth>16</depth><samplerate>48000</samplerate></samplecharacteristics><channelcount>2</channelcount></audio>')
214
+ lines.append('\t\t\t\t\t\t</media>')
215
+ lines.append('\t\t\t\t\t</file>')
216
+ lines.append('\t\t\t\t</clipitem>')
 
 
 
 
 
 
 
217
 
218
+ gap_frames = seconds_to_frames(seg.get('gap', 0.0), fps)
 
219
  timeline_head_frames = tl_end + gap_frames
220
 
221
  lines.append('\t\t\t</track>')
 
223
 
224
  # --- AUDIO TRACK ---
225
  lines.append('\t\t<audio>')
226
+ lines.append('\t\t\t<track>')
227
+
228
  timeline_head_frames = 0
229
  for i, seg in enumerate(segments, 1):
230
+ src_in_frames = seconds_to_frames(seg['src_start'], fps) + start_offset_frames
231
+ src_out_frames = seconds_to_frames(seg['src_end'], fps) + start_offset_frames
232
  duration_frames = src_out_frames - src_in_frames
 
233
  tl_start = timeline_head_frames
234
  tl_end = tl_start + duration_frames
 
 
 
235
 
236
  lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
237
+ lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
 
238
  lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
239
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
240
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
241
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
242
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
243
+ lines.append('\t\t\t\t\t<sourcetrack><mediatype>audio</mediatype><trackindex>1</trackindex></sourcetrack>')
244
+ lines.append('\t\t\t\t</clipitem>')
245
+ timeline_head_frames = tl_end + seconds_to_frames(seg.get('gap', 0.0), fps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  lines.append('\t\t\t</track>')
248
  lines.append('\t\t</audio>')
 
298
  st.markdown("""
299
  **Instructions**
300
  * Upload your file here (video or audio).
301
+ * Set your timeline FPS, Source Timecode, and transcription quality in the sidebar.
302
  * Junior Editor will transcribe and separate speakers. You can then instruct it to find engaging bits or construct a narrative.
303
  * It will create an EDL or XML to import back into your editing software (Resolve, Premiere).
304
  * **For Multicam Workflows:** Use the **XML** option in the sidebar and enter the **exact name** of your Multicam Sequence.
 
313
  fps_options = [23.98, 24, 25, 29.97, 30, 50, 59.94, 60]
314
  fps = st.selectbox("Timeline FPS", fps_options, index=2)
315
 
316
+ st.header("Source Timecode")
317
+ st.info("Set this if your source file should reference a specific time-of-day timecode (e.g. for MP3s).")
318
+ source_start_tc = st.text_input("Source Start Timecode", value="00:00:00:00", help="Format: HH:MM:SS:FF")
319
+
320
  st.header("Export Settings")
321
  export_format = st.radio("Output Format", ["EDL", "XML (Multicam)"], index=0)
322
 
323
  input_label = "EDL Reel Name"
324
  input_help = "Leave empty to use the uploaded file name."
 
325
  if export_format == "XML (Multicam)":
326
  input_label = "Multicam Sequence Name"
327
  input_help = "EXACT name of your Multicam Clip in Resolve."
 
454
  else:
455
  with st.spinner("Junior Editor is thinking..."):
456
  final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
457
+ # Parse the offset from sidebar input
458
+ offset_sec = timecode_to_seconds(source_start_tc, fps)
459
 
460
  edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
461
  if edl_segments:
462
  if export_format == "EDL":
463
+ final_output = generate_cmx_edl("Junior_Editor_Cut", edl_segments, final_source_name, fps, offset_sec)
464
  ext = "edl"
465
  else:
466
+ final_output = generate_xml("Junior_Editor_Cut", edl_segments, final_source_name, fps, offset_sec)
467
  ext = "xml"
468
 
469
  st.subheader("Ready for Import")
 
470
  with st.expander("Show XML/EDL Code", expanded=False):
471
  st.code(final_output, language="xml" if ext == "xml" else "text")
472