NickVerri commited on
Commit
aaf23e4
·
verified ·
1 Parent(s): e5d8a97

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +55 -34
app.py CHANGED
@@ -8,10 +8,13 @@ import torch
8
  from datetime import timedelta
9
  from pyannote.audio import Pipeline
10
 
11
- # --- Configuration ---
12
- ENV_API_KEY = os.environ.get("GEMINI_API_KEY", "")
 
 
13
 
14
  def format_timecode(seconds, fps=25):
 
15
  td = timedelta(seconds=seconds)
16
  total_seconds = int(td.total_seconds())
17
  hours = total_seconds // 3600
@@ -21,6 +24,7 @@ def format_timecode(seconds, fps=25):
21
  return f"{hours:02}:{minutes:02}:{secs:02}:{frames:02}"
22
 
23
  def generate_cmx_edl(edl_title, segments, source_name, fps=25):
 
24
  edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
25
  rec_start = 0.0
26
  for i, seg in enumerate(segments, 1):
@@ -29,6 +33,7 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
29
  duration = seg['src_end'] - seg['src_start']
30
  rec_in = format_timecode(rec_start, fps)
31
  rec_out = format_timecode(rec_start + duration, fps)
 
32
  edl_lines.append(f"{i:03} AX V C {src_in} {src_out} {rec_in} {rec_out}")
33
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
34
  edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
@@ -36,70 +41,86 @@ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
36
  return "\n".join(edl_lines)
37
 
38
  def call_gemini_for_edl(transcript_data, story_prompt, api_key):
 
39
  if not api_key:
40
- st.error("Gemini API Key is missing. Please provide it in the sidebar.")
41
  return None
 
42
  url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
 
43
  system_prompt = (
44
- "You are an expert Documentary Senior Editor. Use the provided transcript JSON (which includes speaker IDs and word-level timestamps) to create a condensed story. "
 
45
  "Output ONLY a valid JSON array of segments with 'src_start', 'src_end', and 'note'. "
46
- "Remove fluff, repeats, and interviewer interruptions. Focus on the most engaging content."
47
  )
 
48
  payload = {
49
- "contents": [{"parts": [{"text": f"Brief: {story_prompt}\n\nTranscript:\n{json.dumps(transcript_data)}"}]}],
50
  "systemInstruction": {"parts": [{"text": system_prompt}]},
51
  "generationConfig": {"responseMimeType": "application/json"}
52
  }
 
53
  try:
54
  res = requests.post(url, json=payload)
55
  res.raise_for_status()
56
  return json.loads(res.json()['candidates'][0]['content']['parts'][0]['text'])
57
  except Exception as e:
58
- st.error(f"Error calling Gemini: {e}")
59
  return None
60
 
61
- # --- App Interface ---
62
  st.set_page_config(page_title="DocAI Editor", layout="wide")
63
- st.title("Documentary AI: Senior Editor Pipeline")
64
 
65
  with st.sidebar:
66
  st.header("Settings")
67
- user_key = st.text_input("Gemini API Key", type="password")
68
- hf_token = st.text_input("Hugging Face Token (for Diarization)", type="password", help="Required for pyannote models.")
69
- active_api_key = user_key if user_key else ENV_API_KEY
 
 
70
  fps = st.number_input("Timeline FPS", value=25)
71
 
72
- uploaded_file = st.file_uploader("Upload Video/Audio", type=["mp4", "m4a", "wav", "mp3", "mov"])
73
 
74
  if uploaded_file:
 
75
  if "transcript" not in st.session_state:
76
  if st.button("Step 1: Transcribe & Diarize"):
77
- if not hf_token:
78
- st.error("Please provide a Hugging Face Token in the sidebar for diarization.")
79
  else:
80
- with st.spinner("Processing... This includes audio extraction, speaker separation, and word-level transcription:"):
81
- # Save file
82
  with open("temp_input", "wb") as f:
83
  f.write(uploaded_file.getbuffer())
84
 
85
- # Extract audio
86
  subprocess.run([
87
- "ffmpeg", "-i", "temp_input", "-vn", "-acodec", "pcm_s16le",
88
- "-ar", "16000", "-ac", "1", "audio.wav", "-y"
 
89
  ])
90
 
91
  # 1. Diarization
92
- pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization@2.1", use_auth_token=hf_token)
93
- diarization = pipeline("audio.wav")
 
 
 
 
 
 
 
94
 
95
- # 2. Whisper Transcription (Word Level)
96
  model = whisper.load_model("base")
97
- result = model.transcribe("audio.wav", word_timestamps=True)
98
 
99
- # 3. Alignment Logic
100
  final_segments = []
101
  for segment in result['segments']:
102
- # Determine speaker for this segment based on mid-point
103
  mid_time = (segment['start'] + segment['end']) / 2
104
  speaker = "Unknown"
105
  for turn, _, speaker_id in diarization.itertracks(yield_label=True):
@@ -112,7 +133,7 @@ if uploaded_file:
112
  "text": segment['text'],
113
  "start": segment['start'],
114
  "end": segment['end'],
115
- "words": segment.get('words', []) # Word-level timestamps
116
  })
117
 
118
  st.session_state.transcript = final_segments
@@ -120,16 +141,16 @@ if uploaded_file:
120
 
121
  if "transcript" in st.session_state:
122
  st.divider()
123
- brief = st.text_area("Creative Brief", placeholder="e.g., Focus on the story about yeast, remove the interviewer.")
124
 
125
  if st.button("Step 2: Create EDL"):
126
  if not active_api_key:
127
  st.error("Gemini API Key required.")
128
  else:
129
- with st.spinner("Senior Editor analyzing..."):
130
- segments = call_gemini_for_edl(st.session_state.transcript, brief, active_api_key)
131
- if segments:
132
- edl_content = generate_cmx_edl("AI_Edit", segments, uploaded_file.name, fps)
133
  st.subheader("EDL Preview")
134
- st.text_area("Output", value=edl_content, height=250)
135
- st.download_button("Download EDL", data=edl_content, file_name="edit.edl")
 
8
  from datetime import timedelta
9
  from pyannote.audio import Pipeline
10
 
11
+ # --- Configuration & Tokens ---
12
+ # You can hardcode your token here or use the Sidebar in the App
13
+ HF_TOKEN = os.environ.get("HF_TOKEN", "REPLACE_WITH_YOUR_HF_TOKEN")
14
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
15
 
16
  def format_timecode(seconds, fps=25):
17
+ """Converts seconds to HH:MM:SS:FF for Resolve/Premiere."""
18
  td = timedelta(seconds=seconds)
19
  total_seconds = int(td.total_seconds())
20
  hours = total_seconds // 3600
 
24
  return f"{hours:02}:{minutes:02}:{secs:02}:{frames:02}"
25
 
26
  def generate_cmx_edl(edl_title, segments, source_name, fps=25):
27
+ """Constructs a CMX 3600 formatted EDL."""
28
  edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
29
  rec_start = 0.0
30
  for i, seg in enumerate(segments, 1):
 
33
  duration = seg['src_end'] - seg['src_start']
34
  rec_in = format_timecode(rec_start, fps)
35
  rec_out = format_timecode(rec_start + duration, fps)
36
+
37
  edl_lines.append(f"{i:03} AX V C {src_in} {src_out} {rec_in} {rec_out}")
38
  edl_lines.append(f"* FROM CLIP NAME: {source_name}")
39
  edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
 
41
  return "\n".join(edl_lines)
42
 
43
  def call_gemini_for_edl(transcript_data, story_prompt, api_key):
44
+ """Sends diarized, word-level transcript to Gemini Senior Editor."""
45
  if not api_key:
46
+ st.error("Gemini API Key is missing.")
47
  return None
48
+
49
  url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
50
+
51
  system_prompt = (
52
+ "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
53
+ "(which includes Speaker IDs and word-level timestamps) to create a condensed story. "
54
  "Output ONLY a valid JSON array of segments with 'src_start', 'src_end', and 'note'. "
55
+ "Rules: Remove fluff/repeats, ignore interviewer interruptions, and focus on the hook."
56
  )
57
+
58
  payload = {
59
+ "contents": [{"parts": [{"text": f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"}]}],
60
  "systemInstruction": {"parts": [{"text": system_prompt}]},
61
  "generationConfig": {"responseMimeType": "application/json"}
62
  }
63
+
64
  try:
65
  res = requests.post(url, json=payload)
66
  res.raise_for_status()
67
  return json.loads(res.json()['candidates'][0]['content']['parts'][0]['text'])
68
  except Exception as e:
69
+ st.error(f"Senior Editor AI Error: {e}")
70
  return None
71
 
72
+ # --- Streamlit UI ---
73
  st.set_page_config(page_title="DocAI Editor", layout="wide")
74
+ st.title("Documentary AI: Pipeline")
75
 
76
  with st.sidebar:
77
  st.header("Settings")
78
+ input_gemini_key = st.text_input("Gemini API Key", type="password", value=GEMINI_API_KEY)
79
+ input_hf_token = st.text_input("HF Token (Diarization)", type="password", value=HF_TOKEN)
80
+
81
+ active_api_key = input_gemini_key if input_gemini_key else GEMINI_API_KEY
82
+ active_hf_token = input_hf_token if input_hf_token else HF_TOKEN
83
  fps = st.number_input("Timeline FPS", value=25)
84
 
85
+ uploaded_file = st.file_uploader("Upload Video/Audio Clip", type=["mp4", "m4a", "wav", "mp3", "mov"])
86
 
87
  if uploaded_file:
88
+ # --- Step 1: Technical Processing ---
89
  if "transcript" not in st.session_state:
90
  if st.button("Step 1: Transcribe & Diarize"):
91
+ if not active_hf_token or active_hf_token.startswith("REPLACE"):
92
+ st.error("Please provide a valid Hugging Face Token.")
93
  else:
94
+ with st.spinner("Processing... Extracting audio, identifying speakers, and transcribing:"):
95
+ # Save local temp file
96
  with open("temp_input", "wb") as f:
97
  f.write(uploaded_file.getbuffer())
98
 
99
+ # Optimized Audio: m4a, 64kbps, 16kHz, mono
100
  subprocess.run([
101
+ "ffmpeg", "-i", "temp_input",
102
+ "-vn", "-acodec", "aac", "-ab", "64k", "-ar", "16000", "-ac", "1",
103
+ "audio_optimized.m4a", "-y"
104
  ])
105
 
106
  # 1. Diarization
107
+ try:
108
+ pipeline = Pipeline.from_pretrained(
109
+ "pyannote/speaker-diarization@2.1",
110
+ use_auth_token=active_hf_token
111
+ )
112
+ diarization = pipeline("audio_optimized.m4a")
113
+ except Exception as e:
114
+ st.error(f"Diarization Error: {e}")
115
+ st.stop()
116
 
117
+ # 2. Whisper Transcription
118
  model = whisper.load_model("base")
119
+ result = model.transcribe("audio_optimized.m4a", word_timestamps=True)
120
 
121
+ # 3. Alignment
122
  final_segments = []
123
  for segment in result['segments']:
 
124
  mid_time = (segment['start'] + segment['end']) / 2
125
  speaker = "Unknown"
126
  for turn, _, speaker_id in diarization.itertracks(yield_label=True):
 
133
  "text": segment['text'],
134
  "start": segment['start'],
135
  "end": segment['end'],
136
+ "words": segment.get('words', [])
137
  })
138
 
139
  st.session_state.transcript = final_segments
 
141
 
142
  if "transcript" in st.session_state:
143
  st.divider()
144
+ brief = st.text_area("Creative Brief", placeholder="e.g. Focus on the yeast story, remove the interviewer.")
145
 
146
  if st.button("Step 2: Create EDL"):
147
  if not active_api_key:
148
  st.error("Gemini API Key required.")
149
  else:
150
+ with st.spinner("Analyzing..."):
151
+ edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, active_api_key)
152
+ if edl_segments:
153
+ final_edl = generate_cmx_edl("AI_Senior_Editor_Cut", edl_segments, uploaded_file.name, fps)
154
  st.subheader("EDL Preview")
155
+ st.code(final_edl, language="text")
156
+ st.download_button("Download EDL", data=final_edl, file_name="edit.edl")