NickVerri commited on
Commit
8a23baa
·
verified ·
1 Parent(s): a40c33b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -81
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} AX V C {src_in} {src_out} {rec_in} {rec_out}")
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="DocAI Editor (WhisperX)", layout="wide")
159
- st.title("Documentary AI: Pipeline (WhisperX)")
 
 
 
 
 
 
 
 
 
 
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 st.button("Step 1: Transcribe & Diarize"):
193
- if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
194
- st.error("Please provide a valid Hugging Face Token.")
195
- else:
196
- status_container = st.empty()
197
- with status_container.container():
198
- st.write("🔄 **Processing Started...**")
199
-
 
 
200
  with open("temp_input", "wb") as f:
201
  f.write(uploaded_file.getbuffer())
202
 
203
- st.write("🎵 **Extracting Audio (WAV 16k Mono)...**")
 
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
- try:
211
- device = "cuda" if torch.cuda.is_available() else "cpu"
212
- if device == "cpu":
213
- st.warning("⚠️ No GPU detected. WhisperX will be very slow.")
214
- else:
215
- st.write(f"🚀 **Loading WhisperX on {device}...**")
216
-
217
- # 1. Transcribe
218
- compute_type = "float16" if device == "cuda" else "int8"
219
- model = whisperx.load_model(model_size, device, compute_type=compute_type)
220
-
221
- st.write("📝 **Transcribing...**")
222
- audio = whisperx.load_audio("temp_audio.wav")
223
- result = model.transcribe(audio, batch_size=16, language=target_language)
224
-
225
- # Memory cleanup
226
- del model
227
- gc.collect()
228
- torch.cuda.empty_cache()
229
-
230
- # 2. Align
231
- st.write("⏱️ **Aligning Audio...**")
232
- model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=device)
233
- result = whisperx.align(result["segments"], model_a, metadata, audio, device, return_char_alignments=False)
234
-
235
- del model_a
236
- gc.collect()
237
- torch.cuda.empty_cache()
238
-
239
- # 3. Diarize
240
- st.write("🗣️ **Diarizing Speakers...**")
241
- diarize_model = whisperx.DiarizationPipeline(use_auth_token=ACTIVE_HF_TOKEN, device=device)
242
-
243
- diarize_kwargs = {}
244
- if num_speakers > 0:
245
- diarize_kwargs = {"min_speakers": num_speakers, "max_speakers": num_speakers}
246
-
247
- diarize_segments = diarize_model(audio, **diarize_kwargs)
248
-
249
- # 4. Final Merge
250
- st.write("🔗 **Merging Transcripts...**")
251
- final_result = whisperx.assign_word_speakers(diarize_segments, result)
252
-
253
- processed_segments = []
254
- for segment in final_result["segments"]:
255
- processed_segments.append({
256
- "speaker": segment.get("speaker", "Unknown"),
257
- "text": segment["text"].strip(),
258
- "start": segment["start"],
259
- "end": segment["end"]
260
- })
261
-
262
- st.session_state.transcript = processed_segments
263
- st.success(f"Complete! Found {len(processed_segments)} segments.")
 
264
 
265
- except Exception as e:
266
- st.error(f"Processing Error: {e}")
267
- # Clean up temp files
268
- if os.path.exists("temp_input"): os.remove("temp_input")
269
- if os.path.exists("temp_audio.wav"): os.remove("temp_audio.wav")
270
- st.stop()
271
 
 
272
  if "transcript" in st.session_state:
273
  st.divider()
274
- with st.expander("Transcript Preview (Diarized)"):
275
- for seg in st.session_state.transcript[:20]:
276
  st.markdown(f"**{seg['speaker']}:** {seg['text']}")
277
 
278
- brief = st.text_area("Creative Brief", placeholder="e.g. Focus on the yeast story.")
 
279
 
280
- if st.button("Step 2: Create EDL"):
281
  if not ACTIVE_GEMINI_KEY:
282
- st.error("Gemini API Key required.")
283
  else:
284
- with st.spinner("Analyzing..."):
 
 
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("AI_Senior_Editor_Cut", edl_segments, uploaded_file.name, fps)
288
- st.subheader("EDL Preview")
289
  st.code(final_edl, language="text")
290
- st.download_button("Download EDL", data=final_edl, file_name="edit.edl")
 
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")