Auto-update: Thu Aug 8 21:13:23 PDT 2024

This commit is contained in:
sanj 2024-08-08 21:13:23 -07:00
parent 44071da1e2
commit 772637e957
3 changed files with 162 additions and 78 deletions

View file

@ -1,4 +1,5 @@
# __init__.py # __init__.py
import os import os
from pathlib import Path from pathlib import Path
import ipaddress import ipaddress
@ -18,12 +19,15 @@ os.makedirs(LOGS_DIR, exist_ok=True)
L = Logger("Central", LOGS_DIR) L = Logger("Central", LOGS_DIR)
# API essentials # API essentials
print("Loading API configuration...")
API = APIConfig.load('api', 'secrets') API = APIConfig.load('api', 'secrets')
Dir = DirConfig.load('dirs') Dir = DirConfig.load('dirs')
HOST = f"{API.BIND}:{API.PORT}" HOST = f"{API.BIND}:{API.PORT}"
LOCAL_HOSTS = [ipaddress.ip_address(localhost.strip()) for localhost in os.getenv('LOCAL_HOSTS', '127.0.0.1').split(',')] + ['localhost'] LOCAL_HOSTS = [ipaddress.ip_address(localhost.strip()) for localhost in os.getenv('LOCAL_HOSTS', '127.0.0.1').split(',')] + ['localhost']
SUBNET_BROADCAST = os.getenv("SUBNET_BROADCAST", '10.255.255.255') SUBNET_BROADCAST = os.getenv("SUBNET_BROADCAST", '10.255.255.255')
MAX_CPU_CORES = min(int(os.getenv("MAX_CPU_CORES", int(multiprocessing.cpu_count()/2))), multiprocessing.cpu_count()) MAX_CPU_CORES = min(int(os.getenv("MAX_CPU_CORES", int(multiprocessing.cpu_count()/2))), multiprocessing.cpu_count())
IMG = Configuration.load('img', 'secrets', Dir) IMG = Configuration.load('img', 'secrets', Dir)
Llm = Configuration.load('llm', 'secrets', Dir) Llm = Configuration.load('llm', 'secrets', Dir)
News = Configuration.load('news', 'secrets', Dir) News = Configuration.load('news', 'secrets', Dir)
@ -32,6 +36,39 @@ Scrape = Configuration.load('scrape', 'secrets', Dir)
Serve = Configuration.load('serve', 'secrets', Dir) Serve = Configuration.load('serve', 'secrets', Dir)
Tts = Configuration.load('tts', 'secrets', Dir) Tts = Configuration.load('tts', 'secrets', Dir)
print(f"Tts configuration loaded: {Tts}")
print(f"Tts.elevenlabs: {Tts.elevenlabs}")
print(f"Tts.elevenlabs.key: {Tts.elevenlabs.key}")
print(f"Tts.elevenlabs.voices: {Tts.elevenlabs.voices}")
# Additional debug logging for Configuration class
print(f"Configuration.resolve_placeholders method: {Configuration.resolve_placeholders}")
print(f"Configuration.resolve_string_placeholders method: {Configuration.resolve_string_placeholders}")
# Check if secrets are properly loaded
print(f"Secrets in Tts config: {[attr for attr in dir(Tts) if attr.isupper()]}")
# Verify the structure of Tts.elevenlabs
print(f"Type of Tts.elevenlabs: {type(Tts.elevenlabs)}")
print(f"Attributes of Tts.elevenlabs: {dir(Tts.elevenlabs)}")
# Check if the ElevenLabs API key is properly resolved
print(f"ElevenLabs API key (masked): {'*' * len(Tts.elevenlabs.key) if hasattr(Tts.elevenlabs, 'key') else 'Not found'}")
# Verify the structure of Tts.elevenlabs.voices
print(f"Type of Tts.elevenlabs.voices: {type(Tts.elevenlabs.voices)}")
print(f"Attributes of Tts.elevenlabs.voices: {dir(Tts.elevenlabs.voices)}")
# Check if the default voice is set
print(f"Default voice: {Tts.elevenlabs.default if hasattr(Tts.elevenlabs, 'default') else 'Not found'}")
# Additional checks
print(f"Is 'get' method available on Tts.elevenlabs.voices? {'get' in dir(Tts.elevenlabs.voices)}")
print(f"Is 'values' method available on Tts.elevenlabs.voices? {'values' in dir(Tts.elevenlabs.voices)}")
print("Initialization complete")
# Directories & general paths # Directories & general paths
ROUTER_DIR = BASE_DIR / "routers" ROUTER_DIR = BASE_DIR / "routers"
DATA_DIR = BASE_DIR / "data" DATA_DIR = BASE_DIR / "data"

View file

@ -68,6 +68,7 @@ class Logger:
def get_module_logger(self, module_name): def get_module_logger(self, module_name):
return self.logger.bind(name=module_name) return self.logger.bind(name=module_name)
L = Logger("classes", "classes") L = Logger("classes", "classes")
logger = L.get_module_logger("classes") logger = L.get_module_logger("classes")
def debug(text: str): logger.debug(text) def debug(text: str): logger.debug(text)
@ -103,17 +104,20 @@ class Configuration(BaseModel):
with secrets_path.open('r') as file: with secrets_path.open('r') as file:
secrets_data = yaml.safe_load(file) secrets_data = yaml.safe_load(file)
debug(f"Loaded secrets data from {secrets_path}") debug(f"Loaded secrets data from {secrets_path}")
if isinstance(config_data, list): if isinstance(config_data, list):
for item in config_data: config_data = {"configurations": config_data, "SECRETS": secrets_data}
if isinstance(item, dict): elif isinstance(config_data, dict):
item.update(secrets_data) config_data['SECRETS'] = secrets_data
else: else:
config_data.update(secrets_data) raise ValueError(f"Unexpected configuration data type: {type(config_data)}")
if isinstance(config_data, list):
if not isinstance(config_data, dict):
config_data = {"configurations": config_data} config_data = {"configurations": config_data}
if config_data.get('HOME') is None: if config_data.get('HOME') is None:
config_data['HOME'] = str(Path.home()) config_data['HOME'] = str(Path.home())
warn(f"HOME was None in config, set to default: {config_data['HOME']}") debug(f"HOME was None in config, set to default: {config_data['HOME']}")
load_dotenv() load_dotenv()
instance = cls.create_dynamic_model(**config_data) instance = cls.create_dynamic_model(**config_data)
@ -127,6 +131,7 @@ class Configuration(BaseModel):
err(f"Error loading configuration: {str(e)}") err(f"Error loading configuration: {str(e)}")
raise raise
@classmethod @classmethod
def _resolve_path(cls, path: Union[str, Path], default_dir: str) -> Path: def _resolve_path(cls, path: Union[str, Path], default_dir: str) -> Path:
base_path = Path(__file__).parent.parent # This will be two levels up from this file base_path = Path(__file__).parent.parent # This will be two levels up from this file
@ -137,6 +142,7 @@ class Configuration(BaseModel):
path = base_path / path path = base_path / path
return path return path
def resolve_placeholders(self, data: Any) -> Any: def resolve_placeholders(self, data: Any) -> Any:
if isinstance(data, dict): if isinstance(data, dict):
resolved_data = {k: self.resolve_placeholders(v) for k, v in data.items()} resolved_data = {k: self.resolve_placeholders(v) for k, v in data.items()}
@ -154,21 +160,13 @@ class Configuration(BaseModel):
else: else:
return data return data
def resolve_string_placeholders(self, value: str) -> Any: def resolve_string_placeholders(self, value: str) -> Any:
pattern = r'\{\{\s*([^}]+)\s*\}\}' if isinstance(value, str) and value.startswith('{{') and value.endswith('}}'):
matches = re.findall(pattern, value) key = value[2:-2].strip()
parts = key.split('.')
for match in matches:
parts = match.split('.')
if len(parts) == 2 and parts[0] == 'SECRET': if len(parts) == 2 and parts[0] == 'SECRET':
replacement = getattr(self, parts[1].strip(), '') return getattr(self.SECRETS, parts[1], '')
if not replacement:
warn(f"Secret '{parts[1].strip()}' not found in secrets file")
else:
replacement = getattr(self, match, value)
value = value.replace('{{' + match + '}}', str(replacement))
return value return value
@ -188,6 +186,41 @@ class Configuration(BaseModel):
) )
return DynamicModel(**data) return DynamicModel(**data)
def has_key(self, key_path: str) -> bool:
"""
Check if a key exists in the configuration or its nested objects.
:param key_path: Dot-separated path to the key (e.g., 'elevenlabs.voices.Victoria')
:return: True if the key exists, False otherwise
"""
parts = key_path.split('.')
current = self
for part in parts:
if hasattr(current, part):
current = getattr(current, part)
else:
return False
return True
def get_value(self, key_path: str, default=None):
"""
Get the value of a key in the configuration or its nested objects.
:param key_path: Dot-separated path to the key (e.g., 'elevenlabs.voices.Victoria')
:param default: Default value to return if the key doesn't exist
:return: The value of the key if it exists, otherwise the default value
"""
parts = key_path.split('.')
current = self
for part in parts:
if hasattr(current, part):
current = getattr(current, part)
else:
return default
return current
class Config: class Config:
extra = "allow" extra = "allow"
arbitrary_types_allowed = True arbitrary_types_allowed = True

View file

@ -63,7 +63,7 @@ async def list_11l_voices():
formatted_list += f"{name}: `{id}`\n" formatted_list += f"{name}: `{id}`\n"
except Exception as e: except Exception as e:
err(f"Error determining voice ID: {str(e)}") err(f"Error determining voice ID: {e}")
return PlainTextResponse(formatted_list, status_code=200) return PlainTextResponse(formatted_list, status_code=200)
@ -78,13 +78,13 @@ async def select_voice(voice_name: str) -> str:
debug(f"Checking {item.name.lower()}") debug(f"Checking {item.name.lower()}")
if item.name.lower() == f"{voice_name_lower}.wav": if item.name.lower() == f"{voice_name_lower}.wav":
debug(f"select_voice received query to use voice: {voice_name}. Found {item} inside {VOICE_DIR}.") debug(f"select_voice received query to use voice: {voice_name}. Found {item} inside {VOICE_DIR}.")
return str(item) return item
err(f"Voice file not found") err(f"Voice file not found")
raise HTTPException(status_code=404, detail="Voice file not found") raise HTTPException(status_code=404, detail="Voice file not found")
except Exception as e: except Exception as e:
err(f"Voice file not found: {str(e)}") err(f"Voice file not found: {e}")
return None return None
@ -119,7 +119,7 @@ async def generate_speech_endpoint(
else: else:
return await generate_speech(bg_tasks, text_content, voice, voice_file, model, speed, podcast) return await generate_speech(bg_tasks, text_content, voice, voice_file, model, speed, podcast)
except Exception as e: except Exception as e:
err(f"Error in TTS: {str(e)}") err(f"Error in TTS: {e}")
err(traceback.format_exc()) err(traceback.format_exc())
raise HTTPException(status_code=666, detail="error in TTS") raise HTTPException(status_code=666, detail="error in TTS")
@ -127,7 +127,7 @@ async def generate_speech_endpoint(
async def generate_speech( async def generate_speech(
bg_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
text: str, text: str,
voice: str = None, voice: Optional[str] = None,
voice_file: UploadFile = None, voice_file: UploadFile = None,
model: str = None, model: str = None,
speed: float = 1.1, speed: float = 1.1,
@ -153,8 +153,8 @@ async def generate_speech(
output_path = output_dir / f"{dt_datetime.now().strftime('%Y%m%d%H%M%S')} {title}.wav" output_path = output_dir / f"{dt_datetime.now().strftime('%Y%m%d%H%M%S')} {title}.wav"
debug(f"Model: {model}") debug(f"Model: {model}")
debug(f"API.EXTENSIONS.elevenlabs: {getattr(API.EXTENSIONS, 'elevenlabs', None)}") debug(f"Voice: {voice}")
debug(f"API.EXTENSIONS.xtts: {getattr(API.EXTENSIONS, 'xtts', None)}") debug(f"Tts.elevenlabs: {Tts.elevenlabs}")
if model == "eleven_turbo_v2" and getattr(API.EXTENSIONS, 'elevenlabs', False): if model == "eleven_turbo_v2" and getattr(API.EXTENSIONS, 'elevenlabs', False):
info("Using ElevenLabs.") info("Using ElevenLabs.")
@ -176,7 +176,7 @@ async def generate_speech(
if podcast: if podcast:
podcast_path = Path(Dir.PODCAST) / Path(audio_file_path).name podcast_path = Path(Dir.PODCAST) / Path(audio_file_path).name
shutil.copy(str(audio_file_path), str(podcast_path)) shutil.copy(audio_file_path, podcast_path)
if podcast_path.exists(): if podcast_path.exists():
info(f"Saved to podcast path: {podcast_path}") info(f"Saved to podcast path: {podcast_path}")
else: else:
@ -184,18 +184,18 @@ async def generate_speech(
if podcast_path != audio_file_path: if podcast_path != audio_file_path:
info(f"Podcast mode enabled, so we will remove {audio_file_path}") info(f"Podcast mode enabled, so we will remove {audio_file_path}")
bg_tasks.add_task(os.remove, str(audio_file_path)) bg_tasks.add_task(os.remove, audio_file_path)
else: else:
warn(f"Podcast path set to same as audio file path...") warn(f"Podcast path set to same as audio file path...")
return str(podcast_path) return podcast_path
return str(audio_file_path) return audio_file_path
except Exception as e: except Exception as e:
err(f"Failed to generate speech: {str(e)}") err(f"Failed to generate speech: {e}")
err(f"Traceback: {traceback.format_exc()}") err(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Failed to generate speech: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to generate speech: {e}")
@ -215,14 +215,15 @@ async def determine_voice_id(voice_name: str) -> str:
debug(f"Searching for voice id for {voice_name}") debug(f"Searching for voice id for {voice_name}")
debug(f"Tts.elevenlabs.voices: {Tts.elevenlabs.voices}") debug(f"Tts.elevenlabs.voices: {Tts.elevenlabs.voices}")
voices = Tts.elevenlabs.voices # Check if the voice is in the configured voices
if voice_name in voices: if voice_name and Tts.has_key(f'elevenlabs.voices.{voice_name}'):
return voices[voice_name] voice_id = Tts.get_value(f'elevenlabs.voices.{voice_name}')
debug(f"Found voice ID in config - {voice_id}")
return voice_id
debug(f"Requested voice not among the voices specified in config/tts.yaml. Checking with ElevenLabs API.") debug(f"Requested voice not among the voices specified in config/tts.yaml. Checking with ElevenLabs API using api_key: {Tts.elevenlabs.key}.")
url = "https://api.elevenlabs.io/v1/voices" url = "https://api.elevenlabs.io/v1/voices"
headers = {"xi-api-key": Tts.elevenlabs.key} headers = {"xi-api-key": Tts.elevenlabs.key}
debug(f"Using key: {Tts.elevenlabs.key}")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
response = await client.get(url, headers=headers) response = await client.get(url, headers=headers)
@ -237,14 +238,25 @@ async def determine_voice_id(voice_name: str) -> str:
err(f"Failed to get voices from ElevenLabs API. Status code: {response.status_code}") err(f"Failed to get voices from ElevenLabs API. Status code: {response.status_code}")
err(f"Response content: {response.text}") err(f"Response content: {response.text}")
except Exception as e: except Exception as e:
err(f"Error determining voice ID: {str(e)}") err(f"Error determining voice ID: {e}")
warn(f"Voice '{voice_name}' not found; using the default specified in config/tts.yaml: {Tts.elevenlabs.default}") warn(f"Voice '{voice_name}' not found; using the default specified in config/tts.yaml: {Tts.elevenlabs.default}")
return voices.get(Tts.elevenlabs.default, next(iter(voices.values()))) if Tts.has_key(f'elevenlabs.voices.{Tts.elevenlabs.default}'):
return Tts.get_value(f'elevenlabs.voices.{Tts.elevenlabs.default}')
else:
err(f"Default voice '{Tts.elevenlabs.default}' not found in configuration. Using first available voice.")
first_voice = next(iter(vars(Tts.elevenlabs.voices)))
return Tts.get_value(f'elevenlabs.voices.{first_voice}')
async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = None, output_dir: str = None): async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = None, output_dir: str = None):
# Debug logging
debug(f"API.EXTENSIONS: {API.EXTENSIONS}")
debug(f"API.EXTENSIONS.elevenlabs: {getattr(API.EXTENSIONS, 'elevenlabs', None)}")
debug(f"Tts config: {Tts}")
if getattr(API.EXTENSIONS, 'elevenlabs', False):
voice_id = await determine_voice_id(voice) voice_id = await determine_voice_id(voice)
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
@ -252,10 +264,10 @@ async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = N
"text": input_text, "text": input_text,
"model_id": model "model_id": model
} }
# Make sure this is the correct way to access the API key
headers = {"Content-Type": "application/json", "xi-api-key": Tts.elevenlabs.key} headers = {"Content-Type": "application/json", "xi-api-key": Tts.elevenlabs.key}
debug(f"Using ElevenLabs API key: {Tts.elevenlabs.key}")
try: try:
async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: # 5 minutes timeout async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client:
response = await client.post(url, json=payload, headers=headers) response = await client.post(url, json=payload, headers=headers)
output_dir = output_dir if output_dir else TTS_OUTPUT_DIR output_dir = output_dir if output_dir else TTS_OUTPUT_DIR
title = title if title else dt_datetime.now().strftime("%Y%m%d%H%M%S") title = title if title else dt_datetime.now().strftime("%Y%m%d%H%M%S")
@ -266,13 +278,15 @@ async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = N
audio_file.write(response.content) audio_file.write(response.content)
return file_path return file_path
else: else:
err(f"Error from ElevenLabs API. Status code: {response.status_code}") raise HTTPException(status_code=response.status_code, detail="Error from ElevenLabs API")
err(f"Response content: {response.text}")
raise HTTPException(status_code=response.status_code, detail=f"Error from ElevenLabs API: {response.text}")
except Exception as e: except Exception as e:
err(f"Error from Elevenlabs API: {e}") err(f"Error from Elevenlabs API: {e}")
raise HTTPException(status_code=500, detail=f"Error from ElevenLabs API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error from ElevenLabs API: {e}")
else:
warn(f"elevenlabs_tts called but ElevenLabs module is not enabled in config.")
raise HTTPException(status_code=400, detail="ElevenLabs TTS is not enabled")
@ -304,7 +318,7 @@ async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None)
existing_checksum = hashlib.md5(f.read()).hexdigest() existing_checksum = hashlib.md5(f.read()).hexdigest()
if checksum == existing_checksum: if checksum == existing_checksum:
return str(existing_file) return existing_file
base_name = existing_file.stem base_name = existing_file.stem
counter = 1 counter = 1
@ -315,7 +329,7 @@ async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None)
with open(new_file, 'wb') as f: with open(new_file, 'wb') as f:
f.write(content) f.write(content)
return str(new_file) return new_file
else: else:
debug(f"No voice specified or file provided, using default voice: {Tts.xtts.default}") debug(f"No voice specified or file provided, using default voice: {Tts.xtts.default}")
@ -367,14 +381,14 @@ async def local_tts(
XTTS.tts_to_file, XTTS.tts_to_file,
text=segment, text=segment,
speed=speed, speed=speed,
file_path=str(segment_file_path), file_path=segment_file_path,
speaker_wav=[voice_file_path], speaker_wav=[voice_file_path],
language="en" language="en"
) )
debug(f"Segment file generated: {segment_file_path}") debug(f"Segment file generated: {segment_file_path}")
# Load and combine audio in a separate thread # Load and combine audio in a separate thread
segment_audio = await asyncio.to_thread(AudioSegment.from_wav, str(segment_file_path)) segment_audio = await asyncio.to_thread(AudioSegment.from_wav, segment_file_path)
combined_audio += segment_audio combined_audio += segment_audio
# Delete the segment file # Delete the segment file
@ -387,7 +401,7 @@ async def local_tts(
await asyncio.to_thread(combined_audio.export, file_path, format="wav") await asyncio.to_thread(combined_audio.export, file_path, format="wav")
return str(file_path) return file_path
else: else:
warn(f"local_tts called but xtts module disabled!") warn(f"local_tts called but xtts module disabled!")
@ -501,4 +515,4 @@ def copy_to_podcast_dir(file_path):
print(f"Permission denied while copying the file: {file_path}") print(f"Permission denied while copying the file: {file_path}")
except Exception as e: except Exception as e:
print(f"An error occurred while copying the file: {file_path}") print(f"An error occurred while copying the file: {file_path}")
print(f"Error details: {str(e)}") print(f"Error details: {e}")