Auto-update: Sat Jun 29 16:58:00 PDT 2024

This commit is contained in:
sanj 2024-06-29 16:58:00 -07:00
parent 88612ab20a
commit 565a576c48
8 changed files with 252 additions and 142 deletions

View file

@ -93,13 +93,13 @@ DEFAULT_VOICE = os.getenv("DEFAULT_VOICE", "Luna")
DEFAULT_11L_VOICE = os.getenv("DEFAULT_11L_VOICE", "Victoria")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
### Summarization
SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens
SUMMARY_CHUNK_OVERLAP = int(os.getenv("SUMMARY_CHUNK_OVERLAP", 100)) # measured in tokens
SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 16384)) # measured in tokens
SUMMARY_CHUNK_OVERLAP = int(os.getenv("SUMMARY_CHUNK_OVERLAP", 256)) # measured in tokens
SUMMARY_TPW = float(os.getenv("SUMMARY_TPW", 1.3)) # measured in tokens
SUMMARY_LENGTH_RATIO = int(os.getenv("SUMMARY_LENGTH_RATIO", 4)) # measured as original to length ratio
SUMMARY_MIN_LENGTH = int(os.getenv("SUMMARY_MIN_LENGTH", 150)) # measured in tokens
SUMMARY_MODEL = os.getenv("SUMMARY_MODEL", "dolphin-llama3:8b-256k")
SUMMARY_TOKEN_LIMIT = int(os.getenv("SUMMARY_TOKEN_LIMIT", 4096))
SUMMARY_TOKEN_LIMIT = int(os.getenv("SUMMARY_TOKEN_LIMIT", 16384))
SUMMARY_INSTRUCT = os.getenv('SUMMARY_INSTRUCT', "You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.")
SUMMARY_INSTRUCT_TTS = os.getenv('SUMMARY_INSTRUCT_TTS', "You are an AI assistant that provides email summaries for Sanjay. Your response will undergo Text-To-Speech conversion and added to Sanjay's private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following email.")

View file

@ -338,8 +338,14 @@ class Geocoder:
processed_locations = []
for loc in locations:
if isinstance(loc, tuple):
processed_locations.append(Location(latitude=loc[0], longitude=loc[1]))
processed_locations.append(Location(
latitude=loc[0],
longitude=loc[1],
datetime=datetime.now(timezone.utc)
))
elif isinstance(loc, Location):
if loc.datetime is None:
loc.datetime = datetime.now(timezone.utc)
processed_locations.append(loc)
else:
raise ValueError(f"Unsupported location type: {type(loc)}")
@ -348,26 +354,39 @@ class Geocoder:
geocode_results = await asyncio.gather(*[self.location(lat, lon) for lat, lon in coordinates])
elevations = await asyncio.gather(*[self.elevation(lat, lon) for lat, lon in coordinates])
timezones = await asyncio.gather(*[self.timezone(lat, lon) for lat, lon in coordinates])
timezone_results = await asyncio.gather(*[self.timezone(lat, lon) for lat, lon in coordinates])
def create_display_name(override_name, result):
parts = []
if override_name:
parts.append(override_name)
if result.get('name') and result['name'] != override_name:
parts.append(result['name'])
if result.get('admin1'):
parts.append(result['admin1'])
if result.get('cc'):
parts.append(result['cc'])
return ', '.join(filter(None, parts))
geocoded_locations = []
for location, result, elevation, timezone in zip(processed_locations, geocode_results, elevations, timezones):
for location, result, elevation, tz_result in zip(processed_locations, geocode_results, elevations, timezone_results):
result = result[0] # Unpack the first result
override_name = result.get('override_name')
geocoded_location = Location(
latitude=location.latitude,
longitude=location.longitude,
elevation=elevation,
datetime=location.datetime or datetime.now(timezone.utc),
datetime=location.datetime,
zip=result.get("admin2"),
city=result.get("name"),
state=result.get("admin1"),
country=result.get("cc"),
context=location.context or {},
name=override_name or result.get("name"),
display_name=f"{override_name or result.get('name')}, {result.get('admin1')}, {result.get('cc')}",
display_name=create_display_name(override_name, result),
country_code=result.get("cc"),
timezone=timezone
timezone=tz_result
)
# Merge original location data with geocoded data

View file

@ -326,13 +326,13 @@ SYSTEM_MSG=You are a helpful AI assistant.
DEFAULT_LLM=dolphin-mistral
DEFAULT_VISION=llava-llama3
OPENAI_API_KEY=¿SECRET? # <--- not presently implemented for anything
SUMMARY_MODEL=dolphin-mistral
SUMMARY_CHUNK_SIZE=4000
SUMMARY_MODEL='dolphin-llama3:8b-256k'
SUMMARY_CHUNK_SIZE=16384
SUMMARY_CHUNK_OVERLAP=100
SUMMARY_TPW=1.3
SUMMARY_LENGTH_RATIO=4
SUMMARY_MIN_LENGTH=150
SUMMARY_TOKEN_LIMIT=4096
SUMMARY_MIN_LENGTH=64
SUMMARY_TOKEN_LIMIT=16384
SUMMARY_INSTRUCT='You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.'
SUMMARY_INSTRUCT_TTS='You are an AI assistant that summarizes emails -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary. Your response will undergo Text-To-Speech conversion and added to Sanjays private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following.'
DEFAULT_VOICE=joanne

View file

@ -201,7 +201,7 @@ async def summarize_single_email(this_email: IncomingEmail, podcast: bool = Fals
md_summary += f'title: {this_email.subject}\n'
md_summary += f'{summary}\n'
md_summary += f'```\n\n'
md_summary += f'![[{tts_path}]]\n' if tts_path.exists() else ''
md_summary += f'![[{tts_relative}]]\n'# if tts_path.exists() else ''
return md_summary

View file

@ -9,6 +9,7 @@ from typing import List, Dict, Any, Union, Optional
from pydantic import BaseModel, root_validator, ValidationError
import aiofiles
import os
import re
import glob
import chromadb
from openai import OpenAI
@ -26,7 +27,7 @@ import html2text
import markdown
from sijapi import L, LLM_SYS_MSG, DEFAULT_LLM, DEFAULT_VISION, REQUESTS_DIR, OBSIDIAN_CHROMADB_COLLECTION, OBSIDIAN_VAULT_DIR, DOC_DIR, OPENAI_API_KEY, DEFAULT_VOICE, SUMMARY_INSTRUCT, SUMMARY_CHUNK_SIZE, SUMMARY_TPW, SUMMARY_CHUNK_OVERLAP, SUMMARY_LENGTH_RATIO, SUMMARY_TOKEN_LIMIT, SUMMARY_MIN_LENGTH, SUMMARY_MODEL
from sijapi.utilities import convert_to_unix_time, sanitize_filename, ocr_pdf, clean_text, should_use_ocr, extract_text_from_pdf, extract_text_from_docx, read_text_file, str_to_bool, get_extension
from sijapi.routers.tts import generate_speech
from sijapi.routers import tts
from sijapi.routers.asr import transcribe_audio
@ -520,17 +521,56 @@ async def summarize_post(file: Optional[UploadFile] = File(None), text: Optional
summarized_text = await summarize_text(text_content, instruction)
return summarized_text
@llm.post("/speaksummary")
async def summarize_tts_endpoint(bg_tasks: BackgroundTasks, instruction: str = Form(SUMMARY_INSTRUCT), file: Optional[UploadFile] = File(None), text: Optional[str] = Form(None), voice: Optional[str] = Form(DEFAULT_VOICE), speed: Optional[float] = Form(1.2), podcast: Union[bool, str] = Form(False)):
podcast = str_to_bool(str(podcast)) # Proper boolean conversion
text_content = text if text else extract_text(file)
final_output_path = await summarize_tts(text_content, instruction, voice, speed, podcast)
return FileResponse(path=final_output_path, filename=os.path.basename(final_output_path), media_type='audio/wav')
async def summarize_tts_endpoint(
bg_tasks: BackgroundTasks,
instruction: str = Form(SUMMARY_INSTRUCT),
file: Optional[UploadFile] = File(None),
text: Optional[str] = Form(None),
voice: Optional[str] = Form(DEFAULT_VOICE),
speed: Optional[float] = Form(1.2),
podcast: Union[bool, str] = Form(False)
):
try:
podcast = str_to_bool(str(podcast))
if text:
text_content = text
elif file:
# Handle the UploadFile here
content = await file.read()
file_extension = os.path.splitext(file.filename)[1]
temp_file_path = tempfile.mktemp(suffix=file_extension)
with open(temp_file_path, 'wb') as temp_file:
temp_file.write(content)
bg_tasks.add_task(os.remove, temp_file_path)
# Now pass the file path to extract_text
text_content = await extract_text(temp_file_path)
else:
raise ValueError("Either text or file must be provided")
final_output_path = await summarize_tts(text_content, instruction, voice, speed, podcast)
return FileResponse(
path=final_output_path,
filename=os.path.basename(final_output_path),
media_type='audio/wav',
background=bg_tasks
)
except Exception as e:
L.ERR(f"Error in summarize_tts_endpoint: {str(e)}")
return JSONResponse(
status_code=400,
content={"error": str(e)}
)
async def summarize_tts(
text: str,
text: str,
instruction: str = SUMMARY_INSTRUCT,
voice: Optional[str] = DEFAULT_VOICE,
speed: float = 1.1,
@ -539,14 +579,15 @@ async def summarize_tts(
):
LLM = LLM if LLM else Ollama()
summarized_text = await summarize_text(text, instruction, LLM=LLM)
filename = await summarize_text(summarized_text, "Provide a title for this summary no longer than 4 words")
filename = await summarize_text(summarized_text, "Provide a title for this summary no longer than 4 words", length_override=10)
filename = sanitize_filename(filename)
filename = ' '.join(filename.split()[:5])
timestamp = dt_datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{timestamp}{filename}.wav"
bg_tasks = BackgroundTasks()
final_output_path = await generate_speech(bg_tasks, summarized_text, voice, "xtts", speed=speed, podcast=podcast, title=filename)
model = await tts.get_model(voice)
final_output_path = await tts.generate_speech(bg_tasks, summarized_text, voice, model=model, speed=speed, podcast=podcast, title=filename)
L.DEBUG(f"summary_tts completed with final_output_path: {final_output_path}")
return final_output_path
@ -557,18 +598,36 @@ async def get_title(text: str, LLM: Ollama() = None):
title = sanitize_filename(title)
return title
def split_text_into_chunks(text: str) -> List[str]:
"""
Splits the given text into manageable chunks based on predefined size and overlap.
"""
sentences = re.split(r'(?<=[.!?])\s+', text)
words = text.split()
adjusted_chunk_size = max(1, int(SUMMARY_CHUNK_SIZE / SUMMARY_TPW)) # Ensure at least 1
adjusted_overlap = max(0, int(SUMMARY_CHUNK_OVERLAP / SUMMARY_TPW)) # Ensure non-negative
total_words = len(words)
L.DEBUG(f"Total words: {total_words}. SUMMARY_CHUNK_SIZE: {SUMMARY_CHUNK_SIZE}. SUMMARY_TPW: {SUMMARY_TPW}.")
max_words_per_chunk = int(SUMMARY_CHUNK_SIZE / SUMMARY_TPW)
L.DEBUG(f"Maximum words per chunk: {max_words_per_chunk}")
chunks = []
for i in range(0, len(words), adjusted_chunk_size - adjusted_overlap):
L.DEBUG(f"We are on iteration # {i} if split_text_into_chunks.")
chunk = ' '.join(words[i:i + adjusted_chunk_size])
chunks.append(chunk)
current_chunk = []
current_word_count = 0
for sentence in sentences:
sentence_words = sentence.split()
if current_word_count + len(sentence_words) <= max_words_per_chunk:
current_chunk.append(sentence)
current_word_count += len(sentence_words)
else:
if current_chunk:
chunks.append(' '.join(current_chunk))
current_chunk = [sentence]
current_word_count = len(sentence_words)
if current_chunk:
chunks.append(' '.join(current_chunk))
L.DEBUG(f"Split text into {len(chunks)} chunks.")
return chunks
@ -577,92 +636,114 @@ def calculate_max_tokens(text: str) -> int:
return min(tokens_count // 4, SUMMARY_CHUNK_SIZE)
async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], bg_tasks: BackgroundTasks = None) -> str:
if isinstance(file, UploadFile):
file_extension = get_extension(file)
temp_file_path = tempfile.mktemp(suffix=file_extension)
with open(temp_file_path, 'wb') as buffer:
shutil.copyfileobj(file.file, buffer)
file_path = temp_file_path
elif isinstance(file, (bytes, bytearray)):
temp_file_path = tempfile.mktemp()
with open(temp_file_path, 'wb') as buffer:
buffer.write(file)
file_path = temp_file_path
elif isinstance(file, (str, Path)):
file_path = str(file)
else:
raise ValueError("Unsupported file type")
L.INFO(f"Attempting to extract text from file: {file}")
_, file_ext = os.path.splitext(file_path)
file_ext = file_ext.lower()
text_content = ""
try:
if isinstance(file, UploadFile):
L.INFO("File is an UploadFile object")
file_extension = os.path.splitext(file.filename)[1]
temp_file_path = tempfile.mktemp(suffix=file_extension)
with open(temp_file_path, 'wb') as buffer:
content = await file.read()
buffer.write(content)
file_path = temp_file_path
elif isinstance(file, (bytes, bytearray)):
temp_file_path = tempfile.mktemp()
with open(temp_file_path, 'wb') as buffer:
buffer.write(file)
file_path = temp_file_path
elif isinstance(file, (str, Path)):
file_path = str(file)
else:
raise ValueError(f"Unsupported file type: {type(file)}")
if file_ext == '.pdf':
text_content = await extract_text_from_pdf(file_path)
elif file_ext in ['.wav', '.m4a', '.m4v', '.mp3', '.mp4']:
text_content = await transcribe_audio(file_path=file_path)
elif file_ext == '.md':
text_content = await read_text_file(file_path)
text_content = markdown.markdown(text_content)
elif file_ext == '.html':
text_content = await read_text_file(file_path)
text_content = html2text.html2text(text_content)
elif file_ext in ['.txt', '.csv', '.json']:
text_content = await read_text_file(file_path)
elif file_ext == '.docx':
text_content = await extract_text_from_docx(file_path)
_, file_ext = os.path.splitext(file_path)
file_ext = file_ext.lower()
L.INFO(f"File extension: {file_ext}")
if bg_tasks and 'temp_file_path' in locals():
bg_tasks.add_task(os.remove, temp_file_path)
elif 'temp_file_path' in locals():
os.remove(temp_file_path)
if file_ext == '.pdf':
text_content = await extract_text_from_pdf(file_path)
elif file_ext in ['.wav', '.m4a', '.m4v', '.mp3', '.mp4']:
text_content = await transcribe_audio(file_path=file_path)
elif file_ext == '.md':
text_content = await read_text_file(file_path)
text_content = markdown.markdown(text_content)
elif file_ext == '.html':
text_content = await read_text_file(file_path)
text_content = html2text.html2text(text_content)
elif file_ext in ['.txt', '.csv', '.json']:
text_content = await read_text_file(file_path)
elif file_ext == '.docx':
text_content = await extract_text_from_docx(file_path)
else:
raise ValueError(f"Unsupported file extension: {file_ext}")
if bg_tasks and 'temp_file_path' in locals():
bg_tasks.add_task(os.remove, temp_file_path)
elif 'temp_file_path' in locals():
os.remove(temp_file_path)
return text_content
except Exception as e:
L.ERR(f"Error extracting text: {str(e)}")
raise ValueError(f"Error extracting text: {str(e)}")
return text_content
async def summarize_text(text: str, instruction: str = SUMMARY_INSTRUCT, length_override: int = None, length_quotient: float = SUMMARY_LENGTH_RATIO, LLM: Ollama = None):
"""
Process the given text: split into chunks, summarize each chunk, and
potentially summarize the concatenated summary for long texts.
"""
LLM = LLM if LLM else Ollama()
chunked_text = split_text_into_chunks(text)
total_parts = max(1, len(chunked_text)) # Ensure at least 1
total_parts = len(chunked_text)
L.DEBUG(f"Total parts: {total_parts}. Length of chunked text: {len(chunked_text)}")
total_words_count = sum(len(chunk.split()) for chunk in chunked_text)
L.DEBUG(f"Total words count: {total_words_count}")
total_tokens_count = max(1, int(total_words_count * SUMMARY_TPW))
L.DEBUG(f"Total tokens count: {total_tokens_count}")
total_words_count = len(text.split())
total_tokens_count = max(1, int(total_words_count * SUMMARY_TPW)) # Ensure at least 1
total_summary_length = length_override if length_override else total_tokens_count // length_quotient
L.DEBUG(f"Total summary length: {total_summary_length}")
corrected_total_summary_length = min(total_summary_length, SUMMARY_TOKEN_LIMIT)
individual_summary_length = max(1, corrected_total_summary_length // total_parts) # Ensure at least 1
L.DEBUG(f"Corrected total summary length: {corrected_total_summary_length}")
L.DEBUG(f"Text split into {total_parts} chunks.")
summaries = await asyncio.gather(*[
process_chunk(instruction, chunk, i+1, total_parts, individual_summary_length, LLM) for i, chunk in enumerate(chunked_text)
process_chunk(instruction, chunk, i+1, total_parts, LLM=LLM)
for i, chunk in enumerate(chunked_text)
])
concatenated_summary = ' '.join(summaries)
if total_parts > 1:
concatenated_summary = await process_chunk(instruction, concatenated_summary, 1, 1)
return concatenated_summary
summaries = [f"\n\n\nPART {i+1} of {total_parts}:\n\n{summary}" for i, summary in enumerate(summaries)]
async def process_chunk(instruction: str, text: str, part: int, total_parts: int, max_tokens: Optional[int] = None, LLM: Ollama = None) -> str:
"""
Process a portion of text using the ollama library asynchronously.
"""
concatenated_summary = ' '.join(summaries)
L.DEBUG(f"Concatenated summary: {concatenated_summary}")
L.DEBUG(f"Concatenated summary length: {len(concatenated_summary.split())}")
if total_parts > 1:
L.DEBUG(f"Processing the concatenated_summary to smooth the edges...")
concatenated_instruct = f"The following text consists of the concatenated {total_parts} summaries of {total_parts} parts of a single document that had to be split for processing. Reword it for clarity and flow as a single cohesive summary, understanding that it all relates to a single document, but that document likely consists of multiple parts potentially from multiple authors. Do not shorten it and do not omit content, simply smooth out the edges between the parts."
final_summary = await process_chunk(concatenated_instruct, concatenated_summary, 1, 1, length_ratio=1, LLM=LLM)
L.DEBUG(f"Final summary length: {len(final_summary.split())}")
return final_summary
else:
return concatenated_summary
async def process_chunk(instruction: str, text: str, part: int, total_parts: int, length_ratio: float = None, LLM: Ollama = None) -> str:
# L.DEBUG(f"Processing chunk: {text}")
LLM = LLM if LLM else Ollama()
words_count = max(1, len(text.split())) # Ensure at least 1
tokens_count = max(1, int(words_count * SUMMARY_TPW)) # Ensure at least 1
fraction_tokens = max(1, tokens_count // SUMMARY_LENGTH_RATIO) # Ensure at least 1
if max_tokens is None:
max_tokens = min(fraction_tokens, SUMMARY_CHUNK_SIZE // max(1, total_parts)) # Ensure at least 1
max_tokens = max(max_tokens, SUMMARY_MIN_LENGTH) # Ensure a minimum token count to avoid tiny processing chunks
words_count = len(text.split())
tokens_count = max(1, int(words_count * SUMMARY_TPW))
summary_length_ratio = length_ratio if length_ratio else SUMMARY_LENGTH_RATIO
max_tokens = min(tokens_count // summary_length_ratio, SUMMARY_CHUNK_SIZE)
max_tokens = max(max_tokens, SUMMARY_MIN_LENGTH)
L.DEBUG(f"Summarizing part {part} of {total_parts}: Max_tokens: {max_tokens}")
L.DEBUG(f"Processing part {part} of {total_parts}: Words: {words_count}, Estimated tokens: {tokens_count}, Max output tokens: {max_tokens}")
if part and total_parts > 1:
prompt = f"{instruction}. Part {part} of {total_parts}:\n{text}"
@ -674,12 +755,12 @@ async def process_chunk(instruction: str, text: str, part: int, total_parts: int
model=SUMMARY_MODEL,
prompt=prompt,
stream=False,
options={'num_predict': max_tokens, 'temperature': 0.6}
options={'num_predict': max_tokens, 'temperature': 0.5}
)
text_response = response['response']
L.DEBUG(f"Completed LLM.generate for part {part} of {total_parts}")
L.DEBUG(f"Result: {text_response}")
return text_response
async def title_and_summary(extracted_text: str):

View file

@ -273,7 +273,10 @@ async def generate_map(start_date: datetime, end_date: datetime):
return html_content
async def post_location(location: Location):
L.DEBUG(f"post_location called with {location.datetime}")
if not location.datetime:
L.DEBUG(f"location appears to be missing datetime: {location}")
else:
L.DEBUG(f"post_location called with {location.datetime}")
async with DB.get_connection() as conn:
try:
@ -343,28 +346,35 @@ async def post_locate_endpoint(locations: Union[Location, List[Location]]):
locations = [locations]
# Prepare locations
for location in locations:
if not location.datetime:
tz = GEO.tz_current(location.latitude, location.longitude)
location.datetime = datetime.now(tz).isoformat()
for lcn in locations:
if not lcn.datetime:
tz = await GEO.tz_at(lcn.latitude, lcn.longitude)
lcn.datetime = datetime.now(ZoneInfo(tz)).isoformat()
if not location.context:
location.context = {
"action": "manual",
"device_type": "Pythonista",
if not lcn.context:
lcn.context = {
"action": "missing",
"device_type": "API",
"device_model": "Unknown",
"device_name": "Unknown",
"device_os": "Unknown"
}
L.DEBUG(f"Location received for processing: {location}")
L.DEBUG(f"Location received for processing: {lcn}")
geocoded_locations = await GEO.code(locations)
responses = []
for location in geocoded_locations:
L.DEBUG(f"Final location submitted to database: {location}")
location_entry = await post_location(location)
if isinstance(geocoded_locations, List):
for location in geocoded_locations:
L.DEBUG(f"Final location to be submitted to database: {location}")
location_entry = await post_location(location)
if location_entry:
responses.append({"location_data": location_entry})
else:
L.WARN(f"Posting location to database appears to have failed.")
else:
L.DEBUG(f"Final location to be submitted to database: {geocoded_locations}")
location_entry = await post_location(geocoded_locations)
if location_entry:
responses.append({"location_data": location_entry})
else:
@ -373,7 +383,6 @@ async def post_locate_endpoint(locations: Union[Location, List[Location]]):
return {"message": "Locations and weather updated", "results": responses}
@loc.get("/locate", response_model=Location)
async def get_last_location_endpoint() -> JSONResponse:
this_location = await get_last_location()

View file

@ -135,7 +135,7 @@ Obsidian helper. Takes a datetime and creates a new daily note. Note: it uses th
places = await loc.fetch_locations(date_time)
lat, lon = places[0].latitude, places[0].longitude
location = await GEO.code(lat, lon)
location = await GEO.code((lat, lon))
timeslips = await build_daily_timeslips(date_time)
@ -189,7 +189,7 @@ created: "{dt_datetime.now().strftime("%Y-%m-%d %H:%M:%S")}"
return absolute_path
### Daily Note Component Builders ###
async def build_daily_timeslips(date):
'''
@ -325,7 +325,7 @@ async def generate_banner(dt, location: Location = None, forecast: str = None, m
display_name += f"{location.country} " if location.country else ""
if display_name == "Location: ":
geocoded_location = await GEO.code(lat, lon)
geocoded_location = await GEO.code((lat, lon))
if geocoded_location.display_name or geocoded_location.city or geocoded_location.country:
return await generate_banner(dt, geocoded_location, forecast, mood, other_context)
else:
@ -405,7 +405,7 @@ async def update_dn_weather(date_time: dt_datetime, lat: float = None, lon: floa
L.WARN(f"Using {date_time.strftime('%Y-%m-%d %H:%M:%S')} as our datetime in update_dn_weather.")
try:
if lat and lon:
place = GEO.code(lat, lon)
place = GEO.code((lat, lon))
else:
L.DEBUG(f"Updating weather for {date_time}")
@ -425,7 +425,7 @@ async def update_dn_weather(date_time: dt_datetime, lat: float = None, lon: floa
L.INFO(f"City in data: {city}")
else:
location = await GEO.code(lat, lon)
location = await GEO.code((lat, lon))
L.DEBUG(f"location: {location}")
city = location.name
city = city if city else location.city

View file

@ -78,11 +78,10 @@ def select_voice(voice_name: str) -> str:
raise HTTPException(status_code=404, detail="Voice file not found")
except Exception as e:
L.ERR(f"Voice file not found: {str(e)}")
L.ERR(traceback.format_exc())
raise HTTPException(status_code=404, detail="Voice file not found")
return None
@tts.post("/tts")
@tts.post("/tts/speak")
@tts.post("/v1/audio/speech")
async def generate_speech_endpoint(
@ -116,7 +115,6 @@ async def generate_speech_endpoint(
L.ERR(traceback.format_exc())
raise HTTPException(status_code=666, detail="error in TTS")
async def generate_speech(
bg_tasks: BackgroundTasks,
text: str,
@ -136,33 +134,36 @@ async def generate_speech(
model = model if model else await get_model(voice, voice_file)
if model == "eleven_turbo_v2":
L.INFO(f"Using ElevenLabs.")
L.INFO("Using ElevenLabs.")
audio_file_path = await elevenlabs_tts(model, text, voice, title, output_dir)
return str(audio_file_path)
elif model == "xtts":
L.INFO(f"Using XTTS2")
final_output_dir = await local_tts(text, speed, voice, voice_file, podcast, bg_tasks, title, output_dir)
bg_tasks.add_task(os.remove, str(final_output_dir))
return str(final_output_dir)
else:
raise HTTPException(status_code=400, detail="Invalid model specified")
except HTTPException as e:
L.ERR(f"HTTP error: {e}")
L.ERR(traceback.format_exc())
raise e
else: # if model == "xtts":
L.INFO("Using XTTS2")
audio_file_path = await local_tts(text, speed, voice, voice_file, podcast, bg_tasks, title, output_dir)
#else:
# raise HTTPException(status_code=400, detail="Invalid model specified")
if podcast == True:
podcast_path = PODCAST_DIR / audio_file_path.name
L.DEBUG(f"Podcast path: {podcast_path}")
shutil.copy(str(audio_file_path), str(podcast_path))
bg_tasks.add_task(os.remove, str(audio_file_path))
return str(podcast_path)
return str(audio_file_path)
except Exception as e:
L.ERR(f"Error: {e}")
L.ERR(traceback.format_exc())
raise e
L.ERROR(f"Failed to generate speech: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to generate speech: {str(e)}")
async def get_model(voice: str = None, voice_file: UploadFile = None):
if voice_file or (voice and select_voice(voice)):
return "xtts"
elif voice and await determine_voice_id(voice):
return "eleven_turbo_v2"
else:
raise HTTPException(status_code=400, detail="No model or voice specified")
@ -216,7 +217,7 @@ async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = N
"model_id": model
}
headers = {"Content-Type": "application/json", "xi-api-key": ELEVENLABS_API_KEY}
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: # 5 minutes timeout
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 datetime.now().strftime("%Y%m%d%H%M%S")