Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -106,6 +106,10 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
|
|
| 106 |
"""Constructs a CMX 3600 formatted EDL."""
|
| 107 |
edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
|
| 108 |
rec_start = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
for i, seg in enumerate(segments, 1):
|
| 110 |
src_in = format_timecode(seg['src_start'], fps)
|
| 111 |
src_out = format_timecode(seg['src_end'], fps)
|
|
@@ -113,7 +117,7 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
|
|
| 113 |
rec_in = format_timecode(rec_start, fps)
|
| 114 |
rec_out = format_timecode(rec_start + duration, fps)
|
| 115 |
|
| 116 |
-
edl_lines.append(f"{i:03}
|
| 117 |
edl_lines.append(f"* FROM CLIP NAME: {source_name}")
|
| 118 |
edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
|
| 119 |
rec_start += duration
|
|
@@ -155,13 +159,30 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
|
|
| 155 |
return None
|
| 156 |
|
| 157 |
# --- Streamlit UI ---
|
| 158 |
-
st.set_page_config(page_title="
|
| 159 |
-
st.title("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
with st.sidebar:
|
| 162 |
st.header("Project Settings")
|
| 163 |
fps = st.number_input("Timeline FPS", value=25)
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
st.header("Model Settings")
|
| 166 |
model_size = st.selectbox("Whisper Model", ["large-v2", "medium", "base"], index=0)
|
| 167 |
|
|
@@ -188,103 +209,118 @@ with st.sidebar:
|
|
| 188 |
uploaded_file = st.file_uploader("Upload Video/Audio Clip", type=["mp4", "m4a", "wav", "mp3", "mov"])
|
| 189 |
|
| 190 |
if uploaded_file:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
if "transcript" not in st.session_state:
|
| 192 |
-
if
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
| 200 |
with open("temp_input", "wb") as f:
|
| 201 |
f.write(uploaded_file.getbuffer())
|
| 202 |
|
| 203 |
-
st.write("🎵 **
|
|
|
|
| 204 |
subprocess.run([
|
| 205 |
"ffmpeg", "-i", "temp_input",
|
| 206 |
"-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
|
| 207 |
"temp_audio.wav", "-y"
|
| 208 |
])
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
st.stop()
|
| 271 |
|
|
|
|
| 272 |
if "transcript" in st.session_state:
|
| 273 |
st.divider()
|
| 274 |
-
with st.expander("Transcript Preview
|
| 275 |
-
for seg in st.session_state.transcript
|
| 276 |
st.markdown(f"**{seg['speaker']}:** {seg['text']}")
|
| 277 |
|
| 278 |
-
|
|
|
|
| 279 |
|
| 280 |
-
if st.button("
|
| 281 |
if not ACTIVE_GEMINI_KEY:
|
| 282 |
-
st.error("Gemini API Key required.")
|
| 283 |
else:
|
| 284 |
-
with st.spinner("
|
|
|
|
|
|
|
| 285 |
edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
|
| 286 |
if edl_segments:
|
| 287 |
-
final_edl = generate_cmx_edl("
|
| 288 |
-
st.subheader("
|
| 289 |
st.code(final_edl, language="text")
|
| 290 |
-
st.download_button("Download EDL", data=final_edl, file_name="
|
|
|
|
| 106 |
"""Constructs a CMX 3600 formatted EDL."""
|
| 107 |
edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
|
| 108 |
rec_start = 0.0
|
| 109 |
+
|
| 110 |
+
# Sanitize source name for the Reel ID column
|
| 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)
|
|
|
|
| 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 |
rec_start += duration
|
|
|
|
| 159 |
return None
|
| 160 |
|
| 161 |
# --- Streamlit UI ---
|
| 162 |
+
st.set_page_config(page_title="Junior Editor", layout="wide")
|
| 163 |
+
st.title("Junior Editor")
|
| 164 |
+
|
| 165 |
+
st.markdown("""
|
| 166 |
+
**Instructions**
|
| 167 |
+
* Upload your file here, either an video file or audio.
|
| 168 |
+
* Set your timeline FPS, choose the quality of your transcription and ,if you know the language, set this to speed up the transcription phase.
|
| 169 |
+
* Junior Editor will transcribe it and separate by speakers and then await your instruction. You can ask it to find the most engaging bits and put them together from “Speaker 1”, remove all of Speaker 2, or construct a narrative around whatever idea you choose.
|
| 170 |
+
* It will create an EDL to import back into your editing software. If you want it to reference a Master Clip rather than the proxy, type this in the side bar at the start.
|
| 171 |
+
""")
|
| 172 |
+
|
| 173 |
+
st.divider()
|
| 174 |
|
| 175 |
with st.sidebar:
|
| 176 |
st.header("Project Settings")
|
| 177 |
fps = st.number_input("Timeline FPS", value=25)
|
| 178 |
|
| 179 |
+
st.info("💡 **Conform Helper**")
|
| 180 |
+
custom_reel_name = st.text_input(
|
| 181 |
+
"EDL Reel Name",
|
| 182 |
+
placeholder="Paste Raw File Name Here...",
|
| 183 |
+
help="Leave empty to use the uploaded file name. Use this to link proxies to original camera files."
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
st.header("Model Settings")
|
| 187 |
model_size = st.selectbox("Whisper Model", ["large-v2", "medium", "base"], index=0)
|
| 188 |
|
|
|
|
| 209 |
uploaded_file = st.file_uploader("Upload Video/Audio Clip", type=["mp4", "m4a", "wav", "mp3", "mov"])
|
| 210 |
|
| 211 |
if uploaded_file:
|
| 212 |
+
# --- Auto-Reset Logic for New Files ---
|
| 213 |
+
# If the user uploads a new file, we must clear the old transcript from memory
|
| 214 |
+
if "last_processed_file" not in st.session_state or st.session_state.last_processed_file != uploaded_file.name:
|
| 215 |
+
if "transcript" in st.session_state:
|
| 216 |
+
del st.session_state.transcript
|
| 217 |
+
st.session_state.last_processed_file = uploaded_file.name
|
| 218 |
+
|
| 219 |
+
# --- Auto-Process Logic ---
|
| 220 |
if "transcript" not in st.session_state:
|
| 221 |
+
if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
|
| 222 |
+
st.error("Please provide a valid Hugging Face Token in the Sidebar/Secrets.")
|
| 223 |
+
else:
|
| 224 |
+
status_container = st.empty()
|
| 225 |
+
with status_container.container():
|
| 226 |
+
st.info("🤖 **Junior Editor is starting...**")
|
| 227 |
+
progress_bar = st.progress(0)
|
| 228 |
+
|
| 229 |
+
try:
|
| 230 |
+
# Save File
|
| 231 |
with open("temp_input", "wb") as f:
|
| 232 |
f.write(uploaded_file.getbuffer())
|
| 233 |
|
| 234 |
+
st.write("🎵 **Phase 1/4: Extracting Audio...**")
|
| 235 |
+
progress_bar.progress(10)
|
| 236 |
subprocess.run([
|
| 237 |
"ffmpeg", "-i", "temp_input",
|
| 238 |
"-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
|
| 239 |
"temp_audio.wav", "-y"
|
| 240 |
])
|
| 241 |
|
| 242 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 243 |
+
if device == "cpu":
|
| 244 |
+
st.warning("⚠️ No GPU detected. This will be slow.")
|
| 245 |
+
|
| 246 |
+
# 1. Transcribe
|
| 247 |
+
st.write(f"📝 **Phase 2/4: Transcribing (Whisper {model_size})...**")
|
| 248 |
+
progress_bar.progress(30)
|
| 249 |
+
|
| 250 |
+
compute_type = "float16" if device == "cuda" else "int8"
|
| 251 |
+
model = whisperx.load_model(model_size, device, compute_type=compute_type)
|
| 252 |
+
audio = whisperx.load_audio("temp_audio.wav")
|
| 253 |
+
result = model.transcribe(audio, batch_size=16, language=target_language)
|
| 254 |
+
|
| 255 |
+
# Cleanup
|
| 256 |
+
del model
|
| 257 |
+
gc.collect()
|
| 258 |
+
torch.cuda.empty_cache()
|
| 259 |
+
|
| 260 |
+
# 2. Align
|
| 261 |
+
st.write("⏱️ **Phase 3/4: Aligning Text...**")
|
| 262 |
+
progress_bar.progress(60)
|
| 263 |
+
model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=device)
|
| 264 |
+
result = whisperx.align(result["segments"], model_a, metadata, audio, device, return_char_alignments=False)
|
| 265 |
+
|
| 266 |
+
del model_a
|
| 267 |
+
gc.collect()
|
| 268 |
+
torch.cuda.empty_cache()
|
| 269 |
+
|
| 270 |
+
# 3. Diarize
|
| 271 |
+
st.write("🗣️ **Phase 4/4: Identifying Speakers...**")
|
| 272 |
+
progress_bar.progress(80)
|
| 273 |
+
diarize_model = whisperx.DiarizationPipeline(use_auth_token=ACTIVE_HF_TOKEN, device=device)
|
| 274 |
+
|
| 275 |
+
diarize_kwargs = {}
|
| 276 |
+
if num_speakers > 0:
|
| 277 |
+
diarize_kwargs = {"min_speakers": num_speakers, "max_speakers": num_speakers}
|
| 278 |
+
|
| 279 |
+
diarize_segments = diarize_model(audio, **diarize_kwargs)
|
| 280 |
+
|
| 281 |
+
# 4. Final Merge
|
| 282 |
+
st.write("🔗 **Finalizing...**")
|
| 283 |
+
final_result = whisperx.assign_word_speakers(diarize_segments, result)
|
| 284 |
+
|
| 285 |
+
processed_segments = []
|
| 286 |
+
for segment in final_result["segments"]:
|
| 287 |
+
processed_segments.append({
|
| 288 |
+
"speaker": segment.get("speaker", "Unknown"),
|
| 289 |
+
"text": segment["text"].strip(),
|
| 290 |
+
"start": segment["start"],
|
| 291 |
+
"end": segment["end"]
|
| 292 |
+
})
|
| 293 |
+
|
| 294 |
+
st.session_state.transcript = processed_segments
|
| 295 |
+
progress_bar.progress(100)
|
| 296 |
+
st.success(f"Done! Processed {len(processed_segments)} segments.")
|
| 297 |
|
| 298 |
+
except Exception as e:
|
| 299 |
+
st.error(f"Error during processing: {e}")
|
| 300 |
+
if os.path.exists("temp_input"): os.remove("temp_input")
|
| 301 |
+
if os.path.exists("temp_audio.wav"): os.remove("temp_audio.wav")
|
| 302 |
+
st.stop()
|
|
|
|
| 303 |
|
| 304 |
+
# --- Display Results if Transcript Exists ---
|
| 305 |
if "transcript" in st.session_state:
|
| 306 |
st.divider()
|
| 307 |
+
with st.expander("Transcript Preview", expanded=True):
|
| 308 |
+
for seg in st.session_state.transcript:
|
| 309 |
st.markdown(f"**{seg['speaker']}:** {seg['text']}")
|
| 310 |
|
| 311 |
+
st.subheader("Your Instruction")
|
| 312 |
+
brief = st.text_area("What should the Junior Editor do?", placeholder="e.g. Find the most engaging bits and put them together from Speaker 1, or remove all of Speaker 2.")
|
| 313 |
|
| 314 |
+
if st.button("Generate Edit"):
|
| 315 |
if not ACTIVE_GEMINI_KEY:
|
| 316 |
+
st.error("Gemini API Key required in Secrets.")
|
| 317 |
else:
|
| 318 |
+
with st.spinner("Junior Editor is thinking..."):
|
| 319 |
+
final_source_name = custom_reel_name.strip() if custom_reel_name.strip() else uploaded_file.name
|
| 320 |
+
|
| 321 |
edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
|
| 322 |
if edl_segments:
|
| 323 |
+
final_edl = generate_cmx_edl("Junior_Editor_Cut", edl_segments, final_source_name, fps)
|
| 324 |
+
st.subheader("Ready for Import")
|
| 325 |
st.code(final_edl, language="text")
|
| 326 |
+
st.download_button("Download .EDL", data=final_edl, file_name="junior_editor_cut.edl")
|