Auto-update: Thu Jun 27 19:27:06 PDT 2024

This commit is contained in:
sanj 2024-06-27 19:27:06 -07:00
parent 12b4b53705
commit 6acb9e4d8e
6 changed files with 79 additions and 73 deletions

View file

@ -41,7 +41,7 @@ transcription_results = {}
@asr.post("/transcribe") @asr.post("/transcribe")
@asr.post("/v1/audio/transcription") @asr.post("/v1/audio/transcription")
async def transcribe_endpoint( async def transcribe_endpoint(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
file: UploadFile = File(...), file: UploadFile = File(...),
params: str = Form(...) params: str = Form(...)
): ):
@ -58,7 +58,7 @@ async def transcribe_endpoint(
temp_file.write(await file.read()) temp_file.write(await file.read())
temp_file_path = temp_file.name temp_file_path = temp_file.name
transcription_job = await transcribe_audio(file_path=temp_file_path, params=parameters, background_tasks=background_tasks) transcription_job = await transcribe_audio(file_path=temp_file_path, params=parameters, bg_tasks=bg_tasks)
job_id = transcription_job["job_id"] job_id = transcription_job["job_id"]
# Poll for completion # Poll for completion
@ -80,12 +80,13 @@ async def transcribe_endpoint(
# If we've reached this point, the transcription has taken too long # If we've reached this point, the transcription has taken too long
return JSONResponse(content={"status": "timeout", "message": "Transcription is taking longer than expected. Please check back later."}, status_code=202) return JSONResponse(content={"status": "timeout", "message": "Transcription is taking longer than expected. Please check back later."}, status_code=202)
async def transcribe_audio(file_path, params: TranscribeParams, background_tasks: BackgroundTasks): async def transcribe_audio(file_path, params: TranscribeParams, bg_tasks: BackgroundTasks):
L.DEBUG(f"Transcribing audio file from {file_path}...")
file_path = await convert_to_wav(file_path) file_path = await convert_to_wav(file_path)
model = params.model if params.model in WHISPER_CPP_MODELS else 'small' model = params.model if params.model in WHISPER_CPP_MODELS else 'small'
model_path = WHISPER_CPP_DIR / 'models' / f'ggml-{model}.bin' model_path = WHISPER_CPP_DIR / 'models' / f'ggml-{model}.bin'
command = [str(WHISPER_CPP_DIR / 'build' / 'bin' / 'main')] command = [str(WHISPER_CPP_DIR / 'build' / 'bin' / 'main')]
command.extend(['-m', str(model_path)]) command.extend(['-m', str(model_path)])
command.extend(['-t', str(max(1, min(params.threads or MAX_CPU_CORES, MAX_CPU_CORES)))]) command.extend(['-t', str(max(1, min(params.threads or MAX_CPU_CORES, MAX_CPU_CORES)))])
command.extend(['-np']) # Always enable no-prints command.extend(['-np']) # Always enable no-prints
@ -117,7 +118,6 @@ async def transcribe_audio(file_path, params: TranscribeParams, background_tasks
command.extend(['--dtw', params.dtw]) command.extend(['--dtw', params.dtw])
command.extend(['-f', file_path]) command.extend(['-f', file_path])
L.DEBUG(f"Command: {command}") L.DEBUG(f"Command: {command}")
# Create a unique ID for this transcription job # Create a unique ID for this transcription job
@ -127,9 +127,21 @@ async def transcribe_audio(file_path, params: TranscribeParams, background_tasks
transcription_results[job_id] = {"status": "processing", "result": None} transcription_results[job_id] = {"status": "processing", "result": None}
# Run the transcription in a background task # Run the transcription in a background task
background_tasks.add_task(process_transcription, command, file_path, job_id) bg_tasks.add_task(process_transcription, command, file_path, job_id)
return {"job_id": job_id} max_wait_time = 300 # 5 minutes
poll_interval = 1 # 1 second
start_time = asyncio.get_event_loop().time()
while asyncio.get_event_loop().time() - start_time < max_wait_time:
job_status = transcription_results.get(job_id, {})
if job_status["status"] == "completed":
return job_status["result"]
elif job_status["status"] == "failed":
raise Exception(f"Transcription failed: {job_status.get('error', 'Unknown error')}")
await asyncio.sleep(poll_interval)
raise TimeoutError("Transcription timed out")
async def process_transcription(command, file_path, job_id): async def process_transcription(command, file_path, job_id):
try: try:

View file

@ -522,7 +522,7 @@ async def summarize_post(file: Optional[UploadFile] = File(None), text: Optional
return summarized_text return summarized_text
@llm.post("/speaksummary") @llm.post("/speaksummary")
async def summarize_tts_endpoint(background_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)): 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 podcast = str_to_bool(str(podcast)) # Proper boolean conversion
text_content = text if text else extract_text(file) text_content = text if text else extract_text(file)
@ -546,8 +546,8 @@ async def summarize_tts(
timestamp = dt_datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = dt_datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{timestamp}{filename}.wav" filename = f"{timestamp}{filename}.wav"
background_tasks = BackgroundTasks() bg_tasks = BackgroundTasks()
final_output_path = await generate_speech(background_tasks, summarized_text, voice, "xtts", speed=speed, podcast=podcast, title=filename) final_output_path = await generate_speech(bg_tasks, summarized_text, voice, "xtts", speed=speed, podcast=podcast, title=filename)
L.DEBUG(f"summary_tts completed with final_output_path: {final_output_path}") L.DEBUG(f"summary_tts completed with final_output_path: {final_output_path}")
return final_output_path return final_output_path
@ -578,7 +578,7 @@ def calculate_max_tokens(text: str) -> int:
return min(tokens_count // 4, SUMMARY_CHUNK_SIZE) return min(tokens_count // 4, SUMMARY_CHUNK_SIZE)
async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], background_tasks: BackgroundTasks = None) -> str: async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], bg_tasks: BackgroundTasks = None) -> str:
if isinstance(file, UploadFile): if isinstance(file, UploadFile):
file_extension = get_extension(file) file_extension = get_extension(file)
temp_file_path = tempfile.mktemp(suffix=file_extension) temp_file_path = tempfile.mktemp(suffix=file_extension)
@ -614,8 +614,8 @@ async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], bac
elif file_ext == '.docx': elif file_ext == '.docx':
text_content = await extract_text_from_docx(file_path) text_content = await extract_text_from_docx(file_path)
if background_tasks and 'temp_file_path' in locals(): if bg_tasks and 'temp_file_path' in locals():
background_tasks.add_task(os.remove, temp_file_path) bg_tasks.add_task(os.remove, temp_file_path)
elif 'temp_file_path' in locals(): elif 'temp_file_path' in locals():
os.remove(temp_file_path) os.remove(temp_file_path)

View file

@ -14,6 +14,7 @@ import asyncio
import pytz import pytz
import aiohttp import aiohttp
import folium import folium
from zoneinfo import ZoneInfo
import time as timer import time as timer
from dateutil.parser import parse as dateutil_parse from dateutil.parser import parse as dateutil_parse
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -158,38 +159,35 @@ async def geocode(zip_code: Optional[str] = None, latitude: Optional[float] = No
async def localize_datetime(dt: Union[str, datetime], fetch_loc: bool = False) -> datetime: async def localize_datetime(dt: Union[str, datetime], fetch_loc: bool = False) -> datetime:
"""
Localize a datetime object or string to the appropriate timezone.
Args:
dt (Union[str, datetime]): The datetime to localize.
fetch_loc (bool): Whether to fetch the current location for timezone.
Returns:
datetime: A timezone-aware datetime object.
Raises:
ValueError: If the input cannot be parsed as a datetime.
"""
try: try:
# Convert string to datetime if necessary # Convert string to datetime if necessary
if isinstance(dt, str): if isinstance(dt, str):
dt = dateutil_parse(dt) dt = dateutil_parse(dt)
L.DEBUG(f"Converted string '{dt}' to datetime object.") L.DEBUG(f"Converted string '{dt}' to datetime object.")
if not isinstance(dt, datetime): if not isinstance(dt, datetime):
raise ValueError("Input must be a string or datetime object.") raise ValueError("Input must be a string or datetime object.")
# Fetch timezone # Fetch timezone string
if fetch_loc: if fetch_loc:
loc = await get_last_location() loc = await get_last_location()
tz = await DynamicTZ.get_current(loc) tz_str = DynamicTZ.find(loc[0], loc[1]) # Assuming loc is (lat, lon)
L.DEBUG(f"Fetched current timezone: {tz}")
else: else:
tz = await DynamicTZ.get_last() tz_str = DynamicTZ.last_timezone
L.DEBUG(f"Using last known timezone: {tz}")
L.DEBUG(f"Retrieved timezone string: {tz_str}")
# Convert timezone string to ZoneInfo object
try:
tz = ZoneInfo(tz_str)
except Exception as e:
L.WARN(f"Invalid timezone string '{tz_str}'. Falling back to UTC. Error: {e}")
tz = ZoneInfo('UTC')
L.DEBUG(f"Using timezone: {tz}")
# Localize datetime # Localize datetime
if dt.tzinfo is None: if dt.tzinfo is None:
dt = dt.replace(tzinfo=tz) dt = dt.replace(tzinfo=tz)
@ -197,9 +195,8 @@ async def localize_datetime(dt: Union[str, datetime], fetch_loc: bool = False) -
elif dt.tzinfo != tz: elif dt.tzinfo != tz:
dt = dt.astimezone(tz) dt = dt.astimezone(tz)
L.DEBUG(f"Converted datetime from {dt.tzinfo} to {tz}") L.DEBUG(f"Converted datetime from {dt.tzinfo} to {tz}")
return dt return dt
except ValueError as e: except ValueError as e:
L.ERR(f"Error parsing datetime: {e}") L.ERR(f"Error parsing datetime: {e}")
raise raise
@ -208,7 +205,6 @@ async def localize_datetime(dt: Union[str, datetime], fetch_loc: bool = False) -
raise ValueError(f"Failed to localize datetime: {e}") raise ValueError(f"Failed to localize datetime: {e}")
async def find_override_locations(lat: float, lon: float) -> Optional[str]: async def find_override_locations(lat: float, lon: float) -> Optional[str]:
# Load the JSON file # Load the JSON file
with open(NAMED_LOCATIONS, 'r') as file: with open(NAMED_LOCATIONS, 'r') as file:

View file

@ -133,7 +133,7 @@ async def build_daily_timeslips(date):
### CLIPPER ### ### CLIPPER ###
@note.post("/clip") @note.post("/clip")
async def clip_post( async def clip_post(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
file: UploadFile = None, file: UploadFile = None,
url: Optional[str] = Form(None), url: Optional[str] = Form(None),
source: Optional[str] = Form(None), source: Optional[str] = Form(None),
@ -142,65 +142,64 @@ async def clip_post(
voice: str = Form(DEFAULT_VOICE), voice: str = Form(DEFAULT_VOICE),
encoding: str = Form('utf-8') encoding: str = Form('utf-8')
): ):
markdown_filename = await process_article(background_tasks, url, title, encoding, source, tts, voice) markdown_filename = await process_article(bg_tasks, url, title, encoding, source, tts, voice)
return {"message": "Clip saved successfully", "markdown_filename": markdown_filename} return {"message": "Clip saved successfully", "markdown_filename": markdown_filename}
@note.post("/archive") @note.post("/archive")
async def archive_post( async def archive_post(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
file: UploadFile = None, file: UploadFile = None,
url: Optional[str] = Form(None), url: Optional[str] = Form(None),
source: Optional[str] = Form(None), source: Optional[str] = Form(None),
title: Optional[str] = Form(None), title: Optional[str] = Form(None),
encoding: str = Form('utf-8') encoding: str = Form('utf-8')
): ):
markdown_filename = await process_archive(background_tasks, url, title, encoding, source) markdown_filename = await process_archive(bg_tasks, url, title, encoding, source)
return {"message": "Clip saved successfully", "markdown_filename": markdown_filename} return {"message": "Clip saved successfully", "markdown_filename": markdown_filename}
@note.get("/clip") @note.get("/clip")
async def clip_get( async def clip_get(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
url: str, url: str,
title: Optional[str] = Query(None), title: Optional[str] = Query(None),
encoding: str = Query('utf-8'), encoding: str = Query('utf-8'),
tts: str = Query('summary'), tts: str = Query('summary'),
voice: str = Query(DEFAULT_VOICE) voice: str = Query(DEFAULT_VOICE)
): ):
markdown_filename = await process_article(background_tasks, url, title, encoding, tts=tts, voice=voice) markdown_filename = await process_article(bg_tasks, url, title, encoding, tts=tts, voice=voice)
return {"message": "Clip saved successfully", "markdown_filename": markdown_filename} return {"message": "Clip saved successfully", "markdown_filename": markdown_filename}
@note.post("/note/add") @note.post("/note/add")
async def note_add_endpoint(file: Optional[UploadFile] = File(None), text: Optional[str] = Form(None), source: Optional[str] = Form(None)): async def note_add_endpoint(file: Optional[UploadFile] = File(None), text: Optional[str] = Form(None), source: Optional[str] = Form(None), bg_tasks: BackgroundTasks = None):
L.DEBUG(f"Received request on /note/add...")
if not file and not text: if not file and not text:
L.WARN(f"... without any file or text!")
raise HTTPException(status_code=400, detail="Either text or a file must be provided") raise HTTPException(status_code=400, detail="Either text or a file must be provided")
else: else:
result = await process_for_daily_note(file, text, source) result = await process_for_daily_note(file, text, source, bg_tasks)
L.INFO(f"Result on /note/add: {result}") L.INFO(f"Result on /note/add: {result}")
return JSONResponse(result, status_code=204) return JSONResponse(result, status_code=204)
async def process_for_daily_note(file: Optional[UploadFile] = File(None), text: Optional[str] = None, source: Optional[str] = None): async def process_for_daily_note(file: Optional[UploadFile] = File(None), text: Optional[str] = None, source: Optional[str] = None, bg_tasks: BackgroundTasks = None):
now = datetime.now() now = datetime.now()
transcription_entry = "" transcription_entry = ""
file_entry = "" file_entry = ""
if file: if file:
L.DEBUG("File received...")
file_content = await file.read() file_content = await file.read()
audio_io = BytesIO(file_content) audio_io = BytesIO(file_content)
file_type, _ = mimetypes.guess_type(file.filename) file_type, _ = mimetypes.guess_type(file.filename)
L.DEBUG(f"Processing as {file_type}...")
if 'audio' in file_type: subdir = file_type.title() or "Documents"
subdir = "Audio"
elif 'image' in file_type:
subdir = "Images"
else:
subdir = "Documents"
absolute_path, relative_path = assemble_journal_path(now, subdir=subdir, filename=file.filename) absolute_path, relative_path = assemble_journal_path(now, subdir=subdir, filename=file.filename)
L.DEBUG(f"Destination path: {absolute_path}")
with open(absolute_path, 'wb') as f: with open(absolute_path, 'wb') as f:
f.write(file_content) f.write(file_content)
L.DEBUG(f"Processing {f.name}...")
if 'audio' in file_type: if 'audio' in file_type:
transcription = await asr.transcribe_audio(file_path=absolute_path, params=asr.TranscribeParams(model="small-en", language="en", threads=6)) transcription = await asr.transcribe_audio(file_path=absolute_path, params=asr.TranscribeParams(model="small-en", language="en", threads=6), bg_tasks=bg_tasks)
file_entry = f"![[{relative_path}]]" file_entry = f"![[{relative_path}]]"
elif 'image' in file_type: elif 'image' in file_type:
@ -209,7 +208,6 @@ async def process_for_daily_note(file: Optional[UploadFile] = File(None), text:
else: else:
file_entry = f"[Source]({relative_path})" file_entry = f"[Source]({relative_path})"
text_entry = text if text else "" text_entry = text if text else ""
L.DEBUG(f"transcription: {transcription}\nfile_entry: {file_entry}\ntext_entry: {text_entry}") L.DEBUG(f"transcription: {transcription}\nfile_entry: {file_entry}\ntext_entry: {text_entry}")
return await add_to_daily_note(transcription, file_entry, text_entry, now) return await add_to_daily_note(transcription, file_entry, text_entry, now)
@ -262,7 +260,7 @@ async def handle_text(title:str, summary:str, extracted_text:str, date_time: dat
async def process_document( async def process_document(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
document: File, document: File,
title: Optional[str] = None, title: Optional[str] = None,
tts_mode: str = "summary", tts_mode: str = "summary",
@ -301,7 +299,7 @@ added: {timestamp}
datetime_str = datetime.now().strftime("%Y%m%d%H%M%S") datetime_str = datetime.now().strftime("%Y%m%d%H%M%S")
audio_filename = f"{datetime_str} {readable_title}" audio_filename = f"{datetime_str} {readable_title}"
audio_path = await tts.generate_speech( audio_path = await tts.generate_speech(
background_tasks=background_tasks, bg_tasks=bg_tasks,
text=tts_text, text=tts_text,
voice=voice, voice=voice,
model="eleven_turbo_v2", model="eleven_turbo_v2",
@ -336,7 +334,7 @@ added: {timestamp}
async def process_article( async def process_article(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
url: str, url: str,
title: Optional[str] = None, title: Optional[str] = None,
encoding: str = 'utf-8', encoding: str = 'utf-8',
@ -397,7 +395,7 @@ tags:
datetime_str = datetime.now().strftime("%Y%m%d%H%M%S") datetime_str = datetime.now().strftime("%Y%m%d%H%M%S")
audio_filename = f"{datetime_str} {readable_title}" audio_filename = f"{datetime_str} {readable_title}"
try: try:
audio_path = await tts.generate_speech(background_tasks=background_tasks, text=tts_text, voice=voice, model="eleven_turbo_v2", podcast=True, title=audio_filename, audio_path = await tts.generate_speech(bg_tasks=bg_tasks, text=tts_text, voice=voice, model="eleven_turbo_v2", podcast=True, title=audio_filename,
output_dir=Path(OBSIDIAN_VAULT_DIR) / OBSIDIAN_RESOURCES_DIR) output_dir=Path(OBSIDIAN_VAULT_DIR) / OBSIDIAN_RESOURCES_DIR)
audio_ext = Path(audio_path).suffix audio_ext = Path(audio_path).suffix
obsidian_link = f"![[{OBSIDIAN_RESOURCES_DIR}/{audio_filename}{audio_ext}]]" obsidian_link = f"![[{OBSIDIAN_RESOURCES_DIR}/{audio_filename}{audio_ext}]]"
@ -502,7 +500,7 @@ async def html_to_markdown(url: str = None, source: str = None) -> Optional[str]
async def process_archive( async def process_archive(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
url: str, url: str,
title: Optional[str] = None, title: Optional[str] = None,
encoding: str = 'utf-8', encoding: str = 'utf-8',

View file

@ -136,7 +136,7 @@ async def hook_changedetection(webhook_data: dict):
@serve.post("/cl/search") @serve.post("/cl/search")
async def hook_cl_search(request: Request, background_tasks: BackgroundTasks): async def hook_cl_search(request: Request, bg_tasks: BackgroundTasks):
client_ip = request.client.host client_ip = request.client.host
L.DEBUG(f"Received request from IP: {client_ip}") L.DEBUG(f"Received request from IP: {client_ip}")
data = await request.json() data = await request.json()
@ -150,7 +150,7 @@ async def hook_cl_search(request: Request, background_tasks: BackgroundTasks):
json.dump(payload, file, indent=2) json.dump(payload, file, indent=2)
for result in results: for result in results:
background_tasks.add_task(cl_search_process_result, result) bg_tasks.add_task(cl_search_process_result, result)
return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK) return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK)
@serve.post("/cl/docket") @serve.post("/cl/docket")
@ -283,7 +283,7 @@ def shellfish_run_widget_command(args: List[str]):
### COURTLISTENER FUNCTIONS ### ### COURTLISTENER FUNCTIONS ###
async def cl_docket(data, client_ip, background_tasks: BackgroundTasks): async def cl_docket(data, client_ip, bg_tasks: BackgroundTasks):
payload = data['payload'] payload = data['payload']
results = data['payload']['results'] results = data['payload']['results']
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
@ -292,7 +292,7 @@ async def cl_docket(data, client_ip, background_tasks: BackgroundTasks):
json.dump(payload, file, indent=2) json.dump(payload, file, indent=2)
for result in results: for result in results:
background_tasks.add_task(cl_docket_process, result) bg_tasks.add_task(cl_docket_process, result)
return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK) return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK)
async def cl_docket_process(result): async def cl_docket_process(result):

View file

@ -87,7 +87,7 @@ def select_voice(voice_name: str) -> str:
@tts.post("/v1/audio/speech") @tts.post("/v1/audio/speech")
async def generate_speech_endpoint( async def generate_speech_endpoint(
request: Request, request: Request,
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
model: str = Form("eleven_turbo_v2"), model: str = Form("eleven_turbo_v2"),
text: Optional[str] = Form(None), text: Optional[str] = Form(None),
file: Optional[UploadFile] = File(None), file: Optional[UploadFile] = File(None),
@ -110,7 +110,7 @@ async def generate_speech_endpoint(
else: else:
return await stream_tts(text_content, speed, voice, voice_file) return await stream_tts(text_content, speed, voice, voice_file)
else: else:
return await generate_speech(background_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:
L.ERR(f"Error in TTS: {str(e)}") L.ERR(f"Error in TTS: {str(e)}")
L.ERR(traceback.format_exc()) L.ERR(traceback.format_exc())
@ -118,7 +118,7 @@ async def generate_speech_endpoint(
async def generate_speech( async def generate_speech(
background_tasks: BackgroundTasks, bg_tasks: BackgroundTasks,
text: str, text: str,
voice: str = None, voice: str = None,
voice_file: UploadFile = None, voice_file: UploadFile = None,
@ -142,8 +142,8 @@ async def generate_speech(
elif model == "xtts": elif model == "xtts":
L.INFO(f"Using XTTS2") L.INFO(f"Using XTTS2")
final_output_dir = await local_tts(text, speed, voice, voice_file, podcast, background_tasks, title, output_dir) final_output_dir = await local_tts(text, speed, voice, voice_file, podcast, bg_tasks, title, output_dir)
background_tasks.add_task(os.remove, str(final_output_dir)) bg_tasks.add_task(os.remove, str(final_output_dir))
return str(final_output_dir) return str(final_output_dir)
else: else:
raise HTTPException(status_code=400, detail="Invalid model specified") raise HTTPException(status_code=400, detail="Invalid model specified")
@ -282,7 +282,7 @@ async def local_tts(
voice: str, voice: str,
voice_file = None, voice_file = None,
podcast: bool = False, podcast: bool = False,
background_tasks: BackgroundTasks = None, bg_tasks: BackgroundTasks = None,
title: str = None, title: str = None,
output_path: Optional[Path] = None output_path: Optional[Path] = None
) -> str: ) -> str: