Update app.py
Browse files
app.py
CHANGED
|
@@ -179,6 +179,7 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0)
|
|
| 179 |
lines.append('\t\t\t</format>')
|
| 180 |
lines.append('\t\t\t<track>')
|
| 181 |
|
|
|
|
| 182 |
timeline_head_frames = 0
|
| 183 |
for i, seg in enumerate(segments, 1):
|
| 184 |
src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
|
|
@@ -195,8 +196,8 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0)
|
|
| 195 |
clip_note = (raw_note[:75] + '..') if len(raw_note) > 75 else raw_note
|
| 196 |
|
| 197 |
lines.append(f'\t\t\t\t<clipitem id="clipitem-v-{i}">')
|
| 198 |
-
lines.append(f'\t\t\t\t\t<name>{
|
| 199 |
-
lines.append(f'\t\t\t\t\t<duration>
|
| 200 |
lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 201 |
lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
|
| 202 |
lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
|
|
@@ -205,22 +206,33 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0)
|
|
| 205 |
lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
|
| 206 |
|
| 207 |
# --- ROBUST FILE DEFINITION ---
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
lines.append('\t\t\t\t</clipitem>')
|
| 225 |
|
| 226 |
gap_frames = seconds_to_frames(float(seg.get('gap', 0.0)), fps)
|
|
@@ -245,12 +257,15 @@ def generate_xml(sequence_name, segments, source_name, fps=25, start_offset=0.0)
|
|
| 245 |
tl_end = tl_start + duration_frames
|
| 246 |
|
| 247 |
lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
|
|
|
|
| 248 |
lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
|
|
|
|
| 249 |
lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 250 |
lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
|
| 251 |
lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
|
| 252 |
lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
|
| 253 |
lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
|
|
|
|
| 254 |
lines.append('\t\t\t\t\t<sourcetrack><mediatype>audio</mediatype><trackindex>1</trackindex></sourcetrack>')
|
| 255 |
lines.append('\t\t\t\t</clipitem>')
|
| 256 |
timeline_head_frames = tl_end + seconds_to_frames(float(seg.get('gap', 0.0)), fps)
|
|
@@ -311,8 +326,8 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
|
|
| 311 |
"gemini-2.5-pro"
|
| 312 |
]
|
| 313 |
|
| 314 |
-
# Try the
|
| 315 |
-
endpoints_to_try = ["
|
| 316 |
last_error = ""
|
| 317 |
|
| 318 |
for model_name in models_to_try:
|
|
@@ -327,13 +342,13 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
|
|
| 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 |
|
| 332 |
# Handle High Demand (503) or Rate Limits (429)
|
| 333 |
elif res.status_code in [503, 429]:
|
| 334 |
last_error = f"Model {model_name} busy ({res.status_code}) on {api_version}."
|
| 335 |
st.warning(f"Debug: {last_error} Skipping to next model...")
|
| 336 |
-
break # Break inner loop: No point trying
|
| 337 |
|
| 338 |
# Handle endpoints that don't exist (404) or API rejections (400)
|
| 339 |
else:
|
|
@@ -343,7 +358,7 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
|
|
| 343 |
error_details = res.text
|
| 344 |
last_error = f"Model {model_name} rejected request ({res.status_code}) on {api_version}: {error_details}"
|
| 345 |
st.warning(f"Debug: {last_error} Trying fallback endpoint/model...")
|
| 346 |
-
continue # Try
|
| 347 |
|
| 348 |
except json.JSONDecodeError:
|
| 349 |
last_error = f"Model {model_name} returned invalid JSON on {api_version}."
|
|
@@ -360,7 +375,7 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
|
|
| 360 |
|
| 361 |
# If the loop finishes without returning, all models failed
|
| 362 |
st.error(f"All Gemini models failed or are currently experiencing high demand. Last error: {last_error}")
|
| 363 |
-
return None
|
| 364 |
|
| 365 |
# --- Streamlit UI ---
|
| 366 |
st.set_page_config(page_title="Junior Editor", layout="wide")
|
|
@@ -525,21 +540,28 @@ if uploaded_file:
|
|
| 525 |
# Parse the offset from sidebar input
|
| 526 |
offset_sec = timecode_to_seconds(source_start_tc, fps)
|
| 527 |
|
| 528 |
-
edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
|
|
|
|
| 529 |
if edl_segments:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
if export_format == "EDL":
|
| 531 |
-
final_output = generate_cmx_edl(
|
| 532 |
ext = "edl"
|
| 533 |
else:
|
| 534 |
-
final_output = generate_xml(
|
| 535 |
ext = "xml"
|
| 536 |
|
| 537 |
st.subheader("Ready for Import")
|
|
|
|
|
|
|
| 538 |
with st.expander("Show XML/EDL Code", expanded=False):
|
| 539 |
st.code(final_output, language="xml" if ext == "xml" else "text")
|
| 540 |
|
| 541 |
-
st.download_button(f"Download .{ext.upper()}", data=final_output, file_name=f"
|
| 542 |
-
|
| 543 |
-
|
| 544 |
|
| 545 |
|
|
|
|
| 179 |
lines.append('\t\t\t</format>')
|
| 180 |
lines.append('\t\t\t<track>')
|
| 181 |
|
| 182 |
+
file_defined = False
|
| 183 |
timeline_head_frames = 0
|
| 184 |
for i, seg in enumerate(segments, 1):
|
| 185 |
src_start_val = float(seg.get('src_start', seg.get('start', 0.0)))
|
|
|
|
| 196 |
clip_note = (raw_note[:75] + '..') if len(raw_note) > 75 else raw_note
|
| 197 |
|
| 198 |
lines.append(f'\t\t\t\t<clipitem id="clipitem-v-{i}">')
|
| 199 |
+
lines.append(f'\t\t\t\t\t<name>{source_name}</name>')
|
| 200 |
+
lines.append(f'\t\t\t\t\t<duration>8640000</duration>')
|
| 201 |
lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 202 |
lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
|
| 203 |
lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
|
|
|
|
| 206 |
lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
|
| 207 |
|
| 208 |
# --- ROBUST FILE DEFINITION ---
|
| 209 |
+
if not file_defined:
|
| 210 |
+
lines.append(f'\t\t\t\t\t<file id="{master_id}">')
|
| 211 |
+
lines.append(f'\t\t\t\t\t\t<name>{source_name}</name>')
|
| 212 |
+
lines.append(f'\t\t\t\t\t\t<pathurl>{path_url}</pathurl>')
|
| 213 |
+
lines.append(f'\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 214 |
+
lines.append(f'\t\t\t\t\t\t<duration>8640000</duration>')
|
| 215 |
+
lines.append(f'\t\t\t\t\t\t<timecode>')
|
| 216 |
+
lines.append(f'\t\t\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 217 |
+
lines.append(f'\t\t\t\t\t\t\t<string>{start_tc_string}</string>')
|
| 218 |
+
lines.append(f'\t\t\t\t\t\t\t<frame>{start_offset_frames}</frame>')
|
| 219 |
+
lines.append(f'\t\t\t\t\t\t\t<displayformat>NDF</displayformat>')
|
| 220 |
+
lines.append(f'\t\t\t\t\t\t</timecode>')
|
| 221 |
+
lines.append('\t\t\t\t\t\t<media>')
|
| 222 |
+
lines.append('\t\t\t\t\t\t\t<video><samplecharacteristics><width>1920</width><height>1080</height></samplecharacteristics></video>')
|
| 223 |
+
lines.append('\t\t\t\t\t\t\t<audio><samplecharacteristics><depth>16</depth><samplerate>48000</samplerate></samplecharacteristics><channelcount>2</channelcount></audio>')
|
| 224 |
+
lines.append('\t\t\t\t\t\t</media>')
|
| 225 |
+
lines.append('\t\t\t\t\t</file>')
|
| 226 |
+
file_defined = True
|
| 227 |
+
else:
|
| 228 |
+
lines.append(f'\t\t\t\t\t<file id="{master_id}"/>')
|
| 229 |
+
|
| 230 |
+
lines.append('\t\t\t\t\t<marker>')
|
| 231 |
+
lines.append(f'\t\t\t\t\t\t<name>{clip_note}</name>')
|
| 232 |
+
lines.append(f'\t\t\t\t\t\t<in>{src_in_frames}</in>')
|
| 233 |
+
lines.append(f'\t\t\t\t\t\t<out>{src_in_frames}</out>')
|
| 234 |
+
lines.append('\t\t\t\t\t</marker>')
|
| 235 |
+
|
| 236 |
lines.append('\t\t\t\t</clipitem>')
|
| 237 |
|
| 238 |
gap_frames = seconds_to_frames(float(seg.get('gap', 0.0)), fps)
|
|
|
|
| 257 |
tl_end = tl_start + duration_frames
|
| 258 |
|
| 259 |
lines.append(f'\t\t\t\t<clipitem id="clipitem-a-{i}">')
|
| 260 |
+
lines.append(f'\t\t\t\t\t<name>{source_name}</name>')
|
| 261 |
lines.append(f'\t\t\t\t\t<masterclipid>{master_id}</masterclipid>')
|
| 262 |
+
lines.append(f'\t\t\t\t\t<duration>8640000</duration>')
|
| 263 |
lines.append(f'\t\t\t\t\t<rate><timebase>{timebase}</timebase></rate>')
|
| 264 |
lines.append(f'\t\t\t\t\t<start>{tl_start}</start>')
|
| 265 |
lines.append(f'\t\t\t\t\t<end>{tl_end}</end>')
|
| 266 |
lines.append(f'\t\t\t\t\t<in>{src_in_frames}</in>')
|
| 267 |
lines.append(f'\t\t\t\t\t<out>{src_out_frames}</out>')
|
| 268 |
+
lines.append(f'\t\t\t\t\t<file id="{master_id}"/>')
|
| 269 |
lines.append('\t\t\t\t\t<sourcetrack><mediatype>audio</mediatype><trackindex>1</trackindex></sourcetrack>')
|
| 270 |
lines.append('\t\t\t\t</clipitem>')
|
| 271 |
timeline_head_frames = tl_end + seconds_to_frames(float(seg.get('gap', 0.0)), fps)
|
|
|
|
| 326 |
"gemini-2.5-pro"
|
| 327 |
]
|
| 328 |
|
| 329 |
+
# Try the v1beta endpoint first as it natively supports systemInstruction and responseMimeType in REST
|
| 330 |
+
endpoints_to_try = ["v1beta", "v1"]
|
| 331 |
last_error = ""
|
| 332 |
|
| 333 |
for model_name in models_to_try:
|
|
|
|
| 342 |
result_json = res.json()
|
| 343 |
raw_text = result_json['candidates'][0]['content']['parts'][0]['text']
|
| 344 |
cleaned_text = clean_json_response(raw_text)
|
| 345 |
+
return json.loads(cleaned_text), model_name, api_version
|
| 346 |
|
| 347 |
# Handle High Demand (503) or Rate Limits (429)
|
| 348 |
elif res.status_code in [503, 429]:
|
| 349 |
last_error = f"Model {model_name} busy ({res.status_code}) on {api_version}."
|
| 350 |
st.warning(f"Debug: {last_error} Skipping to next model...")
|
| 351 |
+
break # Break inner loop: No point trying another endpoint if rate limited. Move to next model.
|
| 352 |
|
| 353 |
# Handle endpoints that don't exist (404) or API rejections (400)
|
| 354 |
else:
|
|
|
|
| 358 |
error_details = res.text
|
| 359 |
last_error = f"Model {model_name} rejected request ({res.status_code}) on {api_version}: {error_details}"
|
| 360 |
st.warning(f"Debug: {last_error} Trying fallback endpoint/model...")
|
| 361 |
+
continue # Try v1, or the next model
|
| 362 |
|
| 363 |
except json.JSONDecodeError:
|
| 364 |
last_error = f"Model {model_name} returned invalid JSON on {api_version}."
|
|
|
|
| 375 |
|
| 376 |
# If the loop finishes without returning, all models failed
|
| 377 |
st.error(f"All Gemini models failed or are currently experiencing high demand. Last error: {last_error}")
|
| 378 |
+
return None, None, None
|
| 379 |
|
| 380 |
# --- Streamlit UI ---
|
| 381 |
st.set_page_config(page_title="Junior Editor", layout="wide")
|
|
|
|
| 540 |
# Parse the offset from sidebar input
|
| 541 |
offset_sec = timecode_to_seconds(source_start_tc, fps)
|
| 542 |
|
| 543 |
+
edl_segments, used_model, used_endpoint = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
|
| 544 |
+
|
| 545 |
if edl_segments:
|
| 546 |
+
# Create a safe filename without special characters
|
| 547 |
+
safe_filename = "".join([c for c in final_source_name if c.isalnum() or c in (' ', '_', '-')]).strip()
|
| 548 |
+
safe_filename = safe_filename.replace(' ', '_')
|
| 549 |
+
if not safe_filename:
|
| 550 |
+
safe_filename = "junior_editor_cut"
|
| 551 |
+
|
| 552 |
if export_format == "EDL":
|
| 553 |
+
final_output = generate_cmx_edl(final_source_name, edl_segments, final_source_name, fps, offset_sec)
|
| 554 |
ext = "edl"
|
| 555 |
else:
|
| 556 |
+
final_output = generate_xml(final_source_name, edl_segments, final_source_name, fps, offset_sec)
|
| 557 |
ext = "xml"
|
| 558 |
|
| 559 |
st.subheader("Ready for Import")
|
| 560 |
+
st.success(f"✅ Edit generated successfully using **{used_model}** (via {used_endpoint}).")
|
| 561 |
+
|
| 562 |
with st.expander("Show XML/EDL Code", expanded=False):
|
| 563 |
st.code(final_output, language="xml" if ext == "xml" else "text")
|
| 564 |
|
| 565 |
+
st.download_button(f"Download .{ext.upper()}", data=final_output, file_name=f"{safe_filename}.{ext}")
|
|
|
|
|
|
|
| 566 |
|
| 567 |
|