NickVerri commited on
Commit
97cf94e
·
verified ·
1 Parent(s): 0f4ee1b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -20
app.py CHANGED
@@ -10,6 +10,8 @@ import requests
10
  import gc
11
  import math
12
  import uuid
 
 
13
  from datetime import timedelta
14
 
15
  # --- CRITICAL ENVIRONMENT FIXES ---
@@ -77,6 +79,18 @@ ACTIVE_GEMINI_KEY = ENV_GEMINI_KEY if ENV_GEMINI_KEY else HARDCODED_GEMINI_KEY
77
 
78
  # --- HELPER FUNCTIONS ---
79
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def format_timecode(seconds, fps=25):
81
  """Converts seconds to HH:MM:SS:FF (for EDL)."""
82
  td = timedelta(seconds=seconds)
@@ -114,10 +128,19 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
114
  return "\n".join(edl_lines)
115
 
116
  def generate_xml(sequence_name, segments, source_name, fps=25):
117
- """Constructs a Hybrid XML compatible with both Premiere and Resolve."""
118
 
 
119
  master_id = "masterfile-1"
120
 
 
 
 
 
 
 
 
 
121
  lines = []
122
  lines.append('<?xml version="1.0" encoding="UTF-8"?>')
123
  lines.append('<!DOCTYPE xmeml>')
@@ -125,8 +148,8 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
125
  lines.append('<sequence>')
126
  lines.append(f'\t<name>{sequence_name}</name>')
127
  lines.append('\t<rate>')
128
- lines.append(f'\t\t<timebase>{int(fps)}</timebase>')
129
- lines.append(f'\t\t<ntsc>{str(fps % 1 != 0).upper()}</ntsc>')
130
  lines.append('\t</rate>')
131
  lines.append('\t<media>')
132
 
@@ -134,7 +157,7 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
134
  lines.append('\t\t<video>')
135
  lines.append('\t\t\t<format>')
136
  lines.append('\t\t\t\t<samplecharacteristics>')
137
- lines.append(f'\t\t\t\t\t<rate><timebase>{int(fps)}</timebase></rate>')
138
  lines.append('\t\t\t\t\t<width>1920</width>')
139
  lines.append('\t\t\t\t\t<height>1080</height>')
140
  lines.append('\t\t\t\t\t<anamorphic>FALSE</anamorphic>')
@@ -152,35 +175,34 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
152
  tl_start = timeline_head_frames
153
  tl_end = tl_start + duration_frames
154
 
 
155
  raw_note = seg.get('note', 'Junior Editor Selection')
156
  clip_note = (raw_note[:75] + '..') if len(raw_note) > 75 else raw_note
157
 
158
  lines.append(f'\t\t\t\t<clipitem id="clipitem-v-{i}">')
159
  lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
160
  lines.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
161
- lines.append(f'\t\t\t\t\t<rate><timebase>{int(fps)}</timebase></rate>')
162
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
163
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
164
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
165
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
166
  lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
167
 
168
- # --- UNIVERSAL FILE DEFINITION ---
169
- # 1. <pathurl>: Used by Resolve to find the file.
170
- # 2. <media>: Used by Premiere to understand the file structure.
171
  lines.append(f'\t\t\t\t\t<file id="{master_id}">')
172
  lines.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
173
- lines.append(f'\t\t\t\t\t\t<pathurl>file://localhost/{source_name}.mov</pathurl>')
174
- lines.append(f'\t\t\t\t\t\t<rate><timebase>{int(fps)}</timebase></rate>')
175
  lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
176
  lines.append(f'\t\t\t\t\t\t<timecode>')
177
- lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{int(fps)}</timebase></rate>')
178
  lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
179
  lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
180
  lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
181
  lines.append(f'\t\t\t\t\t\t</timecode>')
182
-
183
- # --- MEDIA BLOCK (Required by Premiere) ---
184
  lines.append(f'\t\t\t\t\t\t<media>')
185
  lines.append(f'\t\t\t\t\t\t\t<video>')
186
  lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics>')
@@ -190,9 +212,9 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
190
  lines.append(f'\t\t\t\t\t\t\t\t<channelcount>2</channelcount>')
191
  lines.append(f'\t\t\t\t\t\t\t</audio>')
192
  lines.append(f'\t\t\t\t\t\t</media>')
193
- # ------------------------------------------
194
-
195
  lines.append(f'\t\t\t\t\t</file>')
 
 
196
  lines.append(f'\t\t\t\t</clipitem>')
197
 
198
  gap_seconds = seg.get('gap', 0.0)
@@ -221,15 +243,35 @@ def generate_xml(sequence_name, segments, source_name, fps=25):
221
  lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
222
  lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
223
  lines.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
224
- lines.append(f'\t\t\t\t\t<rate><timebase>{int(fps)}</timebase></rate>')
225
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
226
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
227
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
228
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
229
  lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
230
 
231
- # Reuse same file definition
232
- lines.append(f'\t\t\t\t\t<file id="{master_id}"/>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  # SOURCE TRACK MAPPING (Simple Mono)
235
  lines.append('\t\t\t\t\t<sourcetrack>')
@@ -283,7 +325,9 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
283
  res = requests.post(url, json=payload)
284
  res.raise_for_status()
285
  result_json = res.json()
286
- return json.loads(result_json['candidates'][0]['content']['parts'][0]['text'])
 
 
287
  except Exception as e:
288
  st.error(f"Senior Editor AI Error: {e}")
289
  return None
@@ -305,7 +349,7 @@ st.divider()
305
 
306
  with st.sidebar:
307
  st.header("Project Settings")
308
- fps = st.number_input("Timeline FPS", value=25)
309
 
310
  st.header("Export Settings")
311
  export_format = st.radio("Output Format", ["EDL", "XML (Multicam)"], index=0)
 
10
  import gc
11
  import math
12
  import uuid
13
+ import re
14
+ from urllib.parse import quote
15
  from datetime import timedelta
16
 
17
  # --- CRITICAL ENVIRONMENT FIXES ---
 
79
 
80
  # --- HELPER FUNCTIONS ---
81
 
82
+ def clean_json_response(text):
83
+ """Strips markdown code fences from Gemini output to prevent JSON errors."""
84
+ try:
85
+ # Remove ```json ... ``` or just ``` ... ```
86
+ pattern = r"```(?:json)?\s*(.*?)```"
87
+ match = re.search(pattern, text, re.DOTALL)
88
+ if match:
89
+ return match.group(1).strip()
90
+ return text.strip()
91
+ except Exception:
92
+ return text
93
+
94
  def format_timecode(seconds, fps=25):
95
  """Converts seconds to HH:MM:SS:FF (for EDL)."""
96
  td = timedelta(seconds=seconds)
 
128
  return "\n".join(edl_lines)
129
 
130
  def generate_xml(sequence_name, segments, source_name, fps=25):
131
+ """Constructs a Robust XML for Premiere AND Resolve."""
132
 
133
+ # Static UUID to ensure all cuts are seen as the same master clip
134
  master_id = "masterfile-1"
135
 
136
+ # FPS Logic (Fix for 29.97 vs 30 issues)
137
+ timebase = int(round(fps))
138
+ is_ntsc = "TRUE" if fps % 1 != 0 else "FALSE"
139
+
140
+ # URL Encode the filename to handle spaces (Critical for Premiere)
141
+ encoded_name = quote(source_name)
142
+ path_url = f"file://localhost/{encoded_name}.mov"
143
+
144
  lines = []
145
  lines.append('<?xml version="1.0" encoding="UTF-8"?>')
146
  lines.append('<!DOCTYPE xmeml>')
 
148
  lines.append('<sequence>')
149
  lines.append(f'\t<name>{sequence_name}</name>')
150
  lines.append('\t<rate>')
151
+ lines.append(f'\t\t<timebase>{timebase}</timebase>')
152
+ lines.append(f'\t\t<ntsc>{is_ntsc}</ntsc>')
153
  lines.append('\t</rate>')
154
  lines.append('\t<media>')
155
 
 
157
  lines.append('\t\t<video>')
158
  lines.append('\t\t\t<format>')
159
  lines.append('\t\t\t\t<samplecharacteristics>')
160
+ lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
161
  lines.append('\t\t\t\t\t<width>1920</width>')
162
  lines.append('\t\t\t\t\t<height>1080</height>')
163
  lines.append('\t\t\t\t\t<anamorphic>FALSE</anamorphic>')
 
175
  tl_start = timeline_head_frames
176
  tl_end = tl_start + duration_frames
177
 
178
+ # Truncate clip names for Premiere safety (max 80 chars)
179
  raw_note = seg.get('note', 'Junior Editor Selection')
180
  clip_note = (raw_note[:75] + '..') if len(raw_note) > 75 else raw_note
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>')
188
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
189
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
190
  lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
191
 
192
+ # --- FULL ROBUST FILE DEFINITION ---
193
+ # We repeat the full definition every time. It's verbose, but it's the only way
194
+ # to guarantee Premiere sees the metadata AND Resolve sees the path.
195
  lines.append(f'\t\t\t\t\t<file id="{master_id}">')
196
  lines.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
197
+ lines.append(f'\t\t\t\t\t\t<pathurl>{path_url}</pathurl>')
198
+ lines.append(f'\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
199
  lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
200
  lines.append(f'\t\t\t\t\t\t<timecode>')
201
+ lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
202
  lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
203
  lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
204
  lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
205
  lines.append(f'\t\t\t\t\t\t</timecode>')
 
 
206
  lines.append(f'\t\t\t\t\t\t<media>')
207
  lines.append(f'\t\t\t\t\t\t\t<video>')
208
  lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics>')
 
212
  lines.append(f'\t\t\t\t\t\t\t\t<channelcount>2</channelcount>')
213
  lines.append(f'\t\t\t\t\t\t\t</audio>')
214
  lines.append(f'\t\t\t\t\t\t</media>')
 
 
215
  lines.append(f'\t\t\t\t\t</file>')
216
+ # -----------------------------------
217
+
218
  lines.append(f'\t\t\t\t</clipitem>')
219
 
220
  gap_seconds = seg.get('gap', 0.0)
 
243
  lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
244
  lines.append(f'\t\t\t\t\t<name>{clip_note}</name>')
245
  lines.append(f'\t\t\t\t\t<duration>{duration_frames}</duration>')
246
+ lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
247
  lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
248
  lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
249
  lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
250
  lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
251
  lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
252
 
253
+ # Reuse same file definition (Verbose)
254
+ lines.append(f'\t\t\t\t\t<file id="{master_id}">')
255
+ lines.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
256
+ lines.append(f'\t\t\t\t\t\t<pathurl>{path_url}</pathurl>')
257
+ lines.append(f'\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
258
+ lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
259
+ lines.append(f'\t\t\t\t\t\t<timecode>')
260
+ lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
261
+ lines.append(f'\t\t\t\t\t\t\t<string>00:00:00:00</string>')
262
+ lines.append(f'\t\t\t\t\t\t\t<frame>0</frame>')
263
+ lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
264
+ lines.append(f'\t\t\t\t\t\t</timecode>')
265
+ lines.append(f'\t\t\t\t\t\t<media>')
266
+ lines.append(f'\t\t\t\t\t\t\t<video>')
267
+ lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics>')
268
+ lines.append(f'\t\t\t\t\t\t\t</video>')
269
+ lines.append(f'\t\t\t\t\t\t\t<audio>')
270
+ lines.append(f'\t\t\t\t\t\t\t\t<samplecharacteristics><depth>16</depth><samplerate>48000</samplerate></samplecharacteristics>')
271
+ lines.append(f'\t\t\t\t\t\t\t\t<channelcount>2</channelcount>')
272
+ lines.append(f'\t\t\t\t\t\t\t</audio>')
273
+ lines.append(f'\t\t\t\t\t\t</media>')
274
+ lines.append(f'\t\t\t\t\t</file>')
275
 
276
  # SOURCE TRACK MAPPING (Simple Mono)
277
  lines.append('\t\t\t\t\t<sourcetrack>')
 
325
  res = requests.post(url, json=payload)
326
  res.raise_for_status()
327
  result_json = res.json()
328
+ raw_text = result_json['candidates'][0]['content']['parts'][0]['text']
329
+ cleaned_text = clean_json_response(raw_text)
330
+ return json.loads(cleaned_text)
331
  except Exception as e:
332
  st.error(f"Senior Editor AI Error: {e}")
333
  return None
 
349
 
350
  with st.sidebar:
351
  st.header("Project Settings")
352
+ fps = st.number_input("Timeline FPS", value=25.0, format="%.3f")
353
 
354
  st.header("Export Settings")
355
  export_format = st.radio("Output Format", ["EDL", "XML (Multicam)"], index=0)