Update app.py
Browse files
app.py
CHANGED
@@ -1,266 +1,662 @@
|
|
1 |
import gradio as gr
|
2 |
import asyncio
|
|
|
3 |
from pathlib import Path
|
4 |
-
from google import genai
|
5 |
-
from google.genai import types
|
6 |
import os
|
7 |
-
|
8 |
-
|
9 |
-
from youtube_transcript_api import YouTubeTranscriptApi
|
10 |
import re
|
11 |
import pandas as pd
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
@dataclass
|
16 |
class ContentRequest:
|
17 |
prompt_key: str
|
18 |
|
19 |
class ContentGenerator:
|
20 |
-
def __init__(self
|
21 |
self.current_prompts = self._load_default_prompts()
|
22 |
-
self.client
|
23 |
|
24 |
def _load_default_prompts(self) -> Dict[str, str]:
|
25 |
-
|
26 |
-
|
27 |
-
# Load CSV examples
|
28 |
-
try:
|
29 |
-
timestamps_df = pd.read_csv("data/Timestamps.csv")
|
30 |
-
titles_df = pd.read_csv("data/Titles & Thumbnails.csv")
|
31 |
-
descriptions_df = pd.read_csv("data/Viral Episode Descriptions.csv")
|
32 |
-
clips_df = pd.read_csv("data/Viral Twitter Clips.csv")
|
33 |
-
|
34 |
-
# Format timestamp examples
|
35 |
-
timestamp_examples = "\n\n".join(timestamps_df['Timestamps'].dropna().tolist())
|
36 |
-
|
37 |
-
# Format title examples
|
38 |
-
title_examples = "\n".join([
|
39 |
-
f'Title: "{row.Titles}"\nThumbnail: "{row.Thumbnail}"'
|
40 |
-
for _, row in titles_df.iterrows()
|
41 |
-
])
|
42 |
-
|
43 |
-
# Format description examples
|
44 |
-
description_examples = "\n".join([
|
45 |
-
f'Tweet: "{row["Tweet Text"]}"'
|
46 |
-
for _, row in descriptions_df.iterrows()
|
47 |
-
])
|
48 |
-
|
49 |
-
# Format clip examples
|
50 |
-
clip_examples = "\n\n".join([
|
51 |
-
f'Tweet Text: "{row["Tweet Text"]}"\nClip Transcript: "{row["Clip Transcript"]}"'
|
52 |
-
for _, row in clips_df.iterrows() if pd.notna(row["Tweet Text"])
|
53 |
-
])
|
54 |
-
|
55 |
-
except Exception as e:
|
56 |
-
print(f"Warning: Error loading CSV examples: {e}")
|
57 |
-
timestamp_examples = ""
|
58 |
-
title_examples = ""
|
59 |
-
description_examples = ""
|
60 |
-
clip_examples = ""
|
61 |
-
|
62 |
-
# Load base prompts and inject examples
|
63 |
prompts = {}
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
return prompts
|
80 |
|
81 |
async def generate_content(self, request: ContentRequest, transcript: str) -> str:
|
82 |
-
|
|
|
|
|
83 |
try:
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
response =
|
90 |
-
model=
|
91 |
-
config=types.GenerateContentConfig(system_instruction=self.current_prompts[request.prompt_key]),
|
92 |
-
contents=transcript
|
93 |
)
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
except Exception as e:
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
111 |
def get_transcript(video_id: str) -> str:
|
112 |
-
|
113 |
try:
|
114 |
-
|
115 |
-
|
|
|
|
|
|
|
116 |
except Exception as e:
|
117 |
-
return f"
|
118 |
|
|
|
119 |
class TranscriptProcessor:
|
120 |
def __init__(self):
|
121 |
-
self.generator = ContentGenerator(
|
122 |
-
|
123 |
|
|
|
124 |
def _get_youtube_transcript(self, url: str) -> str:
|
125 |
-
|
|
|
|
|
|
|
|
|
126 |
try:
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
|
133 |
-
async def process_transcript(self, audio_file):
|
134 |
-
"""Process input and generate all content."""
|
135 |
-
audio_path = audio_file.name
|
136 |
try:
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
{
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
|
163 |
-
|
164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
|
166 |
def update_prompts(self, *values) -> str:
|
167 |
-
|
168 |
-
self.generator.
|
169 |
-
["previews", "clips", "description", "timestamps", "titles_and_thumbnails"],
|
170 |
-
values
|
171 |
-
))
|
172 |
-
return "Prompts updated for this session!"
|
173 |
|
174 |
|
|
|
175 |
|
176 |
def create_interface():
|
177 |
-
"""Create the Gradio interface."""
|
178 |
processor = TranscriptProcessor()
|
179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
with gr.Blocks(title="Gemini Podcast Content Generator") as app:
|
181 |
gr.Markdown(
|
182 |
"""
|
183 |
# Gemini Podcast Content Generator
|
184 |
-
Generate
|
185 |
-
|
|
|
186 |
"""
|
187 |
-
)
|
188 |
|
189 |
with gr.Tab("Generate Content"):
|
|
|
|
|
|
|
|
|
|
|
190 |
input_audio = gr.File(
|
191 |
-
label="Upload Audio File",
|
192 |
-
|
193 |
-
file_types=["audio"]
|
194 |
)
|
195 |
-
submit_btn = gr.Button("Generate Content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
|
197 |
-
|
|
|
198 |
|
199 |
-
|
200 |
-
|
201 |
-
print(f"Input text: {text[:100]}...")
|
202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
try:
|
204 |
-
|
205 |
-
|
206 |
-
|
|
|
207 |
except Exception as e:
|
208 |
-
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
|
211 |
submit_btn.click(
|
212 |
fn=process_wrapper,
|
213 |
-
inputs=input_audio,
|
214 |
-
outputs=
|
215 |
-
queue=True
|
216 |
)
|
217 |
|
218 |
with gr.Tab("Customize Prompts"):
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
""
|
226 |
-
|
|
|
|
|
|
|
|
|
227 |
|
228 |
-
prompt_inputs = [
|
229 |
-
gr.Textbox(
|
230 |
-
label=f"{key.replace('_', ' ').title()} Prompt",
|
231 |
-
lines=10,
|
232 |
-
value=processor.generator.current_prompts[key]
|
233 |
-
)
|
234 |
-
for key in [
|
235 |
-
"previews",
|
236 |
-
"clips",
|
237 |
-
"description",
|
238 |
-
"timestamps",
|
239 |
-
"titles_and_thumbnails"
|
240 |
-
]
|
241 |
-
]
|
242 |
-
status = gr.Textbox(label="Status", interactive=False)
|
243 |
-
|
244 |
-
# Update prompts when they change
|
245 |
-
for prompt in prompt_inputs:
|
246 |
-
prompt.change(
|
247 |
-
fn=processor.update_prompts,
|
248 |
-
inputs=prompt_inputs,
|
249 |
-
outputs=[status]
|
250 |
-
)
|
251 |
-
|
252 |
-
# Reset button
|
253 |
reset_btn = gr.Button("Reset to Default Prompts")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
reset_btn.click(
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
),
|
259 |
-
outputs=[status] + prompt_inputs,
|
260 |
)
|
261 |
|
262 |
return app
|
263 |
|
|
|
264 |
if __name__ == "__main__":
|
265 |
-
|
266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
import asyncio
|
3 |
+
from asyncio import Semaphore # Added for concurrency control
|
4 |
from pathlib import Path
|
|
|
|
|
5 |
import os
|
6 |
+
import tempfile # Added for temporary chunk files
|
7 |
+
import traceback # Import traceback for better error logging
|
|
|
8 |
import re
|
9 |
import pandas as pd
|
10 |
+
from dataclasses import dataclass
|
11 |
+
from typing import Dict, AsyncGenerator, Tuple, Any, List
|
12 |
+
|
13 |
+
# Use standard import convention for genai
|
14 |
+
# Assuming genai is installed and configured elsewhere
|
15 |
+
from google import genai
|
16 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
17 |
|
18 |
+
# Import pydub for audio manipulation
|
19 |
+
from pydub import AudioSegment
|
20 |
+
from pydub.exceptions import CouldntDecodeError
|
21 |
+
|
22 |
+
|
23 |
+
# --- Constants ---
|
24 |
+
PROMPT_KEYS = ["titles_and_thumbnails", "description", "previews", "clips", "timestamps"]
|
25 |
+
PROMPT_DISPLAY_NAMES = {
|
26 |
+
"titles_and_thumbnails": "Titles and Thumbnails",
|
27 |
+
"description": "Twitter Description",
|
28 |
+
"previews": "Preview Clips",
|
29 |
+
"clips": "Twitter Clips",
|
30 |
+
"timestamps": "Timestamps"
|
31 |
+
}
|
32 |
+
# --- MODIFIED: Increased chunk size to 30 minutes ---
|
33 |
+
AUDIO_CHUNK_DURATION_MS = 30 * 60 * 1000 # Process audio in 30-minute chunks
|
34 |
+
# --- ADDED: Concurrency Limits ---
|
35 |
+
MAX_CONCURRENT_TRANSCRIPTIONS = 3 # Limit simultaneous transcription API calls
|
36 |
+
MAX_CONCURRENT_GENERATIONS = 4 # Limit simultaneous content generation API calls
|
37 |
+
|
38 |
+
# --- Core Classes (ContentRequest, ContentGenerator) ---
|
39 |
+
# (ContentRequest and ContentGenerator remain unchanged)
|
40 |
@dataclass
|
41 |
class ContentRequest:
|
42 |
prompt_key: str
|
43 |
|
44 |
class ContentGenerator:
|
45 |
+
def __init__(self):
|
46 |
self.current_prompts = self._load_default_prompts()
|
47 |
+
self.client: genai.Client | None = None
|
48 |
|
49 |
def _load_default_prompts(self) -> Dict[str, str]:
|
50 |
+
# (Implementation identical to previous version)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
prompts = {}
|
52 |
+
timestamp_examples, title_examples, description_examples, clip_examples = "", "", "", ""
|
53 |
+
try:
|
54 |
+
data_dir = Path("data")
|
55 |
+
if data_dir.is_dir():
|
56 |
+
try: timestamps_df = pd.read_csv(data_dir / "Timestamps.csv"); timestamp_examples = "\n\n".join(timestamps_df['Timestamps'].dropna().tolist())
|
57 |
+
except Exception as e: print(f"Warning: Loading Timestamps.csv failed: {e}")
|
58 |
+
try: titles_df = pd.read_csv(data_dir / "Titles & Thumbnails.csv"); title_examples = "\n".join([f'Title: "{r.Titles}"\nThumbnail: "{r.Thumbnail}"' for _, r in titles_df.iterrows() if pd.notna(r.Titles) and pd.notna(r.Thumbnail)])
|
59 |
+
except Exception as e: print(f"Warning: Loading Titles & Thumbnails.csv failed: {e}")
|
60 |
+
try: descriptions_df = pd.read_csv(data_dir / "Viral Episode Descriptions.csv"); description_examples = "\n".join([f'Tweet: "{r["Tweet Text"]}"' for _, r in descriptions_df.iterrows() if pd.notna(r["Tweet Text"])])
|
61 |
+
except Exception as e: print(f"Warning: Loading Viral Episode Descriptions.csv failed: {e}")
|
62 |
+
try: clips_df = pd.read_csv(data_dir / "Viral Twitter Clips.csv"); clip_examples = "\n\n".join([f'Tweet Text: "{r["Tweet Text"]}"\nClip Transcript: "{r["Clip Transcript"]}"' for _, r in clips_df.iterrows() if pd.notna(r["Tweet Text"]) and pd.notna(r["Clip Transcript"])])
|
63 |
+
except Exception as e: print(f"Warning: Loading Viral Twitter Clips.csv failed: {e}")
|
64 |
+
else: print("Warning: 'data' directory not found.")
|
65 |
+
except Exception as e: print(f"Warning: Error accessing 'data' directory: {e}")
|
66 |
+
|
67 |
+
prompts_dir = Path("prompts")
|
68 |
+
if not prompts_dir.is_dir():
|
69 |
+
print("Error: 'prompts' directory not found.")
|
70 |
+
return {key: f"ERROR: Prompt directory missing." for key in PROMPT_KEYS}
|
71 |
+
for key in PROMPT_KEYS:
|
72 |
+
try:
|
73 |
+
prompt = (prompts_dir / f"{key}.txt").read_text(encoding='utf-8')
|
74 |
+
if key == "timestamps": prompt = prompt.replace("{timestamps_examples}", timestamp_examples)
|
75 |
+
elif key == "titles_and_thumbnails": prompt = prompt.replace("{title_examples}", title_examples)
|
76 |
+
elif key == "description": prompt = prompt.replace("{description_examples}", description_examples)
|
77 |
+
elif key == "clips": prompt = prompt.replace("{clip_examples}", clip_examples)
|
78 |
+
prompts[key] = prompt
|
79 |
+
except Exception as e:
|
80 |
+
print(f"Warning: Loading prompt prompts/{key}.txt failed: {e}")
|
81 |
+
prompts[key] = f"Generate {key} based on the transcript. Do not use markdown formatting." # Fallback
|
82 |
+
for key in PROMPT_KEYS: prompts.setdefault(key, f"Generate {key} based on the transcript. Do not use markdown formatting.")
|
83 |
return prompts
|
84 |
|
85 |
async def generate_content(self, request: ContentRequest, transcript: str) -> str:
|
86 |
+
# (Implementation identical to previous version)
|
87 |
+
if not self.client: return "ERROR_CONFIGURATION: Gemini Client not initialized."
|
88 |
+
if not transcript: return "ERROR_INTERNAL: Empty transcript provided for content generation."
|
89 |
try:
|
90 |
+
system_prompt = self.current_prompts.get(request.prompt_key)
|
91 |
+
if not system_prompt: return f"ERROR_INTERNAL: System prompt for '{request.prompt_key}' missing."
|
92 |
+
contents_for_api = [system_prompt, transcript]
|
93 |
+
# --- IMPORTANT: Model kept as gemini-1.5-flash ---
|
94 |
+
model_name = "gemini-2.5-pro-preview-03-25"
|
95 |
+
response = await asyncio.to_thread(
|
96 |
+
self.client.models.generate_content, model=model_name, contents=contents_for_api
|
|
|
|
|
97 |
)
|
98 |
+
if not response: return f"ERROR_API: No response received for {request.prompt_key}."
|
99 |
+
try:
|
100 |
+
if response.text:
|
101 |
+
try:
|
102 |
+
if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
|
103 |
+
reason = response.prompt_feedback.block_reason.name; return f"ERROR_BLOCKED: Blocked by API. Reason: {reason}"
|
104 |
+
except AttributeError: pass
|
105 |
+
return str(response.text.strip())
|
106 |
+
else:
|
107 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
108 |
+
full_text = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text')).strip()
|
109 |
+
if full_text:
|
110 |
+
print(f"Warning: Used fallback text extraction via candidates for {request.prompt_key}")
|
111 |
+
return str(full_text)
|
112 |
+
return f"ERROR_NO_TEXT: Could not extract text from response for {request.prompt_key}."
|
113 |
+
except (ValueError, AttributeError) as e:
|
114 |
+
print(f"Error accessing response text/feedback for {request.prompt_key} (potentially blocked): {e}")
|
115 |
+
reason = "Unknown"
|
116 |
+
try:
|
117 |
+
if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason: reason = response.prompt_feedback.block_reason.name
|
118 |
+
except AttributeError: pass
|
119 |
+
return f"ERROR_BLOCKED: Content generation failed (possibly blocked). Reason: {reason}"
|
120 |
except Exception as e:
|
121 |
+
print(f"Error generating content for {request.prompt_key}: {traceback.format_exc()}")
|
122 |
+
error_str = str(e).lower()
|
123 |
+
# Add specific check for rate limit errors if the API provides clear indicators
|
124 |
+
if "rate limit exceeded" in error_str or "quota exceeded" in error_str or "429" in error_str:
|
125 |
+
return f"ERROR_RATE_LIMIT: API limit likely exceeded. Details: {str(e)}"
|
126 |
+
elif "permission denied" in error_str or "api key not valid" in error_str: return f"ERROR_PERMISSION_DENIED: API Error (Permission Denied?). Check Key. Details: {str(e)}"
|
127 |
+
# elif "quota" in error_str: return f"ERROR_QUOTA: API Quota Error. Details: {str(e)}" # Covered by rate limit check above
|
128 |
+
elif "model" in error_str and "not found" in error_str: return f"ERROR_MODEL_NOT_FOUND: Model name likely incorrect. Details: {str(e)}"
|
129 |
+
else: return f"ERROR_API_GENERAL: API Error during generation. Details: {str(e)}"
|
130 |
+
|
131 |
+
def update_prompts(self, *values):
|
132 |
+
# (Implementation identical to previous version)
|
133 |
+
updated_keys = []
|
134 |
+
for key, value in zip(PROMPT_KEYS, values):
|
135 |
+
if isinstance(value, str): self.current_prompts[key] = value; updated_keys.append(key)
|
136 |
+
return f"Prompts updated: {', '.join(updated_keys)}" if updated_keys else "No prompts updated."
|
137 |
+
|
138 |
+
# (extract_video_id and get_transcript remain unchanged)
|
139 |
+
def extract_video_id(url: str) -> str | None:
|
140 |
+
patterns = [r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", r"youtu\.be\/([0-9A-Za-z_-]{11})"]
|
141 |
+
for pattern in patterns:
|
142 |
+
match = re.search(pattern, url);
|
143 |
+
if match: return match.group(1)
|
144 |
+
return None
|
145 |
|
146 |
def get_transcript(video_id: str) -> str:
|
147 |
+
if not video_id: raise ValueError("Invalid Video ID")
|
148 |
try:
|
149 |
+
t_list = YouTubeTranscriptApi.list_transcripts(video_id)
|
150 |
+
transcript = t_list.find_transcript(['en', 'en-US'])
|
151 |
+
fetched = transcript.fetch()
|
152 |
+
if not fetched: raise ValueError("Fetched transcript empty")
|
153 |
+
return " ".join(entry.get("text", "") for entry in fetched).strip()
|
154 |
except Exception as e:
|
155 |
+
return f"ERROR_TRANSCRIPT_FETCH: Failed for ID '{video_id}'. Reason: {e}"
|
156 |
|
157 |
+
# --- TranscriptProcessor Class (Refactored for Concurrency Control) ---
|
158 |
class TranscriptProcessor:
|
159 |
def __init__(self):
|
160 |
+
self.generator = ContentGenerator()
|
|
|
161 |
|
162 |
+
# (Helper _get_youtube_transcript remains unchanged)
|
163 |
def _get_youtube_transcript(self, url: str) -> str:
|
164 |
+
# ... (identical implementation)
|
165 |
+
print(f"Extracting Video ID from: {url}")
|
166 |
+
video_id = extract_video_id(url)
|
167 |
+
if not video_id: raise ValueError(f"Invalid YouTube URL/ID: {url}")
|
168 |
+
print(f"Video ID: {video_id}. Fetching transcript...")
|
169 |
try:
|
170 |
+
transcript = get_transcript(video_id)
|
171 |
+
if transcript.startswith("ERROR_TRANSCRIPT_FETCH"): raise Exception(transcript)
|
172 |
+
if not transcript: raise ValueError(f"Empty transcript for ID: {video_id}")
|
173 |
+
print(f"Transcript fetched (length: {len(transcript)}).")
|
174 |
+
return transcript
|
175 |
+
except Exception as e: print(f"Error fetching YouTube transcript: {e}"); raise Exception(f"Failed to get YouTube transcript: {str(e)}")
|
176 |
+
|
177 |
+
|
178 |
+
# --- MODIFIED: Added Semaphore argument ---
|
179 |
+
async def _transcribe_chunk(self, client: genai.Client, chunk_path: Path, chunk_index: int, total_chunks: int, semaphore: Semaphore) -> str:
|
180 |
+
"""Transcribes a single audio chunk using Gemini API, respecting the semaphore."""
|
181 |
+
# Acquire semaphore before proceeding
|
182 |
+
async with semaphore:
|
183 |
+
print(f"Semaphore acquired for chunk {chunk_index + 1}/{total_chunks}. Processing...")
|
184 |
+
gemini_audio_file_ref = None
|
185 |
+
try:
|
186 |
+
print(f"Uploading chunk {chunk_index + 1}/{total_chunks}: {chunk_path.name}")
|
187 |
+
gemini_audio_file_ref = await asyncio.to_thread(client.files.upload, file=chunk_path)
|
188 |
+
print(f"Chunk {chunk_index + 1} uploaded. File Ref: {gemini_audio_file_ref.name}")
|
189 |
+
|
190 |
+
prompt_for_transcription = "Transcribe the following audio file accurately."
|
191 |
+
contents = [prompt_for_transcription, gemini_audio_file_ref]
|
192 |
+
# --- IMPORTANT: Model kept as gemini-1.5-flash ---
|
193 |
+
model_name = "gemini-2.5-pro-preview-03-25"
|
194 |
+
|
195 |
+
print(f"Requesting transcription for chunk {chunk_index + 1}...")
|
196 |
+
# Make the API call *within* the semaphore lock
|
197 |
+
transcription_response = await asyncio.to_thread(
|
198 |
+
client.models.generate_content, model=model_name, contents=contents
|
199 |
+
)
|
200 |
+
print(f"Transcription response received for chunk {chunk_index + 1}.")
|
201 |
+
|
202 |
+
# Extract transcript text (identical logic)
|
203 |
+
transcript_piece = ""
|
204 |
+
try:
|
205 |
+
if transcription_response.text:
|
206 |
+
transcript_piece = transcription_response.text.strip()
|
207 |
+
elif transcription_response.candidates and transcription_response.candidates[0].content and transcription_response.candidates[0].content.parts:
|
208 |
+
transcript_piece = "".join(part.text for part in transcription_response.candidates[0].content.parts if hasattr(part, 'text')).strip()
|
209 |
+
|
210 |
+
if not transcript_piece and hasattr(transcription_response, 'prompt_feedback') and transcription_response.prompt_feedback.block_reason:
|
211 |
+
reason = transcription_response.prompt_feedback.block_reason.name
|
212 |
+
print(f"Warning: Transcription blocked for chunk {chunk_index + 1}. Reason: {reason}")
|
213 |
+
return f"[CHUNK_ERROR: Blocked - {reason}]"
|
214 |
+
|
215 |
+
print(f"Chunk {chunk_index + 1} transcript length: {len(transcript_piece)}")
|
216 |
+
return str(transcript_piece)
|
217 |
+
|
218 |
+
except (ValueError, AttributeError, Exception) as extraction_err:
|
219 |
+
print(f"Error extracting transcript for chunk {chunk_index + 1}: {extraction_err}. Response: {transcription_response}")
|
220 |
+
return f"[CHUNK_ERROR: Extraction Failed - {str(extraction_err)}]"
|
221 |
+
|
222 |
+
except Exception as e:
|
223 |
+
print(f"Error processing chunk {chunk_index + 1} (within semaphore): {traceback.format_exc()}")
|
224 |
+
error_str = str(e).lower()
|
225 |
+
# Add specific check for rate limit errors
|
226 |
+
if "rate limit exceeded" in error_str or "quota exceeded" in error_str or "429" in error_str:
|
227 |
+
return f"[CHUNK_ERROR: API Rate Limit Exceeded - {str(e)}]"
|
228 |
+
elif "permission denied" in error_str or "api key not valid" in error_str:
|
229 |
+
return f"[CHUNK_ERROR: API Permission Denied - {str(e)}]"
|
230 |
+
elif "file size" in error_str:
|
231 |
+
return f"[CHUNK_ERROR: File Size Limit Exceeded - {str(e)}]"
|
232 |
+
else:
|
233 |
+
return f"[CHUNK_ERROR: General API/Processing Error - {str(e)}]"
|
234 |
+
finally:
|
235 |
+
# Cleanup happens *before* semaphore is released automatically by 'async with'
|
236 |
+
if gemini_audio_file_ref:
|
237 |
+
# Run cleanup in background to avoid blocking semaphore release if deletion is slow
|
238 |
+
asyncio.create_task(self.delete_uploaded_file(client, gemini_audio_file_ref.name, f"chunk {chunk_index + 1} cleanup"))
|
239 |
+
if chunk_path.exists():
|
240 |
+
try:
|
241 |
+
os.remove(chunk_path)
|
242 |
+
except OSError as e:
|
243 |
+
print(f"Warning: Could not delete local temp chunk file {chunk_path}: {e}")
|
244 |
+
print(f"Semaphore released for chunk {chunk_index + 1}/{total_chunks}.")
|
245 |
+
# Semaphore is automatically released when exiting 'async with' block
|
246 |
+
|
247 |
+
|
248 |
+
async def process_transcript(self, client: genai.Client, audio_file: Any) -> AsyncGenerator[Tuple[str, Any], None]:
|
249 |
+
"""
|
250 |
+
Processes audio with larger chunks and controlled concurrency using Semaphores.
|
251 |
+
"""
|
252 |
+
if AudioSegment is None:
|
253 |
+
yield "error", "Audio processing library (pydub) not loaded. Cannot proceed."
|
254 |
+
return
|
255 |
+
if not client:
|
256 |
+
yield "error", "Gemini Client object was not provided."
|
257 |
+
return
|
258 |
+
self.generator.client = client
|
259 |
+
if not audio_file:
|
260 |
+
yield "error", "No audio file provided."
|
261 |
+
return
|
262 |
+
|
263 |
+
audio_path_str = getattr(audio_file, 'name', None)
|
264 |
+
if not audio_path_str:
|
265 |
+
yield "error", "Invalid audio file object."
|
266 |
+
return
|
267 |
+
original_audio_path = Path(audio_path_str)
|
268 |
+
if not original_audio_path.exists():
|
269 |
+
yield "error", f"Audio file not found: {original_audio_path}"
|
270 |
+
return
|
271 |
+
|
272 |
+
# --- ADDED: Initialize Semaphores ---
|
273 |
+
transcription_semaphore = Semaphore(MAX_CONCURRENT_TRANSCRIPTIONS)
|
274 |
+
generation_semaphore = Semaphore(MAX_CONCURRENT_GENERATIONS)
|
275 |
|
|
|
|
|
|
|
276 |
try:
|
277 |
+
yield "status", f"Loading audio file: {original_audio_path.name}..."
|
278 |
+
print(f"Loading audio file with pydub: {original_audio_path}")
|
279 |
+
try:
|
280 |
+
file_format = original_audio_path.suffix.lower().replace('.', '')
|
281 |
+
audio = AudioSegment.from_file(original_audio_path, format=file_format if file_format else None)
|
282 |
+
except CouldntDecodeError as decode_error:
|
283 |
+
print(f"pydub decode error: {decode_error}. Make sure ffmpeg is installed.")
|
284 |
+
yield "error", f"Failed to load/decode audio file: {original_audio_path.name}. Ensure valid format and ffmpeg."
|
285 |
+
return
|
286 |
+
except Exception as load_err:
|
287 |
+
print(f"Error loading audio with pydub: {traceback.format_exc()}")
|
288 |
+
yield "error", f"Error loading audio file {original_audio_path.name}: {load_err}"
|
289 |
+
return
|
290 |
+
|
291 |
+
duration_ms = len(audio)
|
292 |
+
# --- MODIFIED: Chunk duration increased ---
|
293 |
+
total_chunks = (duration_ms + AUDIO_CHUNK_DURATION_MS - 1) // AUDIO_CHUNK_DURATION_MS
|
294 |
+
print(f"Audio loaded. Duration: {duration_ms / 1000:.2f}s. Splitting into {total_chunks} x {AUDIO_CHUNK_DURATION_MS / 60000:.1f}min chunks.")
|
295 |
+
yield "status", f"Audio loaded ({duration_ms / 1000:.2f}s). Transcribing in {total_chunks} chunks (max {MAX_CONCURRENT_TRANSCRIPTIONS} concurrent)..."
|
296 |
+
|
297 |
+
transcript_pieces = [""] * total_chunks # Pre-allocate list to store pieces in order
|
298 |
+
transcription_tasks = []
|
299 |
+
|
300 |
+
# --- MODIFIED: Create tasks with semaphore ---
|
301 |
+
for i in range(total_chunks):
|
302 |
+
start_ms = i * AUDIO_CHUNK_DURATION_MS
|
303 |
+
end_ms = min((i + 1) * AUDIO_CHUNK_DURATION_MS, duration_ms)
|
304 |
+
chunk = audio[start_ms:end_ms]
|
305 |
+
|
306 |
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_chunk_file:
|
307 |
+
chunk_path = Path(temp_chunk_file.name)
|
308 |
+
try:
|
309 |
+
chunk.export(chunk_path, format="wav")
|
310 |
+
except Exception as export_err:
|
311 |
+
print(f"Error exporting chunk {i+1}: {traceback.format_exc()}")
|
312 |
+
yield "error", f"Failed to create temporary audio chunk file: {export_err}"
|
313 |
+
if chunk_path.exists(): os.remove(chunk_path)
|
314 |
+
return
|
315 |
+
|
316 |
+
# Pass semaphore to the chunk transcription function
|
317 |
+
task = asyncio.create_task(self._transcribe_chunk(client, chunk_path, i, total_chunks, transcription_semaphore))
|
318 |
+
# Store task along with its index to place result correctly
|
319 |
+
transcription_tasks.append((i, task))
|
320 |
+
|
321 |
+
# Process transcription results as they complete, maintaining order
|
322 |
+
processed_chunks = 0
|
323 |
+
# Wait for all tasks using gather, but process results as they come in via callbacks or checking task states?
|
324 |
+
# Using asyncio.gather might be simpler here if we need all results before proceeding. Let's try gather.
|
325 |
+
# results = await asyncio.gather(*(task for _, task in transcription_tasks), return_exceptions=True)
|
326 |
+
|
327 |
+
# Alternative: Process as completed, but store in correct order
|
328 |
+
temp_results = {}
|
329 |
+
for index, task in transcription_tasks:
|
330 |
+
try:
|
331 |
+
result = await task
|
332 |
+
temp_results[index] = result
|
333 |
+
processed_chunks += 1
|
334 |
+
yield "status", f"Transcribed chunk {processed_chunks}/{total_chunks}..."
|
335 |
+
# Check for critical chunk errors immediately if needed
|
336 |
+
if isinstance(result, str) and ("[CHUNK_ERROR: API Rate Limit Exceeded" in result or \
|
337 |
+
"[CHUNK_ERROR: API Permission Denied" in result or \
|
338 |
+
"[CHUNK_ERROR: API Quota Exceeded" in result):
|
339 |
+
print(f"Critical API error in chunk {index + 1}, stopping transcription. Error: {result}")
|
340 |
+
yield "error", f"Transcription stopped. Critical API error in chunk {index + 1}: {result.split('-', 1)[-1].strip()}"
|
341 |
+
# Cancel remaining tasks (important!)
|
342 |
+
for j, other_task in transcription_tasks:
|
343 |
+
if not other_task.done():
|
344 |
+
other_task.cancel()
|
345 |
+
return # Stop processing
|
346 |
+
except asyncio.CancelledError:
|
347 |
+
print(f"Transcription task for chunk {index + 1} was cancelled.")
|
348 |
+
temp_results[index] = "[CHUNK_ERROR: Cancelled]"
|
349 |
+
# If one task is cancelled due to an error in another, we might stop everything
|
350 |
+
if processed_chunks < total_chunks: # Avoid double error message if already stopped
|
351 |
+
yield "error", "Transcription process was cancelled."
|
352 |
+
return
|
353 |
+
except Exception as e:
|
354 |
+
print(f"Error waiting for transcription task {index + 1}: {traceback.format_exc()}")
|
355 |
+
temp_results[index] = f"[CHUNK_ERROR: Task Processing Failed - {str(e)}]"
|
356 |
+
processed_chunks += 1 # Count as processed even though it failed
|
357 |
+
|
358 |
+
# Reconstruct the transcript in the correct order
|
359 |
+
transcript_pieces = [temp_results.get(i, "[CHUNK_ERROR: Missing Result]") for i in range(total_chunks)]
|
360 |
+
full_transcript = " ".join(transcript_pieces).strip()
|
361 |
+
|
362 |
+
# Improved check for transcription failure
|
363 |
+
if not full_transcript or full_transcript.isspace() or all(s.startswith("[CHUNK_ERROR") for s in transcript_pieces if s):
|
364 |
+
error_summary = " ".join(p for p in transcript_pieces if p.startswith("[CHUNK_ERROR"))
|
365 |
+
print(f"Transcription failed or resulted in only errors. Summary: {error_summary}")
|
366 |
+
yield "error", f"Failed to transcribe audio or all chunks failed. Errors: {error_summary[:200]}"
|
367 |
+
return
|
368 |
+
|
369 |
+
print(f"Full transcript concatenated (length: {len(full_transcript)}).")
|
370 |
+
yield "status", "Transcription complete. Generating content..."
|
371 |
+
|
372 |
+
# --- Generate other content using the FULL transcript with Semaphore ---
|
373 |
+
generation_tasks = []
|
374 |
+
for key in PROMPT_KEYS:
|
375 |
+
# Pass generation semaphore to the item generation function
|
376 |
+
task = asyncio.create_task(self._generate_single_item(key, full_transcript, generation_semaphore))
|
377 |
+
generation_tasks.append(task)
|
378 |
+
|
379 |
+
generated_items = 0
|
380 |
+
total_items = len(PROMPT_KEYS)
|
381 |
+
# Process generation results as they complete
|
382 |
+
for future in asyncio.as_completed(generation_tasks):
|
383 |
+
try:
|
384 |
+
key, result = await future # Result from _generate_single_item
|
385 |
+
yield "progress", (key, result)
|
386 |
+
generated_items += 1
|
387 |
+
# More granular status for generation
|
388 |
+
yield "status", f"Generating content ({key} done, {generated_items}/{total_items} total)..."
|
389 |
+
except asyncio.CancelledError:
|
390 |
+
# Should not happen unless transcription failed and cancelled tasks
|
391 |
+
print("Content generation task was cancelled.")
|
392 |
+
yield "error", "Content generation cancelled."
|
393 |
+
return
|
394 |
+
except Exception as e:
|
395 |
+
print(f"Error processing completed generation task: {traceback.format_exc()}")
|
396 |
+
yield "status", f"Error during content generation phase: {str(e)}"
|
397 |
+
# Optionally yield an error for the specific item?
|
398 |
+
# key_if_possible = "unknown_key" # How to get key here? Task doesn't easily pass it back on exception
|
399 |
+
# yield "progress", (key_if_possible, f"ERROR_GENERATION: {str(e)}")
|
400 |
+
|
401 |
+
yield "status", "All content generation tasks complete."
|
402 |
+
|
403 |
+
except FileNotFoundError as e:
|
404 |
+
yield "error", f"File Error: {str(e)}"
|
405 |
+
return
|
406 |
+
except Exception as e: # Catch-all for transcription setup phase
|
407 |
+
print(f"Error during transcription setup/chunking phase: {traceback.format_exc()}")
|
408 |
+
yield "error", f"System Error during transcription setup: {str(e)}"
|
409 |
+
return
|
410 |
+
|
411 |
+
|
412 |
+
async def delete_uploaded_file(self, client: genai.Client, file_name: str, context: str):
|
413 |
+
# (Implementation identical - called in background now)
|
414 |
+
if not client or not file_name:
|
415 |
+
# print(f"Skipping deletion: Invalid client or file name ({context}).") # Reduce noise
|
416 |
+
return
|
417 |
+
try:
|
418 |
+
# print(f"Attempting background cleanup: {file_name} ({context})")
|
419 |
+
await asyncio.to_thread(client.files.delete, name=file_name)
|
420 |
+
print(f"Successfully cleaned up Gemini file: {file_name} ({context})")
|
421 |
+
except Exception as cleanup_err:
|
422 |
+
if "not found" in str(cleanup_err).lower() or "404" in str(cleanup_err):
|
423 |
+
pass # Ignore file not found during cleanup
|
424 |
+
# print(f"Info: File {file_name} likely already deleted ({context}).")
|
425 |
+
else:
|
426 |
+
print(f"Warning: Failed Gemini file cleanup for {file_name} ({context}): {cleanup_err}")
|
427 |
|
428 |
+
|
429 |
+
# --- MODIFIED: Added Semaphore argument ---
|
430 |
+
async def _generate_single_item(self, key: str, transcript: str, semaphore: Semaphore) -> Tuple[str, str]:
|
431 |
+
"""Helper to generate one piece of content, respecting the semaphore."""
|
432 |
+
# Acquire semaphore before calling the API
|
433 |
+
async with semaphore:
|
434 |
+
print(f"Semaphore acquired for generating: {key}. Calling API...")
|
435 |
+
result = await self.generator.generate_content(ContentRequest(key), transcript)
|
436 |
+
print(f"Finished generation task for: {key}. Semaphore released.")
|
437 |
+
# Semaphore is released automatically by 'async with'
|
438 |
+
return key, result
|
439 |
|
440 |
def update_prompts(self, *values) -> str:
|
441 |
+
# (Implementation identical to previous version)
|
442 |
+
return self.generator.update_prompts(*values)
|
|
|
|
|
|
|
|
|
443 |
|
444 |
|
445 |
+
# --- Gradio Interface Creation (UI remains unchanged from previous version) ---
|
446 |
|
447 |
def create_interface():
|
448 |
+
"""Create the Gradio interface (UI definition identical to last version)."""
|
449 |
processor = TranscriptProcessor()
|
450 |
|
451 |
+
key_titles = "titles_and_thumbnails"
|
452 |
+
key_desc = "description"
|
453 |
+
key_previews = "previews"
|
454 |
+
key_clips = "clips"
|
455 |
+
key_timestamps = "timestamps"
|
456 |
+
display_titles = PROMPT_DISPLAY_NAMES[key_titles]
|
457 |
+
display_desc = PROMPT_DISPLAY_NAMES[key_desc]
|
458 |
+
display_previews = PROMPT_DISPLAY_NAMES[key_previews]
|
459 |
+
display_clips = PROMPT_DISPLAY_NAMES[key_clips]
|
460 |
+
display_timestamps = PROMPT_DISPLAY_NAMES[key_timestamps]
|
461 |
+
|
462 |
with gr.Blocks(title="Gemini Podcast Content Generator") as app:
|
463 |
gr.Markdown(
|
464 |
"""
|
465 |
# Gemini Podcast Content Generator
|
466 |
+
Generate social media content from podcast audio using Gemini.
|
467 |
+
Enter your Google API key below and upload an audio file.
|
468 |
+
Audio will be processed in larger (~30min) chunks with controlled concurrency.
|
469 |
"""
|
470 |
+
) # Updated description slightly
|
471 |
|
472 |
with gr.Tab("Generate Content"):
|
473 |
+
google_api_key_input = gr.Textbox(
|
474 |
+
label="Google API Key", type="password",
|
475 |
+
placeholder="Enter your Google API Key here",
|
476 |
+
info="Ensure the API key is valid and has necessary permissions."
|
477 |
+
)
|
478 |
input_audio = gr.File(
|
479 |
+
label="Upload Audio File", file_count="single",
|
480 |
+
file_types=["audio", ".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"]
|
|
|
481 |
)
|
482 |
+
submit_btn = gr.Button("Generate Content", variant="huggingface")
|
483 |
+
|
484 |
+
gr.Markdown("### Processing Status")
|
485 |
+
output_status = gr.Textbox(label="Current Status", value="Idle.", interactive=False, lines=1, max_lines=5)
|
486 |
+
|
487 |
+
gr.Markdown(f"### {display_titles}")
|
488 |
+
output_titles = gr.Textbox(value="...", interactive=False, lines=3, max_lines=10) # No label
|
489 |
+
|
490 |
+
gr.Markdown(f"### {display_desc}")
|
491 |
+
output_desc = gr.Textbox(value="...", interactive=False, lines=3, max_lines=10) # No label
|
492 |
+
|
493 |
+
gr.Markdown(f"### {display_previews}")
|
494 |
+
output_previews = gr.Textbox(value="...", interactive=False, lines=3, max_lines=10) # No label
|
495 |
|
496 |
+
gr.Markdown(f"### {display_clips}")
|
497 |
+
output_clips = gr.Textbox(value="...", interactive=False, lines=3, max_lines=10) # No label
|
498 |
|
499 |
+
gr.Markdown(f"### {display_timestamps}")
|
500 |
+
output_timestamps = gr.Textbox(value="...", interactive=False, lines=3, max_lines=10) # No label
|
|
|
501 |
|
502 |
+
outputs_list = [
|
503 |
+
output_status,
|
504 |
+
output_titles, output_desc, output_previews,
|
505 |
+
output_clips, output_timestamps
|
506 |
+
]
|
507 |
+
results_component_map = {
|
508 |
+
key_titles: output_titles, key_desc: output_desc, key_previews: output_previews,
|
509 |
+
key_clips: output_clips, key_timestamps: output_timestamps
|
510 |
+
}
|
511 |
+
|
512 |
+
# --- process_wrapper (UI Update Logic - largely unchanged) ---
|
513 |
+
async def process_wrapper(google_key, audio_file_obj, progress=gr.Progress(track_tqdm=True)):
|
514 |
+
print("Started Processing...")
|
515 |
+
initial_updates = {
|
516 |
+
output_status: gr.update(value="Initiating..."),
|
517 |
+
output_titles: gr.update(value="β³ Pending..."),
|
518 |
+
output_desc: gr.update(value="β³ Pending..."),
|
519 |
+
output_previews: gr.update(value="β³ Pending..."),
|
520 |
+
output_clips: gr.update(value="β³ Pending..."),
|
521 |
+
output_timestamps: gr.update(value="β³ Pending..."),
|
522 |
+
}
|
523 |
+
yield initial_updates
|
524 |
+
|
525 |
+
if not google_key:
|
526 |
+
yield {output_status: gr.update(value="π Error: Missing Google API Key.")}
|
527 |
+
return
|
528 |
+
if not audio_file_obj:
|
529 |
+
yield {output_status: gr.update(value="π Error: No audio file uploaded.")}
|
530 |
+
return
|
531 |
+
|
532 |
+
masked_key = f"{'*'*(len(google_key)-4)}{google_key[-4:]}" if len(google_key) > 4 else "****"
|
533 |
+
print(f"Using Google Key: {masked_key}")
|
534 |
+
print(f"Audio file: Name='{getattr(audio_file_obj, 'name', 'N/A')}'")
|
535 |
+
client: genai.Client | None = None
|
536 |
try:
|
537 |
+
yield {output_status: gr.update(value="β³ Initializing Gemini Client...")}
|
538 |
+
client = await asyncio.to_thread(genai.Client, api_key=google_key)
|
539 |
+
print("Gemini Client initialized successfully.")
|
540 |
+
yield {output_status: gr.update(value="β
Client Initialized.")}
|
541 |
except Exception as e:
|
542 |
+
error_msg = f"π Error: Failed Client Initialization: {e}"
|
543 |
+
print(f"Client Init Error: {traceback.format_exc()}")
|
544 |
+
yield {output_status: gr.update(value=error_msg)}
|
545 |
+
return
|
546 |
+
|
547 |
+
updates_to_yield = {}
|
548 |
+
try:
|
549 |
+
# Call the refactored processor
|
550 |
+
async for update_type, data in processor.process_transcript(client, audio_file_obj):
|
551 |
+
updates_to_yield = {}
|
552 |
+
if update_type == "status":
|
553 |
+
updates_to_yield[output_status] = gr.update(value=f"β³ {data}")
|
554 |
+
elif update_type == "progress":
|
555 |
+
key, result = data
|
556 |
+
component_to_update = results_component_map.get(key)
|
557 |
+
if component_to_update:
|
558 |
+
ui_result = ""
|
559 |
+
if isinstance(result, str) and result.startswith("ERROR_"):
|
560 |
+
# Handle specific rate limit error display
|
561 |
+
if result.startswith("ERROR_RATE_LIMIT"):
|
562 |
+
ui_result = f"β Error (Rate Limit):\n{result.split(':', 1)[-1].strip()}"
|
563 |
+
else:
|
564 |
+
try:
|
565 |
+
error_type, error_detail = result.split(':', 1)
|
566 |
+
error_type_display = error_type.replace('ERROR_', '').replace('_', ' ').title()
|
567 |
+
ui_result = f"β Error ({error_type_display}):\n{error_detail.strip()}"
|
568 |
+
except ValueError:
|
569 |
+
ui_result = f"β Error:\n{result}"
|
570 |
+
else:
|
571 |
+
ui_result = str(result)
|
572 |
+
updates_to_yield[component_to_update] = gr.update(value=ui_result)
|
573 |
+
else:
|
574 |
+
print(f"Warning: No UI component mapped for result key '{key}'")
|
575 |
+
elif update_type == "error":
|
576 |
+
error_message = f"π Processing Error: {data}"
|
577 |
+
updates_to_yield[output_status] = gr.update(value=error_message)
|
578 |
+
yield updates_to_yield
|
579 |
+
return
|
580 |
+
|
581 |
+
if updates_to_yield:
|
582 |
+
yield updates_to_yield
|
583 |
+
|
584 |
+
final_success_update = {output_status: gr.update(value="β
Processing Complete.")}
|
585 |
+
final_success_update.update(updates_to_yield) # Include any final progress updates
|
586 |
+
yield final_success_update
|
587 |
+
print("Process wrapper finished successfully.")
|
588 |
+
|
589 |
+
except Exception as e:
|
590 |
+
print(f"Error in process_wrapper async loop: {traceback.format_exc()}")
|
591 |
+
error_msg = f"π Unexpected wrapper error: {e}"
|
592 |
+
yield {output_status: gr.update(value=error_msg)}
|
593 |
|
594 |
submit_btn.click(
|
595 |
fn=process_wrapper,
|
596 |
+
inputs=[google_api_key_input, input_audio],
|
597 |
+
outputs=outputs_list
|
|
|
598 |
)
|
599 |
|
600 |
with gr.Tab("Customize Prompts"):
|
601 |
+
# (Customize Prompts tab UI remains unchanged)
|
602 |
+
gr.Markdown("## Customize Generation Prompts")
|
603 |
+
prompt_inputs = []
|
604 |
+
default_prompts = processor.generator.current_prompts
|
605 |
+
for key in PROMPT_KEYS:
|
606 |
+
display_name = PROMPT_DISPLAY_NAMES.get(key, key.replace('_', ' ').title())
|
607 |
+
default_value = default_prompts.get(key, "")
|
608 |
+
prompt_inputs.append(gr.Textbox(label=f"{display_name} Prompt", lines=10, value=default_value or ""))
|
609 |
+
|
610 |
+
status_prompt_tab = gr.Textbox(label="Status", interactive=False)
|
611 |
+
update_btn = gr.Button("Update Session Prompts")
|
612 |
+
update_btn.click(fn=processor.update_prompts, inputs=prompt_inputs, outputs=[status_prompt_tab])
|
613 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
614 |
reset_btn = gr.Button("Reset to Default Prompts")
|
615 |
+
def reset_prompts_ui():
|
616 |
+
try:
|
617 |
+
defaults = processor.generator._load_default_prompts()
|
618 |
+
if any(isinstance(v, str) and v.startswith("ERROR:") for v in defaults.values()): raise ValueError("Failed to load one or more default prompts.")
|
619 |
+
processor.generator.current_prompts = defaults
|
620 |
+
updates = {status_prompt_tab: gr.update(value="Prompts reset to defaults!")}
|
621 |
+
for i, key in enumerate(PROMPT_KEYS):
|
622 |
+
updates[prompt_inputs[i]] = gr.update(value=defaults.get(key, ""))
|
623 |
+
return updates
|
624 |
+
except Exception as e:
|
625 |
+
print(f"Error during prompt reset: {e}")
|
626 |
+
return {status_prompt_tab: gr.update(value=f"Error resetting prompts: {str(e)}")}
|
627 |
+
|
628 |
reset_btn.click(
|
629 |
+
fn=reset_prompts_ui,
|
630 |
+
inputs=None,
|
631 |
+
outputs=[status_prompt_tab] + prompt_inputs
|
|
|
|
|
632 |
)
|
633 |
|
634 |
return app
|
635 |
|
636 |
+
# --- Main Execution Block (Unchanged) ---
|
637 |
if __name__ == "__main__":
|
638 |
+
if AudioSegment is None:
|
639 |
+
print("\nFATAL ERROR: pydub is required but could not be imported.")
|
640 |
+
print("Please install it ('pip install pydub') and ensure ffmpeg is available.")
|
641 |
+
print("Application cannot start correctly.")
|
642 |
+
exit(1)
|
643 |
+
|
644 |
+
Path("prompts").mkdir(exist_ok=True)
|
645 |
+
Path("data").mkdir(exist_ok=True)
|
646 |
+
_prompt_dir = Path("prompts")
|
647 |
+
for key in PROMPT_KEYS:
|
648 |
+
prompt_file = _prompt_dir / f"{key}.txt"
|
649 |
+
if not prompt_file.exists():
|
650 |
+
# Ensure default prompts advise against markdown
|
651 |
+
default_content = f"This is the default placeholder prompt for {PROMPT_DISPLAY_NAMES[key]}. Process the transcript provided. Important: Generate the response as plain text only. Do not use any Markdown formatting (no '#', '*', '_', list formatting, bolding, etc.)."
|
652 |
+
if key == "titles_and_thumbnails": default_content += "\n\nExamples:\n{title_examples}"
|
653 |
+
elif key == "description": default_content += "\n\nExamples:\n{description_examples}"
|
654 |
+
elif key == "clips": default_content += "\n\nExamples:\n{clip_examples}"
|
655 |
+
elif key == "timestamps": default_content += "\n\nExamples:\n{timestamps_examples}"
|
656 |
+
prompt_file.write_text(default_content, encoding='utf-8')
|
657 |
+
print(f"Created dummy prompt file: {prompt_file}")
|
658 |
+
|
659 |
+
print("Starting Gradio application...")
|
660 |
+
app = create_interface()
|
661 |
+
app.launch()
|
662 |
+
print("Gradio application stopped.")
|