Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- generate_audio.py +50 -0
- logger_setup.py +16 -0
- requirements.txt +11 -0
- utils.py +59 -0
generate_audio.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
from elevenlabs.client import ElevenLabs
|
4 |
+
from logger_setup import logger
|
5 |
+
|
6 |
+
# Load environment variables
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
# Use absolute path for output
|
10 |
+
AUDIO_DIR = os.path.join(os.path.dirname(__file__), "audio_outputs")
|
11 |
+
|
12 |
+
# Verify API key
|
13 |
+
api_key = os.getenv("ELEVENLABS_API_KEY")
|
14 |
+
if not api_key:
|
15 |
+
logger.error("β ELEVENLABS_API_KEY is missing or not loaded from .env")
|
16 |
+
raise RuntimeError("ELEVENLABS_API_KEY missing")
|
17 |
+
|
18 |
+
client = ElevenLabs(api_key=api_key)
|
19 |
+
|
20 |
+
def generate_audio(text: str, voice_id: str, audio_key: str):
|
21 |
+
try:
|
22 |
+
logger.info("π― Starting ElevenLabs audio generation")
|
23 |
+
os.makedirs(AUDIO_DIR, exist_ok=True)
|
24 |
+
|
25 |
+
try:
|
26 |
+
audio_stream = client.text_to_speech.convert_as_stream(
|
27 |
+
text=text,
|
28 |
+
voice_id=voice_id,
|
29 |
+
model_id="eleven_multilingual_v2"
|
30 |
+
)
|
31 |
+
logger.info("β
Audio stream received from ElevenLabs")
|
32 |
+
except Exception as stream_err:
|
33 |
+
logger.error(f"β Failed to get audio stream: {stream_err}")
|
34 |
+
raise
|
35 |
+
|
36 |
+
output_path = os.path.join(AUDIO_DIR, f"{audio_key}.mp3")
|
37 |
+
|
38 |
+
try:
|
39 |
+
with open(output_path, "wb") as f:
|
40 |
+
for chunk in audio_stream:
|
41 |
+
if isinstance(chunk, bytes):
|
42 |
+
f.write(chunk)
|
43 |
+
logger.info(f"β
Audio saved to {output_path}")
|
44 |
+
except Exception as write_err:
|
45 |
+
logger.error(f"β Failed to save audio to file: {write_err}")
|
46 |
+
raise
|
47 |
+
|
48 |
+
except Exception as e:
|
49 |
+
logger.exception("π₯ Exception in generate_audio")
|
50 |
+
raise
|
logger_setup.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# logger_setup.py
|
2 |
+
import logging
|
3 |
+
import os
|
4 |
+
|
5 |
+
LOG_FILE = os.path.join(os.path.dirname(__file__), "logfile.log")
|
6 |
+
|
7 |
+
logging.basicConfig(
|
8 |
+
level=logging.INFO,
|
9 |
+
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
|
10 |
+
handlers=[
|
11 |
+
logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8'),
|
12 |
+
logging.StreamHandler()
|
13 |
+
]
|
14 |
+
)
|
15 |
+
|
16 |
+
logger = logging.getLogger("voice-agent")
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
requests
|
3 |
+
openai
|
4 |
+
python-dotenv
|
5 |
+
PyMuPDF
|
6 |
+
python-docx
|
7 |
+
elevenlabs
|
8 |
+
|
9 |
+
qdrant-client
|
10 |
+
fastembed
|
11 |
+
firecrawl
|
utils.py
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
from urllib.parse import urlparse
|
4 |
+
from bs4 import BeautifulSoup
|
5 |
+
|
6 |
+
AUDIO_DIR = "audio_outputs"
|
7 |
+
|
8 |
+
voice_map = {'grandma GG': 'rKVm0Cb9J2wrzmZupJea', 'tech wizard': 'ocn9CucaUfmmP6Two6Ik', 'perky sidekick': 'DWR3ijzKmphlRUhbBI7t', 'bill the newscaster': 'R1vZMopVRO75M5xBKX52', 'spunky charlie': 'q3yXDjF0aq4JCEo9u2g4', 'sassy teen': 'mBj2IDD9aXruPJHLGCAv'}
|
9 |
+
|
10 |
+
def sanitize_url(url):
|
11 |
+
if not url.startswith(("http://", "https://")):
|
12 |
+
return "https://" + url
|
13 |
+
return url
|
14 |
+
|
15 |
+
def extract_internal_links(html_content, base_url):
|
16 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
17 |
+
parsed_base = urlparse(base_url)
|
18 |
+
base_domain = parsed_base.netloc
|
19 |
+
|
20 |
+
links = set()
|
21 |
+
for tag in soup.find_all("a", href=True):
|
22 |
+
href = tag["href"]
|
23 |
+
parsed_href = urlparse(href)
|
24 |
+
|
25 |
+
if parsed_href.netloc == "" or parsed_href.netloc == base_domain:
|
26 |
+
full_url = parsed_href.geturl()
|
27 |
+
if not full_url.startswith("http"):
|
28 |
+
full_url = f"{parsed_base.scheme}://{base_domain}{href}"
|
29 |
+
links.add(full_url)
|
30 |
+
|
31 |
+
return list(links)
|
32 |
+
|
33 |
+
def crawl_documentation(url):
|
34 |
+
import requests
|
35 |
+
try:
|
36 |
+
response = requests.get(url, timeout=10)
|
37 |
+
response.raise_for_status()
|
38 |
+
return response.text
|
39 |
+
except Exception as e:
|
40 |
+
return f"Error fetching page: {e}"
|
41 |
+
|
42 |
+
def get_voice_prompt_style(voice):
|
43 |
+
tone = {'grandma GG': 'dry, witty, and brutally honest β will roast you if you mess up.', 'tech wizard': 'cryptic, snarky, and a prodigy with code β speaks in digital spells.', 'perky sidekick': 'energetic, cheerful, and endlessly supportive β like a high-five machine.', 'bill the newscaster': 'polished, confident, and composed β delivers everything like breaking news.', 'spunky charlie': 'wildly curious, playful, and full of devil-may-care energy.', 'sassy teen': 'sarcastic, sharp-tongued, and too cool to care β flexes brainpower with attitude.'}
|
44 |
+
return tone.get(voice.lower(), "neutral")
|
45 |
+
|
46 |
+
def save_audio_file(audio_path, content):
|
47 |
+
os.makedirs(AUDIO_DIR, exist_ok=True)
|
48 |
+
with open(audio_path, "wb") as f:
|
49 |
+
f.write(content)
|
50 |
+
|
51 |
+
__all__ = [
|
52 |
+
"sanitize_url",
|
53 |
+
"extract_internal_links",
|
54 |
+
"crawl_documentation",
|
55 |
+
"get_voice_prompt_style",
|
56 |
+
"save_audio_file",
|
57 |
+
"voice_map",
|
58 |
+
"AUDIO_DIR",
|
59 |
+
]
|