NickVerri commited on
Commit
32eafa3
·
verified ·
1 Parent(s): 8a66ab8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -1
app.py CHANGED
@@ -30,4 +30,163 @@ def format_timecode(seconds, fps=25):
30
  minutes = (total_seconds % 3600) // 60
31
  secs = total_seconds % 60
32
  frames = int((seconds - total_seconds) * fps)
33
- return f"{hou
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  minutes = (total_seconds % 3600) // 60
31
  secs = total_seconds % 60
32
  frames = int((seconds - total_seconds) * fps)
33
+ return f"{hours:02}:{minutes:02}:{secs:02}:{frames:02}"
34
+
35
+ def generate_cmx_edl(edl_title, segments, source_name, fps=25):
36
+ """Constructs a CMX 3600 formatted EDL."""
37
+ edl_lines = [f"TITLE: {edl_title}", "FCM: NON-DROP FRAME\n"]
38
+ rec_start = 0.0
39
+ for i, seg in enumerate(segments, 1):
40
+ src_in = format_timecode(seg['src_start'], fps)
41
+ src_out = format_timecode(seg['src_end'], fps)
42
+ duration = seg['src_end'] - seg['src_start']
43
+ rec_in = format_timecode(rec_start, fps)
44
+ rec_out = format_timecode(rec_start + duration, fps)
45
+
46
+ edl_lines.append(f"{i:03} AX V C {src_in} {src_out} {rec_in} {rec_out}")
47
+ edl_lines.append(f"* FROM CLIP NAME: {source_name}")
48
+ edl_lines.append(f"* {seg.get('note', 'Clip')}\n")
49
+ rec_start += duration
50
+ return "\n".join(edl_lines)
51
+
52
+ def call_gemini_for_edl(transcript_data, story_prompt, api_key):
53
+ """Sends diarized, word-level transcript to Gemini Senior Editor."""
54
+ if not api_key:
55
+ st.error("Gemini API Key is missing. Set it in Space Secrets or app.py.")
56
+ return None
57
+
58
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={api_key}"
59
+
60
+ # Updated system prompt to explicitly ignore interviewer comments
61
+ system_prompt = (
62
+ "You are an expert Documentary Senior Editor. Use the provided transcript JSON "
63
+ "(which includes Speaker IDs and word-level timestamps) to create a condensed story. "
64
+ "Output ONLY a valid JSON array of segments with 'src_start', 'src_end', and 'note'. "
65
+ "CRITICAL RULES:\n"
66
+ "1. IGNORE ALL INTERVIEWER COMMENTS: Do not include any speech or segments where the interviewer is speaking.\n"
67
+ "2. REMOVE FLUFF: Delete 'um', 'ah', repeats, and irrelevant filler.\n"
68
+ "3. NARRATIVE FLOW: Focus on the subject's high-energy responses and narrative hooks.\n"
69
+ "4. TIMESTAMP INTEGRITY: Use only the exact word-level start and end times from the data."
70
+ )
71
+
72
+ # Cleaned up payload to prevent SyntaxErrors with f-strings
73
+ prompt_text = f"Creative Brief: {story_prompt}\n\nTranscript Data:\n{json.dumps(transcript_data)}"
74
+
75
+ payload = {
76
+ "contents": [{
77
+ "parts": [{"text": prompt_text}]
78
+ }],
79
+ "systemInstruction": {
80
+ "parts": [{"text": system_prompt}]
81
+ },
82
+ "generationConfig": {
83
+ "responseMimeType": "application/json"
84
+ }
85
+ }
86
+
87
+ try:
88
+ res = requests.post(url, json=payload)
89
+ res.raise_for_status()
90
+ result_json = res.json()
91
+ return json.loads(result_json['candidates'][0]['content']['parts'][0]['text'])
92
+ except Exception as e:
93
+ st.error(f"Senior Editor AI Error: {e}")
94
+ return None
95
+
96
+ # --- Streamlit UI ---
97
+ st.set_page_config(page_title="DocAI Editor", layout="wide")
98
+ st.title("Documentary AI: Pipeline")
99
+
100
+ with st.sidebar:
101
+ st.header("Project Settings")
102
+ fps = st.number_input("Timeline FPS", value=25)
103
+
104
+ st.divider()
105
+ st.info("API Keys and Tokens are managed via Environment Secrets for security.")
106
+ if not ACTIVE_GEMINI_KEY:
107
+ st.error("⚠️ Gemini API Key not found!")
108
+ if not ACTIVE_HF_TOKEN:
109
+ st.error("⚠️ HF Token not found!")
110
+
111
+ uploaded_file = st.file_uploader("Upload Video/Audio Clip", type=["mp4", "m4a", "wav", "mp3", "mov"])
112
+
113
+ if uploaded_file:
114
+ # --- Step 1: Technical Processing ---
115
+ if "transcript" not in st.session_state:
116
+ if st.button("Step 1: Transcribe & Diarize"):
117
+ if not ACTIVE_HF_TOKEN or "PASTE_YOUR_HF_TOKEN" in ACTIVE_HF_TOKEN:
118
+ st.error("Please provide a valid Hugging Face Token in Space Secrets or app.py.")
119
+ else:
120
+ with st.spinner("Processing... Extracting audio, identifying speakers, and transcribing:"):
121
+ # Save local temp file
122
+ with open("temp_input", "wb") as f:
123
+ f.write(uploaded_file.getbuffer())
124
+
125
+ # Optimized Audio: m4a, 64kbps, 16kHz, mono
126
+ subprocess.run([
127
+ "ffmpeg", "-i", "temp_input",
128
+ "-vn", "-acodec", "aac", "-ab", "64k", "-ar", "16000", "-ac", "1",
129
+ "audio_optimized.m4a", "-y"
130
+ ])
131
+
132
+ # 1. Diarization
133
+ try:
134
+ pipeline = Pipeline.from_pretrained(
135
+ "pyannote/speaker-diarization-3.1",
136
+ use_auth_token=ACTIVE_HF_TOKEN
137
+ )
138
+
139
+ if torch.cuda.is_available():
140
+ pipeline.to(torch.device("cuda"))
141
+
142
+ diarization = pipeline("audio_optimized.m4a")
143
+ except Exception as e:
144
+ st.error(f"Diarization Error: {e}")
145
+ st.stop()
146
+
147
+ # 2. Whisper Transcription
148
+ model = whisper.load_model("base")
149
+ result = model.transcribe("audio_optimized.m4a", word_timestamps=True)
150
+
151
+ # 3. Alignment
152
+ final_segments = []
153
+ for segment in result['segments']:
154
+ mid_time = (segment['start'] + segment['end']) / 2
155
+ speaker = "Unknown"
156
+ for turn, _, speaker_id in diarization.itertracks(yield_label=True):
157
+ if turn.start <= mid_time <= turn.end:
158
+ speaker = speaker_id
159
+ break
160
+
161
+ final_segments.append({
162
+ "speaker": speaker,
163
+ "text": segment['text'],
164
+ "start": segment['start'],
165
+ "end": segment['end'],
166
+ "words": segment.get('words', [])
167
+ })
168
+
169
+ st.session_state.transcript = final_segments
170
+ st.success("Transcription and Diarization Complete.")
171
+
172
+ if "transcript" in st.session_state:
173
+ st.divider()
174
+
175
+ # Diarization Preview to help the user identify roles
176
+ with st.expander("Transcript Preview (Diarized)"):
177
+ for seg in st.session_state.transcript[:20]: # Show first 20 segments
178
+ st.write(f"**{seg['speaker']}:** {seg['text']}")
179
+
180
+ brief = st.text_area("Creative Brief", placeholder="e.g. Focus on the yeast story, remove the interviewer.")
181
+
182
+ if st.button("Step 2: Create EDL"):
183
+ if not ACTIVE_GEMINI_KEY:
184
+ st.error("Gemini API Key required. Please set it in Secrets or app.py.")
185
+ else:
186
+ with st.spinner("Analyzing..."):
187
+ edl_segments = call_gemini_for_edl(st.session_state.transcript, brief, ACTIVE_GEMINI_KEY)
188
+ if edl_segments:
189
+ final_edl = generate_cmx_edl("AI_Senior_Editor_Cut", edl_segments, uploaded_file.name, fps)
190
+ st.subheader("EDL Preview")
191
+ st.code(final_edl, language="text")
192
+ st.download_button("Download EDL", data=final_edl, file_name="edit.edl")