Merge branch 'main' of https://huggingface.co/spaces/phxdev/podcaster
Browse files- Dockerfile +1 -1
- README.md +6 -77
- api_clients.py +59 -73
- app.py +116 -34
- config.py +8 -0
- models.py +25 -0
- requirements.txt +5 -2
Dockerfile
CHANGED
|
@@ -57,6 +57,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 57 |
# Copy application code
|
| 58 |
COPY . .
|
| 59 |
|
| 60 |
-
|
| 61 |
# Start the realtime.py script
|
| 62 |
CMD ["python", "interface.py"]
|
|
|
|
| 57 |
# Copy application code
|
| 58 |
COPY . .
|
| 59 |
|
| 60 |
+
EXPOSE 7863
|
| 61 |
# Start the realtime.py script
|
| 62 |
CMD ["python", "interface.py"]
|
README.md
CHANGED
|
@@ -7,85 +7,14 @@ sdk: gradio
|
|
| 7 |
sdk_version: "5.12.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
#
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
##
|
| 18 |
|
| 19 |
-
|
| 20 |
-
gradio-podcast-generator
|
| 21 |
-
βββ src
|
| 22 |
-
β βββ app.py # Entry point of the Gradio application
|
| 23 |
-
β βββ scraper.py # Contains the scraping logic
|
| 24 |
-
β βββ podcast_generator.py # Generates the podcast episode
|
| 25 |
-
β βββ tts.py # Converts text to speech
|
| 26 |
-
βββ requirements.txt # Lists project dependencies
|
| 27 |
-
βββ README.md # Project documentation
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
## Setup Instructions
|
| 31 |
-
|
| 32 |
-
1. Clone the repository:
|
| 33 |
-
```
|
| 34 |
-
git clone https://github.com/yourusername/gradio-podcast-generator.git
|
| 35 |
-
cd gradio-podcast-generator
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
2. Install the required dependencies:
|
| 39 |
-
```
|
| 40 |
-
pip install -r requirements.txt
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
## Flask Configuration
|
| 44 |
-
|
| 45 |
-
The application uses Flask with Gradio integration. Here's how to set it up and run it:
|
| 46 |
-
|
| 47 |
-
1. Install the requirements:
|
| 48 |
-
```bash
|
| 49 |
-
pip install flask gradio
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
2. Configure the environment:
|
| 53 |
-
```bash
|
| 54 |
-
export FLASK_APP=app.py
|
| 55 |
-
export FLASK_ENV=development # For development mode
|
| 56 |
-
```
|
| 57 |
-
|
| 58 |
-
3. Run the application:
|
| 59 |
-
```bash
|
| 60 |
-
python app.py
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
The server will start on `http://0.0.0.0:7860` with the following configuration:
|
| 64 |
-
- Host: 0.0.0.0 (accessible from any IP)
|
| 65 |
-
- Port: 7860
|
| 66 |
-
- Debug mode: Enabled
|
| 67 |
-
- Gradio interface: Mounted at root path '/'
|
| 68 |
-
|
| 69 |
-
## Usage
|
| 70 |
-
|
| 71 |
-
To run the Gradio application, execute the following command in your terminal:
|
| 72 |
-
|
| 73 |
-
```
|
| 74 |
-
python src/app.py
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
-
Once the application is running, you can input a URL into the Gradio interface. The application will scrape the content from the URL, generate a podcast episode, and provide an audio output.
|
| 78 |
-
|
| 79 |
-
## Functionality
|
| 80 |
-
|
| 81 |
-
- **Scraping**: The application uses the `scraper.py` module to extract relevant text from the provided URL.
|
| 82 |
-
- **Podcast Generation**: The `podcast_generator.py` module utilizes the `eva-unit-01/eva-llama-3.33-70b` model to create a podcast script that is no longer than 3 minutes.
|
| 83 |
-
- **Text-to-Speech**: The `tts.py` module converts the generated podcast script into audio using the ElevenLabs TTS API.
|
| 84 |
-
|
| 85 |
-
## Contributing
|
| 86 |
-
|
| 87 |
-
Contributions are welcome! Please feel free to submit a pull request or open an issue for any enhancements or bug fixes.
|
| 88 |
-
|
| 89 |
-
## License
|
| 90 |
-
|
| 91 |
-
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
|
|
| 7 |
sdk_version: "5.12.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
short_description: A Podcast Generator powered by FastAPI and Gradio
|
| 11 |
+
python_version: "3.12"
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# URL to Podcast Generator
|
| 15 |
|
| 16 |
+
A FastAPI application with Gradio interface for generating podcasts from web content.
|
| 17 |
|
| 18 |
+
## Running the Application
|
| 19 |
|
| 20 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api_clients.py
CHANGED
|
@@ -2,8 +2,10 @@ from functools import lru_cache
|
|
| 2 |
from typing import List, Tuple, Optional
|
| 3 |
import aiohttp
|
| 4 |
import elevenlabs
|
|
|
|
| 5 |
from contextlib import asynccontextmanager
|
| 6 |
from logger import setup_logger, log_execution_time, log_async_execution_time
|
|
|
|
| 7 |
|
| 8 |
logger = setup_logger("api_clients")
|
| 9 |
|
|
@@ -12,18 +14,31 @@ class OpenRouterClient:
|
|
| 12 |
|
| 13 |
def __init__(self, api_key: str):
|
| 14 |
logger.info("Initializing OpenRouter client")
|
| 15 |
-
if not api_key or len(api_key) < 32:
|
| 16 |
-
logger.error("Invalid API key format")
|
| 17 |
-
raise ValueError("Invalid OpenRouter API key")
|
| 18 |
-
|
| 19 |
self.api_key = api_key
|
| 20 |
self.base_url = "https://openrouter.ai/api/v1"
|
| 21 |
self.headers = {
|
| 22 |
"Authorization": f"Bearer {api_key}",
|
| 23 |
-
"Content-Type": "application/json"
|
| 24 |
}
|
| 25 |
logger.debug("OpenRouter client initialized successfully")
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
@asynccontextmanager
|
| 28 |
async def get_session(self):
|
| 29 |
logger.debug("Creating new aiohttp session")
|
|
@@ -33,7 +48,7 @@ class OpenRouterClient:
|
|
| 33 |
@lru_cache(maxsize=1)
|
| 34 |
async def get_models(self) -> List[Tuple[str, str]]:
|
| 35 |
"""
|
| 36 |
-
Fetch available models from OpenRouter API
|
| 37 |
|
| 38 |
Returns:
|
| 39 |
List of tuples containing (model_id, model_description)
|
|
@@ -44,16 +59,11 @@ class OpenRouterClient:
|
|
| 44 |
logger.info("Fetching available models from OpenRouter")
|
| 45 |
async with self.get_session() as session:
|
| 46 |
async with session.get(f"{self.base_url}/models") as response:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
raise ValueError(f"Failed to fetch models: {error_msg}")
|
| 51 |
-
|
| 52 |
-
models = await response.json()
|
| 53 |
logger.info(f"Successfully fetched {len(models)} models")
|
| 54 |
-
|
| 55 |
-
return [(model['id'], f"{model['name']} ({model['context_length']} tokens)")
|
| 56 |
-
for model in models]
|
| 57 |
|
| 58 |
@log_async_execution_time(logger)
|
| 59 |
async def generate_script(self, content: str, prompt: str, model_id: str) -> str:
|
|
@@ -75,77 +85,53 @@ class OpenRouterClient:
|
|
| 75 |
raise ValueError("Please provide a more detailed prompt")
|
| 76 |
|
| 77 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
async with self.get_session() as session:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
except Exception as e:
|
| 88 |
logger.error(f"Script generation failed", exc_info=True)
|
| 89 |
raise
|
| 90 |
|
| 91 |
-
async def _make_script_request(self, session, content, prompt, model_id):
|
| 92 |
-
async with session.post(
|
| 93 |
-
f"{self.base_url}/chat/completions",
|
| 94 |
-
json={
|
| 95 |
-
"model": model_id,
|
| 96 |
-
"messages": [
|
| 97 |
-
{
|
| 98 |
-
"role": "system",
|
| 99 |
-
"content": "You are an expert podcast script writer. Create engaging, conversational content."
|
| 100 |
-
},
|
| 101 |
-
{
|
| 102 |
-
"role": "user",
|
| 103 |
-
"content": f"""Based on this content: {content}
|
| 104 |
-
Create a 3-minute podcast script focusing on: {prompt}
|
| 105 |
-
Format as a natural conversation with clear speaker parts.
|
| 106 |
-
Include [HOST] and [GUEST] markers for different voices."""
|
| 107 |
-
}
|
| 108 |
-
]
|
| 109 |
-
}
|
| 110 |
-
) as response:
|
| 111 |
-
logger.debug("Sending script generation request")
|
| 112 |
-
|
| 113 |
-
if response.status != 200:
|
| 114 |
-
error_msg = await response.text()
|
| 115 |
-
logger.error(f"Script generation failed: {error_msg}")
|
| 116 |
-
raise ValueError(f"Script generation failed: {error_msg}")
|
| 117 |
-
|
| 118 |
-
return await response.json()
|
| 119 |
-
|
| 120 |
class ElevenLabsClient:
|
| 121 |
-
"""Handles ElevenLabs API interactions with detailed performance tracking"""
|
| 122 |
-
|
| 123 |
def __init__(self, api_key: str):
|
| 124 |
-
logger.info("Initializing ElevenLabs client")
|
| 125 |
self.api_key = api_key
|
| 126 |
elevenlabs.set_api_key(api_key)
|
| 127 |
-
|
| 128 |
-
@lru_cache(maxsize=1)
|
| 129 |
def get_voices(self) -> List[Tuple[str, str]]:
|
| 130 |
"""
|
| 131 |
-
|
| 132 |
|
| 133 |
Returns:
|
| 134 |
-
List of tuples containing (voice_id,
|
| 135 |
-
|
| 136 |
-
logger.info("Fetching available voices from ElevenLabs")
|
| 137 |
-
voices = elevenlabs.voices()
|
| 138 |
-
logger.info(f"Successfully fetched {len(voices)} voices")
|
| 139 |
-
logger.debug(f"Available voices: {[voice.name for voice in voices]}")
|
| 140 |
-
return [(voice.voice_id, voice.name) for voice in voices]
|
| 141 |
-
|
| 142 |
-
@log_execution_time(logger)
|
| 143 |
-
def generate_audio(self, text: str, voice_id: str) -> bytes:
|
| 144 |
-
"""
|
| 145 |
-
Generate audio with comprehensive error handling and quality checks
|
| 146 |
-
|
| 147 |
-
Logs detailed metrics about the input text and resulting audio.
|
| 148 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
logger.info(f"Starting audio generation with voice: {voice_id}")
|
| 150 |
logger.debug(f"Input text length: {len(text)} chars")
|
| 151 |
|
|
@@ -154,7 +140,7 @@ class ElevenLabsClient:
|
|
| 154 |
|
| 155 |
try:
|
| 156 |
start_time = time.time()
|
| 157 |
-
audio = elevenlabs.generate(
|
| 158 |
text=text,
|
| 159 |
voice=voice_id,
|
| 160 |
model="eleven_monolingual_v1"
|
|
|
|
| 2 |
from typing import List, Tuple, Optional
|
| 3 |
import aiohttp
|
| 4 |
import elevenlabs
|
| 5 |
+
import time
|
| 6 |
from contextlib import asynccontextmanager
|
| 7 |
from logger import setup_logger, log_execution_time, log_async_execution_time
|
| 8 |
+
from models import OpenRouterRequest, OpenRouterResponse, Message, OpenRouterModel
|
| 9 |
|
| 10 |
logger = setup_logger("api_clients")
|
| 11 |
|
|
|
|
| 14 |
|
| 15 |
def __init__(self, api_key: str):
|
| 16 |
logger.info("Initializing OpenRouter client")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
self.api_key = api_key
|
| 18 |
self.base_url = "https://openrouter.ai/api/v1"
|
| 19 |
self.headers = {
|
| 20 |
"Authorization": f"Bearer {api_key}",
|
| 21 |
+
"Content-Type": "application/json"
|
| 22 |
}
|
| 23 |
logger.debug("OpenRouter client initialized successfully")
|
| 24 |
|
| 25 |
+
@property
|
| 26 |
+
def api_key(self):
|
| 27 |
+
return self._api_key
|
| 28 |
+
|
| 29 |
+
@api_key.setter
|
| 30 |
+
def api_key(self, value: str):
|
| 31 |
+
if not value or len(value) < 32:
|
| 32 |
+
logger.error("Invalid API key format")
|
| 33 |
+
raise ValueError("Invalid OpenRouter API key")
|
| 34 |
+
self._api_key = value
|
| 35 |
+
# Update headers when API key changes
|
| 36 |
+
self.headers = {
|
| 37 |
+
"Authorization": f"Bearer {value}",
|
| 38 |
+
"Content-Type": "application/json",
|
| 39 |
+
}
|
| 40 |
+
logger.info("OpenRouter API key updated successfully")
|
| 41 |
+
|
| 42 |
@asynccontextmanager
|
| 43 |
async def get_session(self):
|
| 44 |
logger.debug("Creating new aiohttp session")
|
|
|
|
| 48 |
@lru_cache(maxsize=1)
|
| 49 |
async def get_models(self) -> List[Tuple[str, str]]:
|
| 50 |
"""
|
| 51 |
+
Fetch available models from OpenRouter API using pydantic models
|
| 52 |
|
| 53 |
Returns:
|
| 54 |
List of tuples containing (model_id, model_description)
|
|
|
|
| 59 |
logger.info("Fetching available models from OpenRouter")
|
| 60 |
async with self.get_session() as session:
|
| 61 |
async with session.get(f"{self.base_url}/models") as response:
|
| 62 |
+
response.raise_for_status()
|
| 63 |
+
data = await response.json()
|
| 64 |
+
models = [OpenRouterModel(**model) for model in data["data"]]
|
|
|
|
|
|
|
|
|
|
| 65 |
logger.info(f"Successfully fetched {len(models)} models")
|
| 66 |
+
return [(model.id, model.name) for model in models]
|
|
|
|
|
|
|
| 67 |
|
| 68 |
@log_async_execution_time(logger)
|
| 69 |
async def generate_script(self, content: str, prompt: str, model_id: str) -> str:
|
|
|
|
| 85 |
raise ValueError("Please provide a more detailed prompt")
|
| 86 |
|
| 87 |
try:
|
| 88 |
+
request = OpenRouterRequest(
|
| 89 |
+
model=model_id,
|
| 90 |
+
messages=[
|
| 91 |
+
Message(role="system", content="You are a podcast script writer."),
|
| 92 |
+
Message(role="user", content=f"Create a podcast script from this content: {content}")
|
| 93 |
+
]
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
async with self.get_session() as session:
|
| 97 |
+
async with session.post(
|
| 98 |
+
f"{self.base_url}/chat/completions",
|
| 99 |
+
json=request.dict()
|
| 100 |
+
) as response:
|
| 101 |
+
response.raise_for_status()
|
| 102 |
+
data = await response.json()
|
| 103 |
+
router_response = OpenRouterResponse(**data)
|
| 104 |
+
return router_response.choices[0].message.content
|
| 105 |
except Exception as e:
|
| 106 |
logger.error(f"Script generation failed", exc_info=True)
|
| 107 |
raise
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
class ElevenLabsClient:
|
|
|
|
|
|
|
| 110 |
def __init__(self, api_key: str):
|
|
|
|
| 111 |
self.api_key = api_key
|
| 112 |
elevenlabs.set_api_key(api_key)
|
| 113 |
+
|
|
|
|
| 114 |
def get_voices(self) -> List[Tuple[str, str]]:
|
| 115 |
"""
|
| 116 |
+
Synchronously get available voices from ElevenLabs
|
| 117 |
|
| 118 |
Returns:
|
| 119 |
+
List of tuples containing (voice_id, display_name)
|
| 120 |
+
where display_name shows the name and description but not the ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
"""
|
| 122 |
+
try:
|
| 123 |
+
voices = elevenlabs.voices()
|
| 124 |
+
return [(
|
| 125 |
+
voice.voice_id, # Value (hidden from user)
|
| 126 |
+
f"{voice.name} ({voice.labels.get('accent', 'No accent')})" +
|
| 127 |
+
(f" - {voice.description[:50]}..." if voice.description else "")
|
| 128 |
+
) for voice in voices]
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.error("Failed to fetch voices from ElevenLabs", exc_info=True)
|
| 131 |
+
raise
|
| 132 |
+
|
| 133 |
+
async def generate_audio(self, text: str, voice_id: str):
|
| 134 |
+
"""Asynchronously generate audio"""
|
| 135 |
logger.info(f"Starting audio generation with voice: {voice_id}")
|
| 136 |
logger.debug(f"Input text length: {len(text)} chars")
|
| 137 |
|
|
|
|
| 140 |
|
| 141 |
try:
|
| 142 |
start_time = time.time()
|
| 143 |
+
audio = await elevenlabs.generate( # Assuming elevenlabs supports async
|
| 144 |
text=text,
|
| 145 |
voice=voice_id,
|
| 146 |
model="eleven_monolingual_v1"
|
app.py
CHANGED
|
@@ -1,38 +1,120 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
| 3 |
from scraper import scrape_url
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
if __name__ == "__main__":
|
| 34 |
-
|
| 35 |
-
host="0.0.0.0",
|
| 36 |
-
port=7860,
|
| 37 |
-
debug=True
|
| 38 |
-
)
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
import gradio as gr
|
| 4 |
+
from api_clients import OpenRouterClient, ElevenLabsClient
|
| 5 |
+
from logger import setup_logger
|
| 6 |
+
from config import Config
|
| 7 |
from scraper import scrape_url
|
| 8 |
+
|
| 9 |
+
logger = setup_logger("app")
|
| 10 |
+
|
| 11 |
+
# Default choices for dropdowns
|
| 12 |
+
default_voices = [("", "Enter API key to load voices")]
|
| 13 |
+
default_models = [("", "Enter API key to load models")]
|
| 14 |
+
|
| 15 |
+
class PodcasterUI:
|
| 16 |
+
def __init__(self, config: Config):
|
| 17 |
+
self.config = config
|
| 18 |
+
self.router_client = OpenRouterClient(os.getenv('OPENROUTER_API_KEY', ''))
|
| 19 |
+
self.elevenlabs_client = ElevenLabsClient(os.getenv('ELEVENLABS_API_KEY', ''))
|
| 20 |
+
# Store models and voices as instance variables
|
| 21 |
+
self.models = default_models
|
| 22 |
+
self.voices = default_voices
|
| 23 |
+
|
| 24 |
+
async def initialize(self):
|
| 25 |
+
"""Initialize API clients and fetch models/voices"""
|
| 26 |
+
try:
|
| 27 |
+
self.models = await self.router_client.get_models()
|
| 28 |
+
# Since get_voices() might not be async, remove await
|
| 29 |
+
self.voices = self.elevenlabs_client.get_voices()
|
| 30 |
+
logger.info(f"Initialized with {len(self.voices)} voices and {len(self.models)} models")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
logger.error("Failed to initialize API clients", exc_info=True)
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
async def on_submit(self, content: str, model_id: str, voice_id: str, prompt: str = "") -> tuple:
|
| 36 |
+
"""Handle form submission with async API calls"""
|
| 37 |
+
try:
|
| 38 |
+
# First scrape the webpage content
|
| 39 |
+
webpage_content = scrape_url(content)
|
| 40 |
+
if not webpage_content:
|
| 41 |
+
return "Failed to extract content from URL", None
|
| 42 |
+
|
| 43 |
+
# Generate script using the scraped content
|
| 44 |
+
script = await self.router_client.generate_script(webpage_content, prompt, model_id)
|
| 45 |
+
|
| 46 |
+
# Generate audio from the script
|
| 47 |
+
audio = await self.elevenlabs_client.generate_audio(script, voice_id)
|
| 48 |
+
return script, audio
|
| 49 |
+
except Exception as e:
|
| 50 |
+
logger.error("Failed to generate podcast", exc_info=True)
|
| 51 |
+
return str(e), None
|
| 52 |
+
|
| 53 |
+
def create_ui(self) -> gr.Interface:
|
| 54 |
+
with gr.Blocks(title='URL to Podcast Generator', theme='huggingface') as interface:
|
| 55 |
+
gr.Markdown('# URL to Podcast Generator')
|
| 56 |
+
gr.Markdown('Enter a URL to generate a podcast episode based on its content.')
|
| 57 |
+
|
| 58 |
+
with gr.Row():
|
| 59 |
+
with gr.Column(scale=2):
|
| 60 |
+
url_input = gr.Textbox(
|
| 61 |
+
label="Website URL",
|
| 62 |
+
placeholder="Enter the URL of the website you want to convert to a podcast"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
with gr.Row():
|
| 66 |
+
with gr.Column():
|
| 67 |
+
openrouter_model = gr.Dropdown(
|
| 68 |
+
label='AI Model',
|
| 69 |
+
choices=[(name, id) for id, name in self.models], # Swap order for display
|
| 70 |
+
value=self.models[0][1] if len(self.models) > 1 else None,
|
| 71 |
+
type="index" # Use index to get the second element (id) from tuple
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
with gr.Column():
|
| 75 |
+
voice_model = gr.Dropdown(
|
| 76 |
+
label='Voice',
|
| 77 |
+
choices=[(name, id) for id, name in self.voices], # Swap order for display
|
| 78 |
+
value=self.voices[0][1] if len(self.voices) > 1 else None,
|
| 79 |
+
type="index" # Use index to get the second element (id) from tuple
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
prompt_input = gr.Textbox(
|
| 83 |
+
label="Custom Prompt",
|
| 84 |
+
placeholder="Enter a custom prompt to guide the podcast generation (optional)",
|
| 85 |
+
lines=3
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
submit_btn = gr.Button('Generate Podcast', variant='primary')
|
| 89 |
+
|
| 90 |
+
with gr.Column(scale=1):
|
| 91 |
+
script_output = gr.Textbox(label="Generated Script", interactive=False)
|
| 92 |
+
audio_output = gr.Audio(label="Generated Podcast")
|
| 93 |
+
status = gr.Textbox(label='Status', interactive=False)
|
| 94 |
+
|
| 95 |
+
submit_btn.click(
|
| 96 |
+
fn=self.on_submit,
|
| 97 |
+
inputs=[url_input, openrouter_model, voice_model, prompt_input],
|
| 98 |
+
outputs=[script_output, audio_output]
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return interface
|
| 102 |
+
|
| 103 |
+
def main():
|
| 104 |
+
config = Config()
|
| 105 |
+
app = PodcasterUI(config)
|
| 106 |
+
|
| 107 |
+
# Initialize before creating UI
|
| 108 |
+
loop = asyncio.get_event_loop()
|
| 109 |
+
loop.run_until_complete(app.initialize())
|
| 110 |
+
|
| 111 |
+
# Create UI with populated data
|
| 112 |
+
interface = app.create_ui()
|
| 113 |
+
interface.launch(
|
| 114 |
+
server_name="0.0.0.0",
|
| 115 |
+
server_port=7860,
|
| 116 |
+
share=True
|
| 117 |
+
)
|
| 118 |
|
| 119 |
if __name__ == "__main__":
|
| 120 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
config.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
class Config:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
load_dotenv()
|
| 7 |
+
self.openrouter_api_key = os.getenv('OPENROUTER_API_KEY', '')
|
| 8 |
+
self.elevenlabs_api_key = os.getenv('ELEVENLABS_API_KEY', '')
|
models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
class Message(BaseModel):
|
| 5 |
+
role: str
|
| 6 |
+
content: str
|
| 7 |
+
|
| 8 |
+
class OpenRouterRequest(BaseModel):
|
| 9 |
+
model: str
|
| 10 |
+
messages: List[Message]
|
| 11 |
+
|
| 12 |
+
class Choice(BaseModel):
|
| 13 |
+
message: Message
|
| 14 |
+
index: int = 0
|
| 15 |
+
finish_reason: Optional[str] = None
|
| 16 |
+
|
| 17 |
+
class OpenRouterResponse(BaseModel):
|
| 18 |
+
id: str
|
| 19 |
+
choices: List[Choice]
|
| 20 |
+
model: str
|
| 21 |
+
|
| 22 |
+
class OpenRouterModel(BaseModel):
|
| 23 |
+
id: str
|
| 24 |
+
name: str
|
| 25 |
+
description: Optional[str] = None
|
requirements.txt
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
gradio==3.0.0
|
| 2 |
browser-use
|
| 3 |
elevenlabs==0.2.26
|
| 4 |
-
flask==3.0.0
|
| 5 |
pydub==0.25.1 # audio processing library
|
| 6 |
python-dotenv==1.0.0 # for environment variables
|
| 7 |
requests==2.31.0 # for API calls
|
| 8 |
numpy>1.24.3 # common dependency
|
| 9 |
openrouter
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
gradio==3.0.0
|
| 2 |
browser-use
|
| 3 |
elevenlabs==0.2.26
|
|
|
|
| 4 |
pydub==0.25.1 # audio processing library
|
| 5 |
python-dotenv==1.0.0 # for environment variables
|
| 6 |
requests==2.31.0 # for API calls
|
| 7 |
numpy>1.24.3 # common dependency
|
| 8 |
openrouter
|
| 9 |
+
uvicorn
|
| 10 |
+
fastapi
|
| 11 |
+
langchain_anthropic
|
| 12 |
+
langchain_openai
|
| 13 |
+
langchain_google_genai
|