From 42bbc2597c465c139f00a056f6b25f0cf1a5940e Mon Sep 17 00:00:00 2001
From: sanj <67624670+iodrift@users.noreply.github.com>
Date: Mon, 1 Jul 2024 20:48:53 -0700
Subject: [PATCH] Auto-update: Mon Jul  1 20:48:53 PDT 2024

---
 sijapi/__init__.py      |  1 +
 sijapi/__main__.py      |  1 +
 sijapi/classes.py       |  1 +
 sijapi/logs.py          |  1 +
 sijapi/routers/asr.py   |  1 +
 sijapi/routers/ig.py    |  4 ++--
 sijapi/routers/img.py   | 32 +++++++++++++++---------------
 sijapi/routers/llm.py   |  1 +
 sijapi/routers/news.py  | 12 ++++++------
 sijapi/routers/note.py  |  1 +
 sijapi/routers/serve.py |  4 ++--
 sijapi/routers/tts.py   | 43 +++++++++++++++++++++++++----------------
 sijapi/utilities.py     |  1 +
 13 files changed, 60 insertions(+), 43 deletions(-)

diff --git a/sijapi/__init__.py b/sijapi/__init__.py
index eeae5a9..0aeb2e8 100644
--- a/sijapi/__init__.py
+++ b/sijapi/__init__.py
@@ -1,3 +1,4 @@
+# __init__.py
 import os
 import json
 import yaml
diff --git a/sijapi/__main__.py b/sijapi/__main__.py
index c3ee5f5..c5ef1bb 100755
--- a/sijapi/__main__.py
+++ b/sijapi/__main__.py
@@ -1,4 +1,5 @@
 #!/Users/sij/miniforge3/envs/api/bin/python
+#__main__.py
 from fastapi import FastAPI, Request, HTTPException, Response
 from fastapi.responses import JSONResponse
 from fastapi.middleware.cors import CORSMiddleware
diff --git a/sijapi/classes.py b/sijapi/classes.py
index 481cf05..1dd34c3 100644
--- a/sijapi/classes.py
+++ b/sijapi/classes.py
@@ -1,3 +1,4 @@
+# classes.py
 import asyncio
 import json
 import math
diff --git a/sijapi/logs.py b/sijapi/logs.py
index 2142a51..4c97f1a 100644
--- a/sijapi/logs.py
+++ b/sijapi/logs.py
@@ -1,3 +1,4 @@
+# logs.py
 import os
 import sys
 from loguru import logger
diff --git a/sijapi/routers/asr.py b/sijapi/routers/asr.py
index adf2caf..399ab15 100644
--- a/sijapi/routers/asr.py
+++ b/sijapi/routers/asr.py
@@ -1,6 +1,7 @@
 '''
 Uses whisper_cpp to create an OpenAI-compatible Whisper web service.
 '''
+# routers/asr.py
 import os
 import sys
 import uuid
diff --git a/sijapi/routers/ig.py b/sijapi/routers/ig.py
index 8dd1e48..6ab6775 100644
--- a/sijapi/routers/ig.py
+++ b/sijapi/routers/ig.py
@@ -66,7 +66,7 @@ IG_VISION_LLM = os.getenv("IG_VISION_LLM")
 IG_PROMPT_LLM = os.getenv("IG_PROMPT_LLM")
 IG_IMG_GEN = os.getenv("IG_IMG_GEN", "ComfyUI")
 IG_OUTPUT_PLATFORMS = os.getenv("IG_OUTPUT_PLATFORMS", "ig,ghost,obsidian").split(',')
-SD_WORKFLOWS_DIR = os.path.join(COMFYUI_DIR, 'workflows')
+IMG_WORKFLOWS_DIR = os.path.join(COMFYUI_DIR, 'workflows')
 COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output'
 IG_PROFILES_DIR = os.path.join(BASE_DIR, 'profiles')
 IG_PROFILE_DIR = os.path.join(IG_PROFILES_DIR, PROFILE)
@@ -793,7 +793,7 @@ def load_json(json_payload, workflow):
     if json_payload:
         return json.loads(json_payload)
     elif workflow:
-        workflow_path = os.path.join(SD_WORKFLOWS_DIR, f"{workflow}.json" if not workflow.endswith('.json') else workflow)
+        workflow_path = os.path.join(IMG_WORKFLOWS_DIR, f"{workflow}.json" if not workflow.endswith('.json') else workflow)
         with open(workflow_path, 'r') as file:
             return json.load(file)
     else:
diff --git a/sijapi/routers/img.py b/sijapi/routers/img.py
index e19ce14..7748a44 100644
--- a/sijapi/routers/img.py
+++ b/sijapi/routers/img.py
@@ -2,7 +2,7 @@
 Image generation module using StableDiffusion and similar models by way of ComfyUI.
 DEPENDS ON:
   LLM module
-  COMFYUI_URL, COMFYUI_DIR, COMFYUI_OUTPUT_DIR, TS_SUBNET, TS_ADDRESS, DATA_DIR, SD_CONFIG_DIR, SD_IMAGE_DIR, SD_WORKFLOWS_DIR, LOCAL_HOSTS, API.URL, PHOTOPRISM_USER*, PHOTOPRISM_URL*, PHOTOPRISM_PASS*
+  COMFYUI_URL, COMFYUI_DIR, COMFYUI_OUTPUT_DIR, TS_SUBNET, TS_ADDRESS, DATA_DIR, IMG_CONFIG_DIR, IMG_DIR, IMG_WORKFLOWS_DIR, LOCAL_HOSTS, API.URL, PHOTOPRISM_USER*, PHOTOPRISM_URL*, PHOTOPRISM_PASS*
 *unimplemented.
 '''
 
@@ -30,7 +30,7 @@ import shutil
 # from photoprism.Photo import Photo
 # from webdav3.client import Client
 from sijapi.routers.llm import query_ollama
-from sijapi import API, L, COMFYUI_URL, COMFYUI_OUTPUT_DIR, SD_CONFIG_PATH, SD_IMAGE_DIR, SD_WORKFLOWS_DIR
+from sijapi import API, L, COMFYUI_URL, COMFYUI_OUTPUT_DIR, IMG_CONFIG_PATH, IMG_DIR, IMG_WORKFLOWS_DIR
 
 img = APIRouter()
 
@@ -86,7 +86,7 @@ async def workflow(prompt: str, scene: str = None, size: str = None, earlyout: s
     width, height = map(int, size.split('x'))
     L.DEBUG(f"Parsed width: {width}; parsed height: {height}")
 
-    workflow_path = Path(SD_WORKFLOWS_DIR) / scene_workflow['workflow']
+    workflow_path = Path(IMG_WORKFLOWS_DIR) / scene_workflow['workflow']
     workflow_data = json.loads(workflow_path.read_text())
 
     post = {
@@ -104,7 +104,7 @@ async def workflow(prompt: str, scene: str = None, size: str = None, earlyout: s
     print(f"Prompt ID: {prompt_id}")
 
     max_size = max(width, height) if downscale_to_fit else None
-    destination_path = Path(destination_path).with_suffix(".jpg") if destination_path else SD_IMAGE_DIR / f"{prompt_id}.jpg"
+    destination_path = Path(destination_path).with_suffix(".jpg") if destination_path else IMG_DIR / f"{prompt_id}.jpg"
 
     if earlyout:
         asyncio.create_task(generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path))
@@ -132,7 +132,7 @@ async def generate_and_save_image(prompt_id, saved_file_key, max_size, destinati
     
     
 def get_web_path(file_path: Path) -> str:
-    uri = file_path.relative_to(SD_IMAGE_DIR)
+    uri = file_path.relative_to(IMG_DIR)
     web_path = f"{API.URL}/img/{uri}"
     return web_path
 
@@ -174,8 +174,8 @@ async def get_image(status_data, key):
 
 
 async def save_as_jpg(image_data, prompt_id, max_size = None, quality = 100, destination_path: Path = None):
-    destination_path_png = (SD_IMAGE_DIR / prompt_id).with_suffix(".png")
-    destination_path_jpg = destination_path.with_suffix(".jpg") if destination_path else (SD_IMAGE_DIR / prompt_id).with_suffix(".jpg")
+    destination_path_png = (IMG_DIR / prompt_id).with_suffix(".png")
+    destination_path_jpg = destination_path.with_suffix(".jpg") if destination_path else (IMG_DIR / prompt_id).with_suffix(".jpg")
 
     try:
         destination_path_png.parent.mkdir(parents=True, exist_ok=True)
@@ -224,16 +224,16 @@ def set_presets(workflow_data, preset_values):
 
 
 def get_return_path(destination_path):
-    sd_dir = Path(SD_IMAGE_DIR)
+    sd_dir = Path(IMG_DIR)
     if destination_path.parent.samefile(sd_dir):
         return destination_path.name
     else:
         return str(destination_path)
 
 def get_scene(scene):
-    with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file:
-        SD_CONFIG = yaml.safe_load(SD_CONFIG_file)
-    for scene_data in SD_CONFIG['scenes']:
+    with open(IMG_CONFIG_PATH, 'r') as IMG_CONFIG_file:
+        IMG_CONFIG = yaml.safe_load(IMG_CONFIG_file)
+    for scene_data in IMG_CONFIG['scenes']:
         if scene_data['scene'] == scene:
             L.DEBUG(f"Found scene for \"{scene}\".")
             return scene_data
@@ -246,9 +246,9 @@ def get_matching_scene(prompt):
     prompt_lower = prompt.lower()
     max_count = 0
     scene_data = None
-    with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file:
-        SD_CONFIG = yaml.safe_load(SD_CONFIG_file)
-    for sc in SD_CONFIG['scenes']:
+    with open(IMG_CONFIG_PATH, 'r') as IMG_CONFIG_file:
+        IMG_CONFIG = yaml.safe_load(IMG_CONFIG_file)
+    for sc in IMG_CONFIG['scenes']:
         count = sum(1 for trigger in sc['triggers'] if trigger in prompt_lower)
         if count > max_count:
             max_count = count
@@ -259,7 +259,7 @@ def get_matching_scene(prompt):
         return scene_data
     else:
         L.DEBUG(f"No matching scenes found, falling back to default scene.")
-        return SD_CONFIG['scenes'][0]
+        return IMG_CONFIG['scenes'][0]
 
 
 
@@ -400,7 +400,7 @@ async def get_generation_options():
 
 
 async def load_workflow(workflow_path: str, workflow:str):
-    workflow_path = workflow_path if workflow_path else os.path.join(SD_WORKFLOWS_DIR, f"{workflow}.json" if not workflow.endswith('.json') else workflow)
+    workflow_path = workflow_path if workflow_path else os.path.join(IMG_WORKFLOWS_DIR, f"{workflow}.json" if not workflow.endswith('.json') else workflow)
     with open(workflow_path, 'r') as file:
         return json.load(file)
 
diff --git a/sijapi/routers/llm.py b/sijapi/routers/llm.py
index d2be642..4a19e3b 100644
--- a/sijapi/routers/llm.py
+++ b/sijapi/routers/llm.py
@@ -1,6 +1,7 @@
 '''
 Interfaces with Ollama and creates an OpenAI-compatible relay API.
 '''
+# routers/llm.py
 from fastapi import APIRouter, HTTPException, Request, Response, BackgroundTasks, File, Form, UploadFile
 from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
 from datetime import datetime as dt_datetime
diff --git a/sijapi/routers/news.py b/sijapi/routers/news.py
index 9d7c5d9..32097ca 100644
--- a/sijapi/routers/news.py
+++ b/sijapi/routers/news.py
@@ -1,3 +1,4 @@
+# routers/news.py
 import os
 import uuid
 import asyncio
@@ -90,7 +91,7 @@ async def download_and_save_article(article, site_name, earliest_date, bg_tasks:
         try:
             banner_url = article.top_image
             if banner_url:
-                banner_image = download_file(banner_url, Path(OBSIDIAN_VAULT_DIR / OBSIDIAN_RESOURCES_DIR))
+                banner_image = download_file(banner_url, Path(OBSIDIAN_VAULT_DIR / OBSIDIAN_RESOURCES_DIR / f"{dt_datetime.now().strftime('%Y%m%d%H%M%S')}.jpg"))
                 if banner_image:
                     banner_markdown = f"![[{OBSIDIAN_RESOURCES_DIR}/{banner_image}]]"
         except Exception as e:
@@ -120,7 +121,7 @@ tags:
                     bg_tasks=bg_tasks, 
                     text=tts_text, 
                     voice=voice, 
-                    model="eleven_turbo_v2", 
+                    model="xtts2", 
                     podcast=True, 
                     title=audio_filename,
                     output_dir=Path(OBSIDIAN_VAULT_DIR) / OBSIDIAN_RESOURCES_DIR
@@ -176,7 +177,7 @@ async def process_news_site(site, bg_tasks: BackgroundTasks):
                 earliest_date, 
                 bg_tasks, 
                 tts_mode=site.tts if hasattr(site, 'tts') else "off",
-                voice=site.tts if hasattr(site, 'tts') else DEFAULT_11L_VOICE
+                voice=site.voice if hasattr(site, 'voice') else DEFAULT_11L_VOICE
             ))
             tasks.append(task)
         
@@ -237,12 +238,11 @@ async def archive_post(
 async def clip_get(
     bg_tasks: BackgroundTasks,
     url: str,
-    title: Optional[str] = Query(None),
-    encoding: str = Query('utf-8'),
     tts: str = Query('summary'),
     voice: str = Query(DEFAULT_VOICE)
 ):
-    markdown_filename = await process_article(bg_tasks, url, title, encoding, tts=tts, voice=voice)
+    parsed_content = await parse_article(url)
+    markdown_filename = await process_article(bg_tasks, parsed_content, tts, voice)
     return {"message": "Clip saved successfully", "markdown_filename": markdown_filename}
 
 @news.post("/note/add")
diff --git a/sijapi/routers/note.py b/sijapi/routers/note.py
index 38ae5ad..1fd2f00 100644
--- a/sijapi/routers/note.py
+++ b/sijapi/routers/note.py
@@ -1,6 +1,7 @@
 '''
 Manages an Obsidian vault, in particular daily notes, using information and functionality drawn from the other routers, primarily calendar, email, ig, llm, rag, img, serve, time, tts, and weather.
 '''
+# routers/note.py
 from fastapi import APIRouter, BackgroundTasks, File, UploadFile, Form, HTTPException, Response, Query, Path as FastAPIPath
 from fastapi.responses import JSONResponse, PlainTextResponse
 import os, re
diff --git a/sijapi/routers/serve.py b/sijapi/routers/serve.py
index 67a617d..17a4416 100644
--- a/sijapi/routers/serve.py
+++ b/sijapi/routers/serve.py
@@ -30,7 +30,7 @@ from selenium.webdriver.support import expected_conditions as EC
 from sijapi import (
     L, LOGS_DIR, TS_ID, CASETABLE_PATH, COURTLISTENER_DOCKETS_URL, COURTLISTENER_API_KEY,
     COURTLISTENER_BASE_URL, COURTLISTENER_DOCKETS_DIR, COURTLISTENER_SEARCH_DIR, ALERTS_DIR,
-    MAC_UN, MAC_PW, MAC_ID, TS_TAILNET, DATA_DIR, SD_IMAGE_DIR, PUBLIC_KEY, OBSIDIAN_VAULT_DIR
+    MAC_UN, MAC_PW, MAC_ID, TS_TAILNET, DATA_DIR, IMG_DIR, PUBLIC_KEY, OBSIDIAN_VAULT_DIR
 )
 from sijapi.utilities import bool_convert, sanitize_filename, assemble_journal_path
 from sijapi.routers import loc, note
@@ -44,7 +44,7 @@ async def get_pgp():
 
 @serve.get("/img/{image_name}")
 def serve_image(image_name: str):
-    image_path = os.path.join(SD_IMAGE_DIR, image_name)
+    image_path = os.path.join(IMG_DIR, image_name)
     if os.path.exists(image_path):
         return FileResponse(image_path)
     else:
diff --git a/sijapi/routers/tts.py b/sijapi/routers/tts.py
index b5b8dc0..2972607 100644
--- a/sijapi/routers/tts.py
+++ b/sijapi/routers/tts.py
@@ -14,7 +14,7 @@ from typing import Optional, Union, List
 from pydub import AudioSegment
 from TTS.api import TTS
 from pathlib import Path
-from datetime import datetime
+from datetime import datetime as dt_datetime
 from time import time
 import torch
 import traceback
@@ -66,21 +66,26 @@ async def list_11l_voices():
      
         
 
-
-def select_voice(voice_name: str) -> str:
+async def select_voice(voice_name: str) -> str:
     try:
-        voice_file = VOICE_DIR / f"{voice_name}.wav"
-        L.DEBUG(f"select_voice received query to use voice: {voice_name}. Looking for {voice_file} inside {VOICE_DIR}.")
+        # Case Insensitive comparison
+        voice_name_lower = voice_name.lower()
+        L.DEBUG(f"Looking for {voice_name_lower}")
+        for item in VOICE_DIR.iterdir():
+            L.DEBUG(f"Checking {item.name.lower()}")
+            if item.name.lower() == f"{voice_name_lower}.wav":
+                L.DEBUG(f"select_voice received query to use voice: {voice_name}. Found {item} inside {VOICE_DIR}.")
+                return str(item)
+
+        L.ERR(f"Voice file not found")
+        raise HTTPException(status_code=404, detail="Voice file not found")
 
-        if voice_file.is_file():
-            return str(voice_file)
-        else:
-            raise HTTPException(status_code=404, detail="Voice file not found")
     except Exception as e:
         L.ERR(f"Voice file not found: {str(e)}")
         return None
 
 
+
 @tts.post("/tts")
 @tts.post("/tts/speak")
 @tts.post("/v1/audio/speech")
@@ -132,13 +137,14 @@ async def generate_speech(
 
     try:
         model = model if model else await get_model(voice, voice_file)
-
+        title = title if title else "TTS audio"
+        output_path = output_dir / f"{dt_datetime.now().strftime('%Y%m%d%H%M%S')} {title}.wav"
         if model == "eleven_turbo_v2":
             L.INFO("Using ElevenLabs.")
             audio_file_path = await elevenlabs_tts(model, text, voice, title, output_dir)
         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)
+            audio_file_path = await local_tts(text, speed, voice, voice_file, podcast, bg_tasks, title, output_path)
         #else:
         #    raise HTTPException(status_code=400, detail="Invalid model specified")
 
@@ -158,7 +164,7 @@ async def generate_speech(
 
 
 async def get_model(voice: str = None, voice_file: UploadFile = None):
-    if voice_file or (voice and select_voice(voice)):
+    if voice_file or (voice and await select_voice(voice)):
         return "xtts"
     
     elif voice and await determine_voice_id(voice):
@@ -220,7 +226,7 @@ async def elevenlabs_tts(model: str, input_text: str, voice: str, title: str = N
     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")
+        title = title if title else dt_datetime.now().strftime("%Y%m%d%H%M%S")
         filename = f"{sanitize_filename(title)}.mp3"
         file_path = Path(output_dir) / filename
         if response.status_code == 200:            
@@ -245,7 +251,9 @@ async def get_text_content(text: Optional[str], file: Optional[UploadFile]) -> s
 
 async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None) -> str:
     if voice:
-        return select_voice(voice)
+        L.DEBUG(f"Looking for voice: {voice}")
+        selected_voice = await select_voice(voice)
+        return selected_voice
     elif voice_file and isinstance(voice_file, UploadFile):
         VOICE_DIR.mkdir(exist_ok=True)
 
@@ -272,8 +280,9 @@ async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None)
         return str(new_file)
     
     else:
-        L.DEBUG(f"{datetime.now().strftime('%Y%m%d%H%M%S')}: No voice specified or file provided, using default voice: {DEFAULT_VOICE}")
-        return select_voice(DEFAULT_VOICE)
+        L.DEBUG(f"{dt_datetime.now().strftime('%Y%m%d%H%M%S')}: No voice specified or file provided, using default voice: {DEFAULT_VOICE}")
+        selected_voice = await select_voice(DEFAULT_VOICE)
+        return selected_voice
 
 
 
@@ -290,7 +299,7 @@ async def local_tts(
     if output_path:
         file_path = Path(output_path)
     else:
-        datetime_str = datetime.now().strftime("%Y%m%d%H%M%S")
+        datetime_str = dt_datetime.now().strftime("%Y%m%d%H%M%S")
         title = sanitize_filename(title) if title else "Audio"
         filename = f"{datetime_str}_{title}.wav"
         file_path = TTS_OUTPUT_DIR / filename
diff --git a/sijapi/utilities.py b/sijapi/utilities.py
index e9f6395..6ee7fa4 100644
--- a/sijapi/utilities.py
+++ b/sijapi/utilities.py
@@ -1,3 +1,4 @@
+# utilities.py
 import re
 import os
 from fastapi import Form