NickVerri commited on
Commit
3baa5bf
·
verified ·
1 Parent(s): ba719ed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +28 -54
app.py CHANGED
@@ -7,11 +7,12 @@ import requests
7
  import torch
8
  import numpy
9
  import torchaudio
 
10
  from datetime import timedelta
11
  from pyannote.audio import Pipeline
12
- from huggingface_hub import hf_hub_download
13
 
14
- # --- Safe Globals for PyTorch 2.6+ ---
15
  try:
16
  from pyannote.audio.core.task import Specifications, Problem, Resolution
17
  from pyannote.audio.core.model import Model
@@ -42,7 +43,7 @@ ACTIVE_HF_TOKEN = ENV_HF_TOKEN if ENV_HF_TOKEN else HARDCODED_HF_TOKEN
42
  ACTIVE_GEMINI_KEY = ENV_GEMINI_KEY if ENV_GEMINI_KEY else HARDCODED_GEMINI_KEY
43
 
44
  def format_timecode(seconds, fps=25):
45
- """Converts seconds to HH:MM:SS:FF for Resolve/Premiere."""
46
  td = timedelta(seconds=seconds)
47
  total_seconds = int(td.total_seconds())
48
  hours = total_seconds // 3600
@@ -76,7 +77,6 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
76
 
77
  url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
78
 
79
- # Updated system prompt to explicitly ignore interviewer comments
80
  system_prompt = (
81
  "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
82
  "(which includes Speaker IDs and word-level timestamps) to create a condensed story. "
@@ -88,19 +88,12 @@ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
88
  "4. TIMESTAMP INTEGRITY: Use only the exact word-level start and end times from the data."
89
  )
90
 
91
- # Cleaned up payload to prevent SyntaxErrors with f-strings
92
  prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
93
 
94
  payload = {
95
- "contents": [{
96
- "parts": [{"text": prompt_text}]
97
- }],
98
- "systemInstruction": {
99
- "parts": [{"text": system_prompt}]
100
- },
101
- "generationConfig": {
102
- "responseMimeType": "application/json"
103
- }
104
  }
105
 
106
  try:
@@ -121,7 +114,7 @@ with st.sidebar:
121
  fps = st.number_input("Timeline FPS", value=25)
122
 
123
  st.divider()
124
- st.info("API Keys and Tokens are managed via Environment Secrets for security.")
125
  if not ACTIVE_GEMINI_KEY:
126
  st.error("⚠️ Gemini API Key not found!")
127
  if not ACTIVE_HF_TOKEN:
@@ -134,18 +127,16 @@ if uploaded_file:
134
  if "transcript" not in st.session_state:
135
  if st.button("Step 1: Transcribe & Diarize"):
136
  if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
137
- st.error("Please provide a valid Hugging Face Token in Space Secrets or app.py.")
138
  else:
139
- status_container = st.empty()
140
- with status_container.container():
141
- st.write("🔄 **Processing Started...**")
142
 
143
- # Save local temp file
144
  with open("temp_input", "wb") as f:
145
  f.write(uploaded_file.getbuffer())
146
 
147
- st.write("🎵 **Extracting Audio (WAV)...**")
148
- # Force strict PCM 16kHz Mono for Pyannote compatibility
149
  subprocess.run([
150
  "ffmpeg", "-i", "temp_input",
151
  "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
@@ -153,63 +144,53 @@ if uploaded_file:
153
  ])
154
 
155
  # 1. Diarization
156
- st.write("🗣️ **Running Speaker Diarization...**")
157
  diarization = None
158
  try:
159
- # Download config manually
160
  config_path = hf_hub_download(
161
  repo_id="pyannote/speaker-diarization-3.1",
162
  filename="config.yaml",
163
  token=ACTIVE_HF_TOKEN
164
  )
165
-
166
  pipeline = Pipeline.from_pretrained(config_path)
167
 
168
- # Load audio manually into tensor for robust processing
169
  waveform, sample_rate = torchaudio.load("temp_audio.wav")
170
 
171
- # Ensure we are on GPU
172
  if torch.cuda.is_available():
173
- st.write("🚀 Using GPU for Diarization")
174
  pipeline.to(torch.device("cuda"))
175
  waveform = waveform.to(torch.device("cuda"))
176
-
177
- # Run pipeline by passing the tensor dictionary
178
- # This is the most reliable way to avoid file I/O issues in Docker
179
  diarization_output = pipeline({"waveform": waveform, "sample_rate": sample_rate})
180
 
181
- # Handle Wrapper
182
  if isinstance(diarization_output, tuple):
183
  diarization = diarization_output[0]
184
  else:
185
  diarization = diarization_output
186
 
187
- # CRITICAL FIX: Extract annotation from wrapper
188
  if not hasattr(diarization, "itertracks"):
189
  if hasattr(diarization_output, "annotation"):
190
  diarization = diarization_output.annotation
191
- elif hasattr(diarization_output, "get"):
192
- diarization = diarization_output.get("annotation", diarization_output)
193
 
194
  except Exception as e:
195
  st.error(f"Diarization Error: {e}")
196
  diarization = None
197
 
198
  # 2. Whisper Transcription
199
- st.write("📝 **Transcribing with Whisper...**")
200
- device = "cuda" if torch.cuda.is_available() else "cpu"
201
- model = whisper.load_model("medium", device=device)
202
  result = model.transcribe("temp_audio.wav", word_timestamps=True)
203
 
204
  # 3. Alignment
205
- st.write("🔗 **Aligning Speakers...**")
206
  final_segments = []
207
-
208
- # Pre-calculate turns
209
  speaker_turns = []
 
210
  if diarization:
211
  try:
212
- # Robust iteration check
213
  iterator = None
214
  if hasattr(diarization, 'itertracks'):
215
  iterator = diarization.itertracks(yield_label=True)
@@ -217,25 +198,21 @@ if uploaded_file:
217
  if iterator:
218
  for turn, _, speaker_id in iterator:
219
  speaker_turns.append({"start": turn.start, "end": turn.end, "speaker": speaker_id})
220
- st.write(f"✅ Found {len(speaker_turns)} speaker turns.")
221
- else:
222
- st.warning("⚠️ Pipeline ran but returned no tracks.")
223
  except Exception as e:
224
- st.error(f"Error iterating tracks: {e}")
225
 
226
  for segment in result['segments']:
227
  mid_time = (segment['start'] + segment['end']) / 2
228
  speaker = "Unknown"
229
 
230
- # Matching logic
231
  if speaker_turns:
232
- # 1. Strict
233
  for turn in speaker_turns:
234
  if turn["start"] <= mid_time <= turn["end"]:
235
  speaker = turn["speaker"]
236
  break
237
 
238
- # 2. Relaxed Fallback
239
  if speaker == "Unknown":
240
  best_dist = 1.0
241
  for turn in speaker_turns:
@@ -257,20 +234,17 @@ if uploaded_file:
257
 
258
  if "transcript" in st.session_state:
259
  st.divider()
260
-
261
- # Diarization Preview
262
  with st.expander("Transcript Preview (Diarized)"):
263
  for seg in st.session_state.transcript[:20]:
264
  st.markdown(f"**{seg['speaker']}:** {seg['text']}")
265
 
266
- brief = st.text_area("Creative Brief", placeholder="e.g. Focus on the yeast story, remove the interviewer.")
267
 
268
  if st.button("Step 2: Create EDL"):
269
  if not ACTIVE_GEMINI_KEY:
270
- st.error("Gemini API Key required. Please set it in Secrets or app.py.")
271
  else:
272
  with st.spinner("Analyzing..."):
273
- # Fixed missing parenthesis here
274
  edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
275
  if edl_segments:
276
  final_edl = generate_cmx_edl("AI_Senior_Editor_Cut", edl_segments, uploaded_file.name, fps)
 
7
  import torch
8
  import numpy
9
  import torchaudio
10
+ import torchaudio.transforms as T
11
  from datetime import timedelta
12
  from pyannote.audio import Pipeline
13
+ from huggingface_hub import login, hf_hub_download
14
 
15
+ # --- Safe Globals ---
16
  try:
17
  from pyannote.audio.core.task import Specifications, Problem, Resolution
18
  from pyannote.audio.core.model import Model
 
43
  ACTIVE_GEMINI_KEY = ENV_GEMINI_KEY if ENV_GEMINI_KEY else HARDCODED_GEMINI_KEY
44
 
45
  def format_timecode(seconds, fps=25):
46
+ """Converts seconds to HH:MM:SS:FF."""
47
  td = timedelta(seconds=seconds)
48
  total_seconds = int(td.total_seconds())
49
  hours = total_seconds // 3600
 
77
 
78
  url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
79
 
 
80
  system_prompt = (
81
  "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
82
  "(which includes Speaker IDs and word-level timestamps) to create a condensed story. "
 
88
  "4. TIMESTAMP INTEGRITY: Use only the exact word-level start and end times from the data."
89
  )
90
 
 
91
  prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
92
 
93
  payload = {
94
+ "contents": [{"parts": [{"text": prompt_text}]}],
95
+ "systemInstruction": {"parts": [{"text": system_prompt}]},
96
+ "generationConfig": {"responseMimeType": "application/json"}
 
 
 
 
 
 
97
  }
98
 
99
  try:
 
114
  fps = st.number_input("Timeline FPS", value=25)
115
 
116
  st.divider()
117
+ st.info("API Keys are managed via Environment Secrets.")
118
  if not ACTIVE_GEMINI_KEY:
119
  st.error("⚠️ Gemini API Key not found!")
120
  if not ACTIVE_HF_TOKEN:
 
127
  if "transcript" not in st.session_state:
128
  if st.button("Step 1: Transcribe & Diarize"):
129
  if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
130
+ st.error("Please provide a valid Hugging Face Token.")
131
  else:
132
+ with st.spinner("Processing... This may take a moment."):
133
+ # Authenticate Globally
134
+ login(token=ACTIVE_HF_TOKEN)
135
 
 
136
  with open("temp_input", "wb") as f:
137
  f.write(uploaded_file.getbuffer())
138
 
139
+ # Convert to strict WAV using FFmpeg
 
140
  subprocess.run([
141
  "ffmpeg", "-i", "temp_input",
142
  "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
 
144
  ])
145
 
146
  # 1. Diarization
 
147
  diarization = None
148
  try:
149
+ # Load Config Manually
150
  config_path = hf_hub_download(
151
  repo_id="pyannote/speaker-diarization-3.1",
152
  filename="config.yaml",
153
  token=ACTIVE_HF_TOKEN
154
  )
 
155
  pipeline = Pipeline.from_pretrained(config_path)
156
 
157
+ # Load Audio into Memory (Fix for file read issues)
158
  waveform, sample_rate = torchaudio.load("temp_audio.wav")
159
 
160
+ # Move to GPU if available
161
  if torch.cuda.is_available():
 
162
  pipeline.to(torch.device("cuda"))
163
  waveform = waveform.to(torch.device("cuda"))
164
+
165
+ # Run Pipeline on Tensor
 
166
  diarization_output = pipeline({"waveform": waveform, "sample_rate": sample_rate})
167
 
168
+ # Unwrap result
169
  if isinstance(diarization_output, tuple):
170
  diarization = diarization_output[0]
171
  else:
172
  diarization = diarization_output
173
 
174
+ # Extract Annotation object
175
  if not hasattr(diarization, "itertracks"):
176
  if hasattr(diarization_output, "annotation"):
177
  diarization = diarization_output.annotation
 
 
178
 
179
  except Exception as e:
180
  st.error(f"Diarization Error: {e}")
181
  diarization = None
182
 
183
  # 2. Whisper Transcription
184
+ model = whisper.load_model("medium", device="cuda" if torch.cuda.is_available() else "cpu")
 
 
185
  result = model.transcribe("temp_audio.wav", word_timestamps=True)
186
 
187
  # 3. Alignment
 
188
  final_segments = []
 
 
189
  speaker_turns = []
190
+
191
  if diarization:
192
  try:
193
+ # Iterate safely
194
  iterator = None
195
  if hasattr(diarization, 'itertracks'):
196
  iterator = diarization.itertracks(yield_label=True)
 
198
  if iterator:
199
  for turn, _, speaker_id in iterator:
200
  speaker_turns.append({"start": turn.start, "end": turn.end, "speaker": speaker_id})
 
 
 
201
  except Exception as e:
202
+ print(f"Error iterating tracks: {e}")
203
 
204
  for segment in result['segments']:
205
  mid_time = (segment['start'] + segment['end']) / 2
206
  speaker = "Unknown"
207
 
 
208
  if speaker_turns:
209
+ # Match speaker
210
  for turn in speaker_turns:
211
  if turn["start"] <= mid_time <= turn["end"]:
212
  speaker = turn["speaker"]
213
  break
214
 
215
+ # Fallback distance matching
216
  if speaker == "Unknown":
217
  best_dist = 1.0
218
  for turn in speaker_turns:
 
234
 
235
  if "transcript" in st.session_state:
236
  st.divider()
 
 
237
  with st.expander("Transcript Preview (Diarized)"):
238
  for seg in st.session_state.transcript[:20]:
239
  st.markdown(f"**{seg['speaker']}:** {seg['text']}")
240
 
241
+ brief = st.text_area("Creative Brief", placeholder="e.g. Focus on the yeast story.")
242
 
243
  if st.button("Step 2: Create EDL"):
244
  if not ACTIVE_GEMINI_KEY:
245
+ st.error("Gemini API Key required.")
246
  else:
247
  with st.spinner("Analyzing..."):
 
248
  edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
249
  if edl_segments:
250
  final_edl = generate_cmx_edl("AI_Senior_Editor_Cut", edl_segments, uploaded_file.name, fps)