Update app.py
Browse files
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 |
-
|
|
|
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 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 |
-
|
| 115 |
-
|
|
|
|
|
|
|
| 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}
|
| 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 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
| 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>
|
| 201 |
-
lines.append(f'\t\t\t\t\t\t\t<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(
|
| 205 |
-
lines.append(
|
| 206 |
-
lines.append(
|
| 207 |
-
lines.append(
|
| 208 |
-
lines.append(
|
| 209 |
-
lines.append(
|
| 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 |
-
|
| 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<
|
| 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(
|
| 250 |
-
|
| 251 |
-
|
| 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 |
|