Auto-update: Thu Aug 8 21:13:23 PDT 2024
This commit is contained in:
parent
44071da1e2
commit
772637e957
3 changed files with 162 additions and 78 deletions
|
@ -1,4 +1,5 @@
|
|||
# __init__.py
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import ipaddress
|
||||
|
@ -18,12 +19,15 @@ os.makedirs(LOGS_DIR, exist_ok=True)
|
|||
L = Logger("Central", LOGS_DIR)
|
||||
|
||||
# API essentials
|
||||
print("Loading API configuration...")
|
||||
API = APIConfig.load('api', 'secrets')
|
||||
Dir = DirConfig.load('dirs')
|
||||
|
||||
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']
|
||||
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())
|
||||
|
||||
IMG = Configuration.load('img', 'secrets', Dir)
|
||||
Llm = Configuration.load('llm', 'secrets', Dir)
|
||||
News = Configuration.load('news', 'secrets', Dir)
|
||||
|
@ -32,6 +36,39 @@ Scrape = Configuration.load('scrape', 'secrets', Dir)
|
|||
Serve = Configuration.load('serve', '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
|
||||
ROUTER_DIR = BASE_DIR / "routers"
|
||||
DATA_DIR = BASE_DIR / "data"
|
||||
|
|
|
@ -68,6 +68,7 @@ class Logger:
|
|||
def get_module_logger(self, module_name):
|
||||
return self.logger.bind(name=module_name)
|
||||
|
||||
|
||||
L = Logger("classes", "classes")
|
||||
logger = L.get_module_logger("classes")
|
||||
def debug(text: str): logger.debug(text)
|
||||
|
@ -103,17 +104,20 @@ class Configuration(BaseModel):
|
|||
with secrets_path.open('r') as file:
|
||||
secrets_data = yaml.safe_load(file)
|
||||
debug(f"Loaded secrets data from {secrets_path}")
|
||||
|
||||
if isinstance(config_data, list):
|
||||
for item in config_data:
|
||||
if isinstance(item, dict):
|
||||
item.update(secrets_data)
|
||||
config_data = {"configurations": config_data, "SECRETS": secrets_data}
|
||||
elif isinstance(config_data, dict):
|
||||
config_data['SECRETS'] = secrets_data
|
||||
else:
|
||||
config_data.update(secrets_data)
|
||||
if isinstance(config_data, list):
|
||||
raise ValueError(f"Unexpected configuration data type: {type(config_data)}")
|
||||
|
||||
if not isinstance(config_data, dict):
|
||||
config_data = {"configurations": config_data}
|
||||
|
||||
if config_data.get('HOME') is None:
|
||||
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()
|
||||
instance = cls.create_dynamic_model(**config_data)
|
||||
|
@ -127,6 +131,7 @@ class Configuration(BaseModel):
|
|||
err(f"Error loading configuration: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
@ -137,6 +142,7 @@ class Configuration(BaseModel):
|
|||
path = base_path / path
|
||||
return path
|
||||
|
||||
|
||||
def resolve_placeholders(self, data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
resolved_data = {k: self.resolve_placeholders(v) for k, v in data.items()}
|
||||
|
@ -154,21 +160,13 @@ class Configuration(BaseModel):
|
|||
else:
|
||||
return data
|
||||
|
||||
|
||||
def resolve_string_placeholders(self, value: str) -> Any:
|
||||
pattern = r'\{\{\s*([^}]+)\s*\}\}'
|
||||
matches = re.findall(pattern, value)
|
||||
|
||||
for match in matches:
|
||||
parts = match.split('.')
|
||||
if isinstance(value, str) and value.startswith('{{') and value.endswith('}}'):
|
||||
key = value[2:-2].strip()
|
||||
parts = key.split('.')
|
||||
if len(parts) == 2 and parts[0] == 'SECRET':
|
||||
replacement = getattr(self, parts[1].strip(), '')
|
||||
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 getattr(self.SECRETS, parts[1], '')
|
||||
return value
|
||||
|
||||
|
||||
|
@ -188,6 +186,41 @@ class Configuration(BaseModel):
|
|||
)
|
||||
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:
|
||||
extra = "allow"
|
||||
arbitrary_types_allowed = True
|
||||
|
|
|
@ -63,7 +63,7 @@ async def list_11l_voices():
|
|||
formatted_list += f"{name}: `{id}`\n"
|
||||
|
||||
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)
|
||||
|
||||
|
@ -78,13 +78,13 @@ async def select_voice(voice_name: str) -> str:
|
|||
debug(f"Checking {item.name.lower()}")
|
||||
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}.")
|
||||
return str(item)
|
||||
return item
|
||||
|
||||
err(f"Voice file not found")
|
||||
raise HTTPException(status_code=404, detail="Voice file not found")
|
||||
|
||||
except Exception as e:
|
||||
err(f"Voice file not found: {str(e)}")
|
||||
err(f"Voice file not found: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
@ -119,7 +119,7 @@ async def generate_speech_endpoint(
|
|||
else:
|
||||
return await generate_speech(bg_tasks, text_content, voice, voice_file, model, speed, podcast)
|
||||
except Exception as e:
|
||||
err(f"Error in TTS: {str(e)}")
|
||||
err(f"Error in TTS: {e}")
|
||||
err(traceback.format_exc())
|
||||
raise HTTPException(status_code=666, detail="error in TTS")
|
||||
|
||||
|
@ -127,7 +127,7 @@ async def generate_speech_endpoint(
|
|||
async def generate_speech(
|
||||
bg_tasks: BackgroundTasks,
|
||||
text: str,
|
||||
voice: str = None,
|
||||
voice: Optional[str] = None,
|
||||
voice_file: UploadFile = None,
|
||||
model: str = None,
|
||||
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"
|
||||
|
||||
debug(f"Model: {model}")
|
||||
debug(f"API.EXTENSIONS.elevenlabs: {getattr(API.EXTENSIONS, 'elevenlabs', None)}")
|
||||
debug(f"API.EXTENSIONS.xtts: {getattr(API.EXTENSIONS, 'xtts', None)}")
|
||||
debug(f"Voice: {voice}")
|
||||
debug(f"Tts.elevenlabs: {Tts.elevenlabs}")
|
||||
|
||||
if model == "eleven_turbo_v2" and getattr(API.EXTENSIONS, 'elevenlabs', False):
|
||||
info("Using ElevenLabs.")
|
||||
|
@ -176,7 +176,7 @@ async def generate_speech(
|
|||
if podcast:
|
||||
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():
|
||||
info(f"Saved to podcast path: {podcast_path}")
|
||||
else:
|
||||
|
@ -184,18 +184,18 @@ async def generate_speech(
|
|||
|
||||
if podcast_path != 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:
|
||||
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:
|
||||
err(f"Failed to generate speech: {str(e)}")
|
||||
err(f"Failed to generate speech: {e}")
|
||||
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"Tts.elevenlabs.voices: {Tts.elevenlabs.voices}")
|
||||
|
||||
voices = Tts.elevenlabs.voices
|
||||
if voice_name in voices:
|
||||
return voices[voice_name]
|
||||
# Check if the voice is in the configured voices
|
||||
if voice_name and Tts.has_key(f'elevenlabs.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"
|
||||
headers = {"xi-api-key": Tts.elevenlabs.key}
|
||||
debug(f"Using key: {Tts.elevenlabs.key}")
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
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"Response content: {response.text}")
|
||||
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}")
|
||||
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):
|
||||
# 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)
|
||||
|
||||
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,
|
||||
"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}
|
||||
debug(f"Using ElevenLabs API key: {Tts.elevenlabs.key}")
|
||||
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)
|
||||
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")
|
||||
|
@ -266,13 +278,15 @@ async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = N
|
|||
audio_file.write(response.content)
|
||||
return file_path
|
||||
else:
|
||||
err(f"Error from ElevenLabs API. Status code: {response.status_code}")
|
||||
err(f"Response content: {response.text}")
|
||||
raise HTTPException(status_code=response.status_code, detail=f"Error from ElevenLabs API: {response.text}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Error from ElevenLabs API")
|
||||
|
||||
except Exception as 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()
|
||||
|
||||
if checksum == existing_checksum:
|
||||
return str(existing_file)
|
||||
return existing_file
|
||||
|
||||
base_name = existing_file.stem
|
||||
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:
|
||||
f.write(content)
|
||||
return str(new_file)
|
||||
return new_file
|
||||
|
||||
else:
|
||||
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,
|
||||
text=segment,
|
||||
speed=speed,
|
||||
file_path=str(segment_file_path),
|
||||
file_path=segment_file_path,
|
||||
speaker_wav=[voice_file_path],
|
||||
language="en"
|
||||
)
|
||||
debug(f"Segment file generated: {segment_file_path}")
|
||||
|
||||
# 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
|
||||
|
||||
# Delete the segment file
|
||||
|
@ -387,7 +401,7 @@ async def local_tts(
|
|||
|
||||
await asyncio.to_thread(combined_audio.export, file_path, format="wav")
|
||||
|
||||
return str(file_path)
|
||||
return file_path
|
||||
|
||||
else:
|
||||
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}")
|
||||
except Exception as e:
|
||||
print(f"An error occurred while copying the file: {file_path}")
|
||||
print(f"Error details: {str(e)}")
|
||||
print(f"Error details: {e}")
|
Loading…
Reference in a new issue