diff --git a/sijapi/__init__.py b/sijapi/__init__.py index 6013a75..254fba5 100644 --- a/sijapi/__init__.py +++ b/sijapi/__init__.py @@ -12,7 +12,7 @@ from typing import List, Optional import traceback import logging from .logs import Logger -from .classes import AutoResponder, IMAPConfig, SMTPConfig, EmailAccount, EmailContact, IncomingEmail +from .classes import AutoResponder, IMAPConfig, SMTPConfig, EmailAccount, EmailContact, IncomingEmail, TimezoneTracker, Database # from sijapi.config.config import load_config # cfg = load_config() @@ -43,6 +43,7 @@ os.makedirs(LOGS_DIR, exist_ok=True) load_dotenv(ENV_PATH) ### API essentials +DB = Database.from_env() ROUTERS = os.getenv('ROUTERS', '').split(',') PUBLIC_SERVICES = os.getenv('PUBLIC_SERVICES', '').split(',') GLOBAL_API_KEY = os.getenv("GLOBAL_API_KEY") @@ -68,29 +69,19 @@ os.makedirs(REQUESTS_DIR, exist_ok=True) REQUESTS_LOG_PATH = LOGS_DIR / "requests.log" -### Databases -DB = os.getenv("DB", 'sijdb') -DB_HOST = os.getenv("DB_HOST", "127.0.0.1") -DB_PORT = os.getenv("DB_PORT", 5432) -DB_USER = os.getenv("DB_USER", 'sij') -DB_PASS = os.getenv("DB_PASS") -DB_SSH = os.getenv("DB_SSH", "100.64.64.15") -DB_SSH_USER = os.getenv("DB_SSH_USER") -DB_SSH_PASS = os.getenv("DB_SSH_ENV") -DB_URL = f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB}' - ### LOCATE AND WEATHER LOCALIZATIONS USER_FULLNAME = os.getenv('USER_FULLNAME') USER_BIO = os.getenv('USER_BIO') -TZ = tz.gettz(os.getenv("TZ", "America/Los_Angeles")) HOME_ZIP = os.getenv("HOME_ZIP") # unimplemented -LOCATION_OVERRIDES = DATA_DIR / "loc_overrides.json" -LOCATIONS_CSV = DATA_DIR / "US.csv" +NAMED_LOCATIONS = CONFIG_DIR / "named-locations.yaml" # DB = DATA_DIR / "weatherlocate.db" # deprecated VISUALCROSSING_BASE_URL = os.getenv("VISUALCROSSING_BASE_URL", "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline") VISUALCROSSING_API_KEY = os.getenv("VISUALCROSSING_API_KEY") - +GEONAMES_TXT = DATA_DIR / "geonames.txt" +LOCATIONS_CSV = DATA_DIR / "US.csv" +TZ = tz.gettz(os.getenv("TZ", "America/Los_Angeles")) +DynamicTZ = TimezoneTracker(DB) ### Obsidian & notes ALLOWED_FILENAME_CHARS = r'[^\w \.-]' @@ -131,7 +122,7 @@ COMFYUI_URL = os.getenv('COMFYUI_URL', "http://localhost:8188") COMFYUI_DIR = Path(os.getenv('COMFYUI_DIR')) COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output' COMFYUI_LAUNCH_CMD = os.getenv('COMFYUI_LAUNCH_CMD', 'mamba activate comfyui && python main.py') -SD_CONFIG_PATH = CONFIG_DIR / 'sd.json' +SD_CONFIG_PATH = CONFIG_DIR / 'sd.yaml' ### Summarization SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens @@ -155,7 +146,7 @@ TTS_DIR = DATA_DIR / "tts" os.makedirs(TTS_DIR, exist_ok=True) VOICE_DIR = TTS_DIR / 'voices' os.makedirs(VOICE_DIR, exist_ok=True) -PODCAST_DIR = TTS_DIR / "sideloads" +PODCAST_DIR = os.getenv("PODCAST_DIR", TTS_DIR / "sideloads") os.makedirs(PODCAST_DIR, exist_ok=True) TTS_OUTPUT_DIR = TTS_DIR / 'outputs' os.makedirs(TTS_OUTPUT_DIR, exist_ok=True) @@ -169,13 +160,7 @@ ICAL_TOGGLE = True if os.getenv("ICAL_TOGGLE") == "True" else False ICS_PATH = DATA_DIR / 'calendar.ics' # deprecated now, but maybe revive? ICALENDARS = os.getenv('ICALENDARS', 'NULL,VOID').split(',') -def load_email_accounts(yaml_path: str) -> List[EmailAccount]: - with open(yaml_path, 'r') as file: - config = yaml.safe_load(file) - return [EmailAccount(**account) for account in config['accounts']] - EMAIL_CONFIG = CONFIG_DIR / "email.yaml" -EMAIL_ACCOUNTS = load_email_accounts(EMAIL_CONFIG) AUTORESPOND = True ### Courtlistener & other webhooks diff --git a/sijapi/classes.py b/sijapi/classes.py index 559bdb1..809c8f8 100644 --- a/sijapi/classes.py +++ b/sijapi/classes.py @@ -1,6 +1,65 @@ from pydantic import BaseModel -from typing import List, Optional, Any -from datetime import datetime +from typing import List, Optional, Any, Tuple, Dict, Union, Tuple +from datetime import datetime, timedelta +import asyncio +import asyncpg +import json +from pydantic import BaseModel, Field +from typing import Optional +import asyncpg +import os + +from pydantic import BaseModel, Field +from typing import Optional + +from pydantic import BaseModel, Field +from typing import Optional +import asyncpg + +from pydantic import BaseModel, Field +from typing import Optional +import asyncpg +from contextlib import asynccontextmanager + +class Database(BaseModel): + host: str = Field(..., description="Database host") + port: int = Field(5432, description="Database port") + user: str = Field(..., description="Database user") + password: str = Field(..., description="Database password") + database: str = Field(..., description="Database name") + db_schema: Optional[str] = Field(None, description="Database schema") + + @asynccontextmanager + async def get_connection(self): + conn = await asyncpg.connect( + host=self.host, + port=self.port, + user=self.user, + password=self.password, + database=self.database + ) + try: + if self.db_schema: + await conn.execute(f"SET search_path TO {self.db_schema}") + yield conn + finally: + await conn.close() + + @classmethod + def from_env(cls): + import os + return cls( + host=os.getenv("DB_HOST", "localhost"), + port=int(os.getenv("DB_PORT", 5432)), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD"), + database=os.getenv("DB_NAME"), + db_schema=os.getenv("DB_SCHEMA") + ) + + def to_dict(self): + return self.dict(exclude_none=True) + class AutoResponder(BaseModel): name: str @@ -8,7 +67,7 @@ class AutoResponder(BaseModel): context: str whitelist: List[str] blacklist: List[str] - img_gen_prompt: Optional[str] = None + image_prompt: Optional[str] = None class IMAPConfig(BaseModel): username: str @@ -26,20 +85,131 @@ class SMTPConfig(BaseModel): class EmailAccount(BaseModel): name: str + refresh: int fullname: Optional[str] bio: Optional[str] + summarize: bool = False + podcast: bool = False imap: IMAPConfig smtp: SMTPConfig autoresponders: Optional[List[AutoResponder]] class EmailContact(BaseModel): email: str - name: str + name: Optional[str] = None class IncomingEmail(BaseModel): sender: str - recipients: List[EmailContact] datetime_received: datetime + recipients: List[EmailContact] subject: str body: str - attachments: Optional[List[Any]] = None \ No newline at end of file + attachments: List[dict] = [] + + + + + +class Location(BaseModel): + latitude: float + longitude: float + datetime: datetime + elevation: Optional[float] = None + altitude: Optional[float] = None + zip: Optional[str] = None + street: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + country: Optional[str] = None + context: Optional[Dict[str, Any]] = None + class_: Optional[str] = None + type: Optional[str] = None + name: Optional[str] = None + display_name: Optional[str] = None + boundingbox: Optional[List[str]] = None + amenity: Optional[str] = None + house_number: Optional[str] = None + road: Optional[str] = None + quarter: Optional[str] = None + neighbourhood: Optional[str] = None + suburb: Optional[str] = None + county: Optional[str] = None + country_code: Optional[str] = None + + class Config: + json_encoders = { + datetime: lambda dt: dt.isoformat(), + } + + +class TimezoneTracker: + def __init__(self, db_config: Database, cache_file: str = 'timezone_cache.json'): + self.db_config = db_config + self.cache_file = cache_file + self.last_timezone: str = "America/Los_Angeles" + self.last_update: Optional[datetime] = None + self.last_location: Optional[Tuple[float, float]] = None + + async def find(self, lat: float, lon: float) -> str: + query = """ + SELECT tzid + FROM timezones + WHERE ST_Contains(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)) + LIMIT 1; + """ + async with await self.db_config.get_connection() as conn: + result = await conn.fetchrow(query, lon, lat) + return result['tzid'] if result else 'Unknown' + + async def refresh(self, location: Union[Location, Tuple[float, float]], force: bool = False) -> str: + if isinstance(location, Location): + lat, lon = location.latitude, location.longitude + else: + lat, lon = location + + current_time = datetime.now() + + if (force or + not self.last_update or + current_time - self.last_update > timedelta(hours=1) or + self.last_location != (lat, lon)): + + new_timezone = await self.find(lat, lon) + + self.last_timezone = new_timezone + self.last_update = current_time + self.last_location = (lat, lon) + + await self.save_to_cache() + + return new_timezone + + return self.last_timezone + + async def save_to_cache(self): + cache_data = { + 'last_timezone': self.last_timezone, + 'last_update': self.last_update.isoformat() if self.last_update else None, + 'last_location': self.last_location + } + with open(self.cache_file, 'w') as f: + json.dump(cache_data, f) + + async def load_from_cache(self): + try: + with open(self.cache_file, 'r') as f: + cache_data = json.load(f) + self.last_timezone = cache_data.get('last_timezone') + self.last_update = datetime.fromisoformat(cache_data['last_update']) if cache_data.get('last_update') else None + self.last_location = tuple(cache_data['last_location']) if cache_data.get('last_location') else None + except (FileNotFoundError, json.JSONDecodeError): + # If file doesn't exist or is invalid, we'll start fresh + pass + + async def get_current(self, location: Union[Location, Tuple[float, float]]) -> str: + await self.load_from_cache() + return await self.refresh(location) + + async def get_last(self) -> Optional[str]: + await self.load_from_cache() + return self.last_timezone diff --git a/sijapi/config/.env-example b/sijapi/config/.env-example index 0c31cb7..e7f7986 100644 --- a/sijapi/config/.env-example +++ b/sijapi/config/.env-example @@ -96,7 +96,7 @@ TRUSTED_SUBNETS=127.0.0.1/32,10.13.37.0/24,100.64.64.0/24 # ────────── # #─── router selection: ──────────────────────────────────────────────────────────── -ROUTERS=asr,calendar,cf,email,health,hooks,llm,locate,note,rag,sd,serve,summarize,time,tts,weather +ROUTERS=asr,calendar,cf,email,health,hooks,llm,locate,note,rag,sd,serve,time,tts,weather UNLOADED=ig #─── notes: ────────────────────────────────────────────────────────────────────── # @@ -218,18 +218,18 @@ TAILSCALE_API_KEY=¿SECRET? # <--- enter your own TS API key # ░░ ░ ░T̷ O̷ G̷ E̷ T̷ H̷ ░ R̷. ░ ░ ░ ░ ░ # J U S T ░ #─── frag, or weat,and locate modules:── H O L D M Y H A N D. -DB=db +DB_NAME=db # DB_HOST=127.0.0.1 DB_PORT=5432 # R E A L T I G H T. DB_USER=postgres -DB_PASS=¿SECRET? # <--- enter your own Postgres password' +DB_PASSWORD=¿SECRET? # <--- enter your own Postgres password' # Y E A H . . . DB_SSH=100.64.64.15 # . . . 𝙹 𝚄 𝚂 𝚃 𝙻 𝙸 𝙺 𝙴 𝚃 𝙷 𝙰 𝚃. DB_SSH_USER=sij -DB_SSH_PASS=¿SECRET? # <--- enter SSH password for pg server (if not localhost) +DB_SSH_PASS=¿SECRET? # <--- enter SSH password for pg server (if not localhost) #─── notes: ────────────────────────────────────────────────── S E E ? 𝕰 𝖅 - 𝕻 𝖅 # # DB, DB_HOST, DB_PORT, DB_USER, and DB_PASS should specify those respective diff --git a/sijapi/config/email.yaml-example b/sijapi/config/email.yaml-example new file mode 100644 index 0000000..c4ad15f --- /dev/null +++ b/sijapi/config/email.yaml-example @@ -0,0 +1,70 @@ +accounts: + - name: REDACT@email.com + fullname: Your full name + bio: 'an ai enthusiast' + imap: + username: REDACT@email.com + password: REDACT + host: '127.0.0.1' + port: 1142 + encryption: STARTTLS + smtp: + username: REDACT@email.com + password: REDACT + host: '127.0.0.1' + port: 1024 + encryption: SSL + autoresponders: + - name: work + style: professional + context: he is currently on leave and will return in late July + whitelist: + - '@work.org' + blacklist: + - 'spam@' + - unsubscribe + - 'no-reply@' + - name: ai + style: cryptic + context: respond to any inquiries with cryptic and vaguely menacing riddles, esoteric assertions, or obscure references. + image_prompt: using visually evocative words, phrases, and sentence fragments, describe an image inspired by the following prompt + whitelist: + - 'colleagues@work.org' + - 'jimbo@' + - 'internal work email:' + blacklist: + - personal + - private + - noneofyerdamnbusiness + - unsubscribe + - 'no-reply@' + - name: otherREDACT@email.com + fullname: sij.ai + bio: an AI bot that responds in riddles. + imap: + username: otherREDACT@email.com + password: REDACT + host: '127.0.0.1' + port: 1142 + encryption: STARTTLS + smtp: + username: otherREDACT@email.com + password: REDACT + host: '127.0.0.1' + port: 1024 + encryption: SSL + autoresponders: + - name: ai + style: cryptic + context: respond to any inquiries with cryptic and vaguely menacing riddles, esoteric assertions, or obscure references. + image_prompt: using visually evocative words, phrases, and sentence fragments, describe an image inspired by the following prompt + whitelist: + - 'bestfriend@gmail.com' + - 'eximstalking@' + - uniquephraseinsubjectorbody + - 'internal work email:' + blacklist: + - work + - '@work.org' + - unsubscribe + - 'no-reply@' diff --git a/sijapi/config/llms.json-example b/sijapi/config/llms.json-example deleted file mode 100644 index c75165d..0000000 --- a/sijapi/config/llms.json-example +++ /dev/null @@ -1,151 +0,0 @@ -{ - "Alpaca": { - "models": [ - "mythomax", - "openhermes", - "deepseek" - ], - "prefix": "\n### Instruction:\n", - "stops": [ - "### Instruction" - ], - "suffix": "\n### Response:\n", - "sysPrefix": "### System\n", - "sysSuffix": "\n" - }, - "Amazon": { - "models": [ - "mistrallite" - ], - "prefix": "<|prompter|>", - "stops": [ - "<|prompter|>", - "" - ], - "suffix": "<|assistant|>", - "sysPrefix": "", - "sysSuffix": "" - }, - "ChatML": { - "models": [ - "dolphin", - "capybara", - "nous-hermes-2" - ], - "prefix": "<|im_end|>\n<|im_start|>user\n", - "stops": [ - "<|im_end|>", - "<|im_start|>" - ], - "suffix": "<|im_end|>\n<|im_start|>assistant\n", - "sysPrefix": "<|im_start|>system\n", - "sysSuffix": "<|im_end|>" - }, - "Llama2": { - "models": [ - "llama2-placeholder" - ], - "prefix": "\n\n[INST] ", - "stops": [ - "[/INST]", - "[INST]" - ], - "suffix": "[/INST]\n\n", - "sysPrefix": "", - "sysSuffix": "\n\n" - }, - "Mistral": { - "models": [ - "mistral-instruct", - "mixtral-8x7b-instruct" - ], - "prefix": "\n[INST] ", - "stops": [ - "[/INST]", - "[INST]", - "" - ], - "suffix": "[/INST]\n", - "sysPrefix": "", - "sysSuffix": "\n" - }, - "Orca": { - "models": [ - "upstage", - "neural", - "solar", - "SOLAR" - ], - "prefix": "\n### User:\n", - "stops": [ - "###", - "User:" - ], - "suffix": "\n### Assistant:\n", - "sysPrefix": "### System:\n", - "sysSuffix": "\n" - }, - "Phi2": { - "models": [ - "phi-2" - ], - "prefix": "\nSangye: ", - "stops": [ - "###", - "User Message" - ], - "suffix": "\nAssistant: ", - "sysPrefix": "Systen: ", - "sysSuffix": "\n" - }, - "Phind": { - "models": [ - "phind" - ], - "prefix": "\n### User Message\n", - "stops": [ - "###", - "User Message" - ], - "suffix": "\n### Assistant\n", - "sysPrefix": "### System Prompt\n", - "sysSuffix": "\n" - }, - "Vicuna": { - "models": [ - "xwin", - "synthia", - "tess" - ], - "prefix": "\nUSER: ", - "stops": [ - "", - "USER:", - "SYSTEM:" - ], - "suffix": "\nASSISTANT: ", - "sysPrefix": "SYSTEM: ", - "sysSuffix": "\n" - }, - "Zephyr": { - "models": [ - "zephyr" - ], - "prefix": " ", - "stops": [ - "" - ], - "suffix": "\n ", - "sysPrefix": " ", - "sysSuffix": "\n" - }, - "default": { - "prefix": "\n### Instruction:\n", - "stops": [ - "### Instruction" - ], - "suffix": "\n### Response:\n", - "sysPrefix": "### System\n", - "sysSuffix": "\n" - } -} diff --git a/sijapi/config/sd.json b/sijapi/config/sd.json index f6ea537..215c20a 100644 --- a/sijapi/config/sd.json +++ b/sijapi/config/sd.json @@ -3,19 +3,19 @@ { "scene": "default", "triggers": [""], - "API_PPrompt": "(Highly-detailed) image of ", - "API_SPrompt": "; ((masterpiece)); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "`oil, paint splash, oil effect, dots, paint, freckles, liquid effect, canvas frame, 3d, bad art, asian, illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, explicit, topless`", + "API_PPrompt": "Highly-detailed image of ", + "API_SPrompt": ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody", + "API_NPrompt": "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon", "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this scene description to its essence, staying true to what it describes: ", - "workflows": [{"workflow": "turbo.json", "size": "1024x768"}] + "workflows": [{"workflow": "default.json", "size": "1024x768"}] }, { "scene": "wallpaper", "triggers": ["wallpaper"], "API_PPrompt": "Stunning widescreen image of ", - "API_SPrompt": ", masterpiece, (subtle:0.7), (nuanced:0.6), best quality, ultra detailed, ultra high resolution, 8k, (documentary:0.3), cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, (eliot porter:0.6), (frans lanting:0.4), (daniel kordan:0.6), landscapephotography, ultra detailed, earth tones, moody", - "API_NPrompt": "FastNegativeV2, (easynegative:0.5), canvas frame, 3d, ((bad art)), illustrated, deformed, blurry, duplicate, Photoshop, video game, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, (Thomas Kinkade:0.5), sentimental, kitsch, kitschy, twee, commercial, holiday card, modern, futuristic, urban, comic, cartoon, FastNegativeV2, epiCNegative, easynegative, verybadimagenegative_v1.3", + "API_SPrompt": ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody", + "API_NPrompt": "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon", "llm_sys_msg": "You are a helpful AI who assists in generating prompts that will be used to generate highly realistic images. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", "llm_pre_prompt": "Using a series of words or sentence fragments separated by commas, describe a professional landscape photograph of a striking scene of nature. You can select any place on Earth that a young model from the Pacific Northwest is likely to travel to. Focus on describing the content and composition of the image. Only use words and phrases that are visually descriptive. This model is especially fond of wild and rugged places, mountains. She favors dark muted earth tones, dramatic lighting, and interesting juxtapositions between foreground and background, or center of frame and outer frame areas. Avoid cliche situations; instread strive for nuance and originality in composition and environment.", "workflows": [{"workflow": "wallpaper.json", "size": "1024x640"}] @@ -29,7 +29,7 @@ ], "API_PPrompt": "Highly-detailed portrait photo of ", "API_SPrompt": "; attractive, cute, (((masterpiece))); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "canvas frame, 3d, ((bad art)), illustrated, deformed, blurry, duplicate, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, nude", + "API_NPrompt": "canvas frame, 3d, bad art, illustrated, deformed, blurry, duplicate, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, easynegative, epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, nude", "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic portrait photos. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on the pictured individual's eyes, pose, and other distinctive features. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words. Remember that the final product will be a still image, and action verbs are not as helpful as simple descriptions of position, appearance, background, etc.", "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: ", "workflows": [ @@ -38,55 +38,6 @@ "size": "768x1024" } ] - }, - { - "scene": "doggystyle", - "triggers": [ - "doggystyle", - "doggy-style", - "doggy style", - "from behind" - ], - "API_PPrompt": "Explicit highly-detailed image of ", - "API_SPrompt": "; ((from behind)), (((doggystyle))), explicit, ((tiny breasts)), flat chest, (((young nude girl))), cute, (covered in cum), sex, porn, nsfw, (((masterpiece))); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "canvas frame, 3d, ((bad art)), ((asian)), illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, censored, pg13", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic erotic/pornographic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on 18+ details concerning body parts, position, etc. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words. Remember that the final product will be a still image, and action verbs are not as helpful as simple descriptions of position, appearance, body parts and fluids, etc.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this pornographic scene description of doggystyle sex to its erotic essence, staying true to what it describes no matter how kinky or taboo: ", - "workflows": [ - { - "workflow": "xxx/doggy.json", - "size": "1024x768", - "preset_values": { - "node": "133", - "type": "depth_map", - "key": "image", - "values": [ - "xxx/doggy/DOGGY_001.png", - "xxx/doggy/DOGGY_002.png", - "xxx/doggy/DOGGY_003.png" - ] - } - } - ] - }, - { - "scene": "nsfw", - "triggers": [ - "nude", - "naked", - "undressed" - ], - "API_PPrompt": "Explicit highly-detailed image of ", - "API_SPrompt": "; ((tiny breasts)), flat chest, (((young nude girl))), cute, nsfw, (((masterpiece))); ((beautiful lighting), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "canvas frame, 3d, ((bad art)), ((asian)), illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, censored, pg13", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic erotic art. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on details concerning body parts, position, etc. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this image of a young girl or woman to its erotic essence: ", - "workflows": [ - { - "workflow": "nude.json", - "size": "768x1024" - } - ] } ] } diff --git a/sijapi/config/sd.yaml-example b/sijapi/config/sd.yaml-example new file mode 100644 index 0000000..3ee12fd --- /dev/null +++ b/sijapi/config/sd.yaml-example @@ -0,0 +1,38 @@ +scenes: + - scene: default + triggers: + - "" + API_PPrompt: "Highly-detailed image of " + API_SPrompt: ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody" + API_NPrompt: "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon" + llm_sys_msg: "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words." + llm_pre_prompt: "Using the most visually descriptive sentence fragments, phrases, and words, distill this scene description to its essence, staying true to what it describes: " + workflows: + - workflow: default.json + size: 1024x768 + + - scene: wallpaper + triggers: + - wallpaper + API_PPrompt: "Stunning widescreen image of " + API_SPrompt: ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody" + API_NPrompt: "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon" + llm_sys_msg: "You are a helpful AI who assists in generating prompts that will be used to generate highly realistic images. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words." + llm_pre_prompt: "Using a series of words or sentence fragments separated by commas, describe a professional landscape photograph of a striking scene of nature. You can select any place on Earth that a young model from the Pacific Northwest is likely to travel to. Focus on describing the content and composition of the image. Only use words and phrases that are visually descriptive. This model is especially fond of wild and rugged places, mountains. She favors dark muted earth tones, dramatic lighting, and interesting juxtapositions between foreground and background, or center of frame and outer frame areas. Avoid cliche situations; instread strive for nuance and originality in composition and environment." + workflows: + - workflow: wallpaper.json + size: 1024x640 + + - scene: portrait + triggers: + - portrait + - profile + - headshot + API_PPrompt: "Highly-detailed portrait photo of " + API_SPrompt: "; attractive, cute, (((masterpiece))); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed." + API_NPrompt: "canvas frame, 3d, bad art, illustrated, deformed, blurry, duplicate, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, easynegative, epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, nude" + llm_sys_msg: "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic portrait photos. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on the pictured individual's eyes, pose, and other distinctive features. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words. Remember that the final product will be a still image, and action verbs are not as helpful as simple descriptions of position, appearance, background, etc." + llm_pre_prompt: "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: " + workflows: + - workflow: selfie.json + size: 768x1024 \ No newline at end of file diff --git a/sijapi/data/loc_overrides.json b/sijapi/data/loc_overrides.json deleted file mode 100644 index f006591..0000000 --- a/sijapi/data/loc_overrides.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "name": "Echo Valley Ranch", - "latitude": 42.8098216, - "longitude": -123.049396, - "radius": 1.5 - } -] \ No newline at end of file diff --git a/sijapi/data/sd/workflows/base.json b/sijapi/data/sd/workflows/base.json deleted file mode 100755 index f231c8c..0000000 --- a/sijapi/data/sd/workflows/base.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "4": { - "inputs": { - "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" - }, - "class_type": "CheckpointLoaderSimple", - "_meta": { - "title": "Load Checkpoint" - } - }, - "6": { - "inputs": { - "text": "API_PPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "7": { - "inputs": { - "text": "API_NPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "8": { - "inputs": { - "samples": [ - "16", - 0 - ], - "vae": [ - "4", - 2 - ] - }, - "class_type": "VAEDecode", - "_meta": { - "title": "VAE Decode" - } - }, - "9": { - "inputs": { - "filename_prefix": "API_", - "images": [ - "8", - 0 - ] - }, - "class_type": "SaveImage", - "_meta": { - "title": "Save Image" - } - }, - "10": { - "inputs": { - "text": "API_SPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "14": { - "inputs": { - "conditioning_1": [ - "6", - 0 - ], - "conditioning_2": [ - "10", - 0 - ] - }, - "class_type": "ConditioningCombine", - "_meta": { - "title": "Conditioning (Combine)" - } - }, - "15": { - "inputs": { - "batch_size": 1, - "width": 1023, - "height": 1025, - "resampling": "nearest-exact", - "X": 0, - "Y": 0, - "Z": 0, - "evolution": 0, - "frame": 0, - "scale": 5, - "octaves": 8, - "persistence": 1.5, - "lacunarity": 2, - "exponent": 4, - "brightness": 0, - "contrast": 0, - "clamp_min": 0, - "clamp_max": 1, - "seed": 648867523029843, - "device": "cpu", - "optional_vae": [ - "4", - 2 - ], - "ppf_settings": [ - "17", - 0 - ] - }, - "class_type": "Perlin Power Fractal Latent (PPF Noise)", - "_meta": { - "title": "Perlin Power Fractal Noise 🦚" - } - }, - "16": { - "inputs": { - "seed": 863091325074880, - "steps": 10, - "cfg": 8, - "sampler_name": "dpmpp_2m_sde", - "scheduler": "karras", - "start_at_step": 0, - "end_at_step": 10000, - "enable_denoise": "false", - "denoise": 1, - "add_noise": "enable", - "return_with_leftover_noise": "disable", - "noise_type": "brownian_fractal", - "noise_blending": "cuberp", - "noise_mode": "additive", - "scale": 1, - "alpha_exponent": 1, - "modulator": 1, - "sigma_tolerance": 0.5, - "boost_leading_sigma": "false", - "guide_use_noise": "true", - "model": [ - "4", - 0 - ], - "positive": [ - "14", - 0 - ], - "negative": [ - "7", - 0 - ], - "latent_image": [ - "15", - 0 - ], - "ppf_settings": [ - "17", - 0 - ], - "ch_settings": [ - "18", - 0 - ] - }, - "class_type": "Power KSampler Advanced (PPF Noise)", - "_meta": { - "title": "Power KSampler Advanced 🦚" - } - }, - "17": { - "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "evolution": 0, - "frame": 0, - "scale": 5, - "octaves": 8, - "persistence": 1.5, - "lacunarity": 2, - "exponent": 4, - "brightness": 0, - "contrast": 0 - }, - "class_type": "Perlin Power Fractal Settings (PPF Noise)", - "_meta": { - "title": "Perlin Power Fractal Settings 🦚" - } - }, - "18": { - "inputs": { - "frequency": 320, - "octaves": 12, - "persistence": 1.5, - "num_colors": 16, - "color_tolerance": 0.05, - "angle_degrees": 45, - "brightness": 0, - "contrast": 0, - "blur": 2.5 - }, - "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", - "_meta": { - "title": "Cross-Hatch Power Fractal Settings 🦚" - } - } -} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/default.json b/sijapi/data/sd/workflows/default.json old mode 100755 new mode 100644 index 9fc0705..741bd75 --- a/sijapi/data/sd/workflows/default.json +++ b/sijapi/data/sd/workflows/default.json @@ -1,298 +1,281 @@ { - "10": { - "_meta": { - "title": "Power KSampler Advanced 🦚" - }, - "class_type": "Power KSampler Advanced (PPF Noise)", + "4": { "inputs": { - "add_noise": "enable", - "alpha_exponent": 1, - "boost_leading_sigma": "false", - "cfg": 4.5, - "ch_settings": [ - "12", + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": "API_PPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "API_NPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "9": { + "inputs": { + "filename_prefix": "API_", + "images": [ + "27", 0 - ], - "denoise": 1, - "enable_denoise": "false", + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "11": { + "inputs": { + "batch_size": 1, + "width": 1023, + "height": 1025, + "resampling": "bicubic", + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0.1, + "frame": 1, + "scale": 13.1, + "octaves": 8, + "persistence": 6.2, + "lacunarity": 5.38, + "exponent": 4.5600000000000005, + "brightness": -0.16, + "contrast": -0.13, + "clamp_min": 0, + "clamp_max": 1, + "seed": 474669046020372, + "device": "cpu", + "optional_vae": [ + "4", + 2 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + } + }, + "13": { + "inputs": { + "seed": 484066073734968, + "steps": 10, + "cfg": 1.8, + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "start_at_step": 0, "end_at_step": 10000, + "enable_denoise": "false", + "denoise": 1, + "add_noise": "enable", + "return_with_leftover_noise": "disable", + "noise_type": "brownian_fractal", + "noise_blending": "cuberp", + "noise_mode": "additive", + "scale": 1, + "alpha_exponent": 1, + "modulator": 1, + "sigma_tolerance": 0.5, + "boost_leading_sigma": "false", "guide_use_noise": "true", - "latent_image": [ - "13", - 0 - ], "model": [ "4", 0 ], - "modulator": 1, + "positive": [ + "20", + 0 + ], "negative": [ "7", 0 ], - "noise_blending": "hslerp", - "noise_mode": "additive", - "noise_type": "vanilla_comfy", - "positive": [ + "latent_image": [ + "11", + 0 + ], + "ppf_settings": [ + "14", + 0 + ], + "ch_settings": [ + "15", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" + } + }, + "14": { + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 5, + "persistence": 4, + "lacunarity": 5, + "exponent": 4.28, + "brightness": -0.3, + "contrast": -0.2 + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + } + }, + "15": { + "inputs": { + "frequency": 332.65500000000003, + "octaves": 32, + "persistence": 1.616, + "num_colors": 256, + "color_tolerance": 0.05, + "angle_degrees": 180, + "brightness": -0.5, + "contrast": -0.05, + "blur": 1.3 + }, + "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" + } + }, + "20": { + "inputs": { + "conditioning_1": [ "6", 0 ], - "ppf_settings": [ - "11", + "conditioning_2": [ + "21", 0 - ], - "return_with_leftover_noise": "disable", - "sampler_name": "dpmpp_2m_sde", - "scale": 1, - "scheduler": "karras", - "seed": 301923985151711, - "sigma_tolerance": 0.5, - "start_at_step": 0, - "steps": 20 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" } }, - "11": { - "_meta": { - "title": "Perlin Power Fractal Settings 🦚" - }, - "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "21": { "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "brightness": 0, - "contrast": 0, - "evolution": 0, - "exponent": 4, - "frame": 0, - "lacunarity": 2, - "octaves": 8, - "persistence": 1.5, - "scale": 5 - } - }, - "12": { - "_meta": { - "title": "Cross-Hatch Power Fractal Settings 🦚" - }, - "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", - "inputs": { - "angle_degrees": 45, - "blur": 2.5, - "brightness": 0, - "color_tolerance": 0.05, - "contrast": 0, - "frequency": 320, - "num_colors": 16, - "octaves": 12, - "persistence": 1.5 - } - }, - "13": { - "_meta": { - "title": "Perlin Power Fractal Noise 🦚" - }, - "class_type": "Perlin Power Fractal Latent (PPF Noise)", - "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "batch_size": 1, - "brightness": 0, - "clamp_max": 1, - "clamp_min": 0, - "contrast": 0, - "device": "cpu", - "evolution": 0, - "exponent": 4, - "frame": 0, - "height": 1025, - "lacunarity": 2.5, - "octaves": 8, - "optional_vae": [ + "text": "API_SPrompt", + "clip": [ "4", - 2 - ], - "persistence": 1.5, - "ppf_settings": [ - "11", - 0 - ], - "resampling": "nearest-exact", - "scale": 5, - "seed": 961984691493347, - "width": 1023 + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" } }, "23": { - "_meta": { - "title": "Ultimate SD Upscale" - }, - "class_type": "UltimateSDUpscale", "inputs": { - "cfg": 7.5, - "denoise": 0.32, - "force_uniform_tiles": true, - "image": [ - "8", + "conditioning": [ + "7", 0 - ], - "mask_blur": 8, - "mode_type": "Chess", - "model": [ - "24", - 0 - ], - "negative": [ - "32", - 0 - ], - "positive": [ - "31", - 0 - ], - "sampler_name": "dpmpp_2m_sde", - "scheduler": "karras", - "seam_fix_denoise": 1, - "seam_fix_mask_blur": 8, - "seam_fix_mode": "Band Pass", - "seam_fix_padding": 16, - "seam_fix_width": 64, - "seed": 221465882658451, - "steps": 16, - "tile_height": 768, - "tile_padding": 32, - "tile_width": 768, - "tiled_decode": false, - "upscale_by": 4, - "upscale_model": [ - "33", - 0 - ], - "vae": [ - "24", - 2 ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" } }, "24": { - "_meta": { - "title": "Load Checkpoint" - }, - "class_type": "CheckpointLoaderSimple", "inputs": { - "ckpt_name": "SD1.5/realisticVisionV60B1_v51VAE.safetensors" - } - }, - "31": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "24", - 1 - ], - "text": "" - } - }, - "32": { - "_meta": { - "title": "ConditioningZeroOut" - }, - "class_type": "ConditioningZeroOut", - "inputs": { - "conditioning": [ - "31", - 0 - ] - } - }, - "33": { - "_meta": { - "title": "Load Upscale Model" + "model_name": "RealESRGAN_x4plus.pth" }, "class_type": "UpscaleModelLoader", - "inputs": { - "model_name": "4x-UltraSharp.pth" + "_meta": { + "title": "Load Upscale Model" } }, - "34": { - "_meta": { - "title": "Save Image" - }, - "class_type": "SaveImage", + "26": { "inputs": { - "filename_prefix": "API_", - "images": [ - "23", + "upscale_model": [ + "24", + 0 + ], + "image": [ + "38", 0 ] + }, + "class_type": "ImageUpscaleWithModel", + "_meta": { + "title": "Upscale Image (using Model)" } }, - "36": { - "_meta": { - "title": "Save Image" - }, - "class_type": "SaveImage", + "27": { "inputs": { - "filename_prefix": "Pre_", - "images": [ - "8", + "factor": 0.5, + "interpolation_mode": "bicubic", + "image": [ + "30", 0 ] + }, + "class_type": "JWImageResizeByFactor", + "_meta": { + "title": "Image Resize by Factor" } }, - "4": { - "_meta": { - "title": "Load Checkpoint" - }, - "class_type": "CheckpointLoaderSimple", + "30": { "inputs": { - "ckpt_name": "Other/playgroundv2.safetensors" + "blur_radius": 3, + "sigma": 1.5, + "image": [ + "26", + 0 + ] + }, + "class_type": "ImageBlur", + "_meta": { + "title": "ImageBlur" } }, - "6": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "API_PPrompt" - } - }, - "7": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "API_NPrompt" - } - }, - "8": { - "_meta": { - "title": "VAE Decode" - }, - "class_type": "VAEDecode", + "38": { "inputs": { "samples": [ - "10", + "13", 0 ], "vae": [ "4", 2 ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" } } } \ No newline at end of file diff --git a/sijapi/data/sd/workflows/landscape.json b/sijapi/data/sd/workflows/landscape.json old mode 100755 new mode 100644 index 8a6223a..e541bd6 --- a/sijapi/data/sd/workflows/landscape.json +++ b/sijapi/data/sd/workflows/landscape.json @@ -1,456 +1,347 @@ { - "11": { + "4": { + "inputs": { + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + }, + "class_type": "CheckpointLoaderSimple", "_meta": { - "title": "CLIP Text Encode (Prompt)" + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": "API_PPrompt", + "clip": [ + "4", + 1 + ] }, "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "12", - 1 - ], - "text": [ - "25", - 0 - ] + "_meta": { + "title": "CLIP Text Encode (Prompt)" } }, - "12": { - "_meta": { - "title": "Load LoRA" - }, - "class_type": "LoraLoader", + "7": { "inputs": { + "text": "API_NPrompt", "clip": [ "4", 1 - ], - "lora_name": "SDXL/add-detail-xl.safetensors", - "model": [ - "4", - 0 - ], - "strength_clip": 0.3, - "strength_model": 0.33 - } - }, - "13": { - "_meta": { - "title": "Load LoRA" - }, - "class_type": "LoraLoader", - "inputs": { - "clip": [ - "12", - 1 - ], - "lora_name": "SDXL/SDXLLandskaper_v1-000003.safetensors", - "model": [ - "12", - 0 - ], - "strength_clip": 0.75, - "strength_model": 0.8 - } - }, - "14": { - "_meta": { - "title": "Power KSampler Advanced 🦚" - }, - "class_type": "Power KSampler Advanced (PPF Noise)", - "inputs": { - "add_noise": "enable", - "alpha_exponent": 1, - "boost_leading_sigma": "false", - "cfg": 8, - "ch_settings": [ - "19", - 0 - ], - "denoise": 1, - "enable_denoise": "false", - "end_at_step": 10000, - "guide_use_noise": "true", - "latent_image": [ - "20", - 0 - ], - "model": [ - "13", - 0 - ], - "modulator": 1, - "negative": [ - "61", - 0 - ], - "noise_blending": "cuberp", - "noise_mode": "additive", - "noise_type": "brownian_fractal", - "positive": [ - "63", - 0 - ], - "ppf_settings": [ - "18", - 0 - ], - "return_with_leftover_noise": "disable", - "sampler_name": "dpmpp_2m_sde", - "scale": 1, - "scheduler": "karras", - "seed": 809193506471910, - "sigma_tolerance": 0.5, - "start_at_step": 0, - "steps": 28 - } - }, - "18": { - "_meta": { - "title": "Perlin Power Fractal Settings 🦚" - }, - "class_type": "Perlin Power Fractal Settings (PPF Noise)", - "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "brightness": 0, - "contrast": 0, - "evolution": 0.2, - "exponent": 5, - "frame": 40, - "lacunarity": 2.4, - "octaves": 8, - "persistence": 1.6, - "scale": 8 - } - }, - "19": { - "_meta": { - "title": "Cross-Hatch Power Fractal Settings 🦚" - }, - "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", - "inputs": { - "angle_degrees": 45, - "blur": 2.5, - "brightness": 0, - "color_tolerance": 0.05, - "contrast": 0, - "frequency": 320, - "num_colors": 32, - "octaves": 24, - "persistence": 1.5 - } - }, - "20": { - "_meta": { - "title": "Perlin Power Fractal Noise 🦚" - }, - "class_type": "Perlin Power Fractal Latent (PPF Noise)", - "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "batch_size": 1, - "brightness": 0, - "clamp_max": 1, - "clamp_min": 0, - "contrast": 0, - "device": "cpu", - "evolution": 0.2, - "exponent": 4, - "frame": 40, - "height": [ - "54", - 1 - ], - "lacunarity": 2.4, - "octaves": 8, - "optional_vae": [ - "4", - 2 - ], - "persistence": 1.6, - "ppf_settings": [ - "18", - 0 - ], - "resampling": "nearest-exact", - "scale": 8, - "seed": 189685705390202, - "width": [ - "54", - 0 ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" } }, - "21": { - "_meta": { - "title": "Conditioning (Combine)" - }, - "class_type": "ConditioningCombine", - "inputs": { - "conditioning_1": [ - "11", - 0 - ], - "conditioning_2": [ - "6", - 0 - ] - } - }, - "23": { - "_meta": { - "title": "String (Multiline)" - }, - "class_type": "JWStringMultiline", - "inputs": { - "text": "API_SPrompt" - } - }, - "24": { - "_meta": { - "title": "String (Multiline)" - }, - "class_type": "JWStringMultiline", - "inputs": { - "text": "API_NPrompt" - } - }, - "25": { - "_meta": { - "title": "String (Multiline)" - }, - "class_type": "JWStringMultiline", - "inputs": { - "text": "API_PPrompt" - } - }, - "28": { - "_meta": { - "title": "Tiled VAE Decode" - }, - "class_type": "VAEDecodeTiled_TiledDiffusion", - "inputs": { - "fast": false, - "samples": [ - "14", - 0 - ], - "tile_size": [ - "53", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "36": { - "_meta": { - "title": "Save Image" - }, - "class_type": "SaveImage", + "9": { "inputs": { "filename_prefix": "API_", "images": [ - "52", + "27", 0 ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" } }, - "4": { - "_meta": { - "title": "Load Checkpoint" - }, - "class_type": "CheckpointLoaderSimple", + "11": { "inputs": { - "ckpt_name": "SDXL/realismEngineSDXL_v20VAE.safetensors" + "batch_size": 1, + "width": 1023, + "height": 1025, + "resampling": "bicubic", + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0.1, + "frame": 1, + "scale": 13.1, + "octaves": 8, + "persistence": 6.2, + "lacunarity": 5.38, + "exponent": 4.5600000000000005, + "brightness": -0.16, + "contrast": -0.13, + "clamp_min": 0, + "clamp_max": 1, + "seed": 474669046020372, + "device": "cpu", + "optional_vae": [ + "4", + 2 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" } }, - "42": { - "_meta": { - "title": "Upscale Model Loader" - }, - "class_type": "Upscale Model Loader", + "13": { "inputs": { - "model_name": "RealESRGAN_x2plus.pth" - } - }, - "52": { - "_meta": { - "title": "Ultimate SD Upscale" - }, - "class_type": "UltimateSDUpscale", - "inputs": { - "cfg": 8, - "denoise": 0.24, - "force_uniform_tiles": true, - "image": [ - "28", + "seed": 484066073734968, + "steps": 8, + "cfg": 1.8, + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "start_at_step": 0, + "end_at_step": 10000, + "enable_denoise": "false", + "denoise": 1, + "add_noise": "enable", + "return_with_leftover_noise": "enable", + "noise_type": "brownian_fractal", + "noise_blending": "cuberp", + "noise_mode": "additive", + "scale": 1, + "alpha_exponent": 1, + "modulator": 1, + "sigma_tolerance": 0.5, + "boost_leading_sigma": "false", + "guide_use_noise": "true", + "model": [ + "4", 0 ], - "mask_blur": 8, - "mode_type": "Linear", - "model": [ - "12", + "positive": [ + "20", 0 ], "negative": [ "7", 0 ], - "positive": [ + "latent_image": [ + "11", + 0 + ], + "ppf_settings": [ + "14", + 0 + ], + "ch_settings": [ + "15", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" + } + }, + "14": { + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 5, + "persistence": 4, + "lacunarity": 5, + "exponent": 4.28, + "brightness": -0.3, + "contrast": -0.2 + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + } + }, + "15": { + "inputs": { + "frequency": 332.65500000000003, + "octaves": 32, + "persistence": 1.616, + "num_colors": 256, + "color_tolerance": 0.05, + "angle_degrees": 180, + "brightness": -0.5, + "contrast": -0.05, + "blur": 1.3 + }, + "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" + } + }, + "20": { + "inputs": { + "conditioning_1": [ + "6", + 0 + ], + "conditioning_2": [ "21", 0 - ], - "sampler_name": "dpmpp_2m_sde", - "scheduler": "karras", - "seam_fix_denoise": 1, - "seam_fix_mask_blur": 8, - "seam_fix_mode": "None", - "seam_fix_padding": 16, - "seam_fix_width": 64, - "seed": 1041855229054013, - "steps": 16, - "tile_height": [ - "53", - 0 - ], - "tile_padding": 32, - "tile_width": [ - "53", - 0 - ], - "tiled_decode": true, + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + }, + "21": { + "inputs": { + "text": "API_SPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "22": { + "inputs": { "upscale_by": 2, + "seed": 589846903558615, + "steps": 20, + "cfg": 1.6, + "sampler_name": "heun", + "scheduler": "sgm_uniform", + "denoise": 0.21, + "mode_type": "Linear", + "tile_width": 512, + "tile_height": 512, + "mask_blur": 8, + "tile_padding": 32, + "seam_fix_mode": "Band Pass", + "seam_fix_denoise": 1, + "seam_fix_width": 64, + "seam_fix_mask_blur": 8, + "seam_fix_padding": 16, + "force_uniform_tiles": true, + "tiled_decode": true, + "image": [ + "38", + 0 + ], + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "23", + 0 + ], + "vae": [ + "4", + 2 + ], "upscale_model": [ - "42", + "24", + 0 + ] + }, + "class_type": "UltimateSDUpscale", + "_meta": { + "title": "Ultimate SD Upscale" + } + }, + "23": { + "inputs": { + "conditioning": [ + "7", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "24": { + "inputs": { + "model_name": "RealESRGAN_x4plus.pth" + }, + "class_type": "UpscaleModelLoader", + "_meta": { + "title": "Load Upscale Model" + } + }, + "26": { + "inputs": { + "upscale_model": [ + "24", + 0 + ], + "image": [ + "22", + 0 + ] + }, + "class_type": "ImageUpscaleWithModel", + "_meta": { + "title": "Upscale Image (using Model)" + } + }, + "27": { + "inputs": { + "factor": 0.5, + "interpolation_mode": "bicubic", + "image": [ + "30", + 0 + ] + }, + "class_type": "JWImageResizeByFactor", + "_meta": { + "title": "Image Resize by Factor" + } + }, + "30": { + "inputs": { + "blur_radius": 3, + "sigma": 1.5, + "image": [ + "26", + 0 + ] + }, + "class_type": "ImageBlur", + "_meta": { + "title": "ImageBlur" + } + }, + "36": { + "inputs": { + "mode": "bicubic", + "factor": 1.25, + "align": "true", + "samples": [ + "13", + 0 + ] + }, + "class_type": "Latent Upscale by Factor (WAS)", + "_meta": { + "title": "Latent Upscale by Factor (WAS)" + } + }, + "38": { + "inputs": { + "samples": [ + "13", 0 ], "vae": [ "4", 2 ] - } - }, - "53": { - "_meta": { - "title": "Integer" }, - "class_type": "JWInteger", - "inputs": { - "value": 768 - } - }, - "54": { + "class_type": "VAEDecode", "_meta": { - "title": "AnyAspectRatio" - }, - "class_type": "AnyAspectRatio", - "inputs": { - "height_ratio": 3, - "rounding_value": 32, - "side_length": 1023, - "width_ratio": 4 - } - }, - "6": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "12", - 1 - ], - "text": [ - "23", - 0 - ] - } - }, - "60": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "13", - 1 - ], - "text": [ - "23", - 0 - ] - } - }, - "61": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "13", - 1 - ], - "text": [ - "24", - 0 - ] - } - }, - "62": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "13", - 1 - ], - "text": [ - "25", - 0 - ] - } - }, - "63": { - "_meta": { - "title": "Conditioning (Combine)" - }, - "class_type": "ConditioningCombine", - "inputs": { - "conditioning_1": [ - "62", - 0 - ], - "conditioning_2": [ - "60", - 0 - ] - } - }, - "7": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "12", - 1 - ], - "text": [ - "24", - 0 - ] + "title": "VAE Decode" } } } \ No newline at end of file diff --git a/sijapi/data/sd/workflows/turbo.json b/sijapi/data/sd/workflows/turbo.json deleted file mode 100755 index 8369894..0000000 --- a/sijapi/data/sd/workflows/turbo.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "4": { - "inputs": { - "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" - }, - "class_type": "CheckpointLoaderSimple", - "_meta": { - "title": "Load Checkpoint" - } - }, - "6": { - "inputs": { - "text": "API_PPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "7": { - "inputs": { - "text": "API_NPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - }, - "8": { - "inputs": { - "samples": [ - "13", - 0 - ], - "vae": [ - "4", - 2 - ] - }, - "class_type": "VAEDecode", - "_meta": { - "title": "VAE Decode" - } - }, - "9": { - "inputs": { - "filename_prefix": "API_", - "images": [ - "8", - 0 - ] - }, - "class_type": "SaveImage", - "_meta": { - "title": "Save Image" - } - }, - "11": { - "inputs": { - "batch_size": 1, - "width": 1023, - "height": 1025, - "resampling": "nearest-exact", - "X": 0, - "Y": 0, - "Z": 0, - "evolution": 0, - "frame": 0, - "scale": 5, - "octaves": 8, - "persistence": 1.5, - "lacunarity": 2, - "exponent": 4, - "brightness": 0, - "contrast": 0, - "clamp_min": 0, - "clamp_max": 1, - "seed": 704513836266662, - "device": "cpu", - "optional_vae": [ - "4", - 2 - ], - "ppf_settings": [ - "14", - 0 - ] - }, - "class_type": "Perlin Power Fractal Latent (PPF Noise)", - "_meta": { - "title": "Perlin Power Fractal Noise 🦚" - } - }, - "13": { - "inputs": { - "seed": 525862638063448, - "steps": 8, - "cfg": 1.6, - "sampler_name": "dpmpp_2m_sde", - "scheduler": "karras", - "start_at_step": 0, - "end_at_step": 10000, - "enable_denoise": "false", - "denoise": 1, - "add_noise": "enable", - "return_with_leftover_noise": "disable", - "noise_type": "brownian_fractal", - "noise_blending": "cuberp", - "noise_mode": "additive", - "scale": 1, - "alpha_exponent": 1, - "modulator": 1, - "sigma_tolerance": 0.5, - "boost_leading_sigma": "false", - "guide_use_noise": "true", - "model": [ - "4", - 0 - ], - "positive": [ - "20", - 0 - ], - "negative": [ - "7", - 0 - ], - "latent_image": [ - "11", - 0 - ], - "ppf_settings": [ - "14", - 0 - ], - "ch_settings": [ - "15", - 0 - ] - }, - "class_type": "Power KSampler Advanced (PPF Noise)", - "_meta": { - "title": "Power KSampler Advanced 🦚" - } - }, - "14": { - "inputs": { - "X": 0, - "Y": 0, - "Z": 0, - "evolution": 0, - "frame": 0, - "scale": 5, - "octaves": 8, - "persistence": 1.5, - "lacunarity": 2, - "exponent": 4, - "brightness": 0, - "contrast": 0 - }, - "class_type": "Perlin Power Fractal Settings (PPF Noise)", - "_meta": { - "title": "Perlin Power Fractal Settings 🦚" - } - }, - "15": { - "inputs": { - "frequency": 320, - "octaves": 12, - "persistence": 1.5, - "num_colors": 16, - "color_tolerance": 0.05, - "angle_degrees": 45, - "brightness": 0, - "contrast": 0, - "blur": 2.5 - }, - "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", - "_meta": { - "title": "Cross-Hatch Power Fractal Settings 🦚" - } - }, - "20": { - "inputs": { - "conditioning_1": [ - "6", - 0 - ], - "conditioning_2": [ - "21", - 0 - ] - }, - "class_type": "ConditioningCombine", - "_meta": { - "title": "Conditioning (Combine)" - } - }, - "21": { - "inputs": { - "text": "API_SPrompt", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP Text Encode (Prompt)" - } - } -} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/wallpaper.json b/sijapi/data/sd/workflows/wallpaper.json index ed4732a..e541bd6 100644 --- a/sijapi/data/sd/workflows/wallpaper.json +++ b/sijapi/data/sd/workflows/wallpaper.json @@ -1,130 +1,174 @@ { - "11": { - "_meta": { - "title": "Perlin Power Fractal Noise 🦚" - }, - "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "4": { "inputs": { + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": "API_PPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "API_NPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "9": { + "inputs": { + "filename_prefix": "API_", + "images": [ + "27", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "11": { + "inputs": { + "batch_size": 1, + "width": 1023, + "height": 1025, + "resampling": "bicubic", "X": 0, "Y": 0, "Z": 0, - "batch_size": 1, - "brightness": 0, - "clamp_max": 1, - "clamp_min": 0, - "contrast": 0, - "device": "cpu", - "evolution": 0, - "exponent": 4, - "frame": 0, - "height": 1025, - "lacunarity": 2, + "evolution": 0.1, + "frame": 1, + "scale": 13.1, "octaves": 8, + "persistence": 6.2, + "lacunarity": 5.38, + "exponent": 4.5600000000000005, + "brightness": -0.16, + "contrast": -0.13, + "clamp_min": 0, + "clamp_max": 1, + "seed": 474669046020372, + "device": "cpu", "optional_vae": [ "4", 2 - ], - "persistence": 1.5, - "resampling": "nearest-exact", - "scale": 5, - "seed": 490162938389882, - "width": 1023 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" } }, "13": { - "_meta": { - "title": "Power KSampler Advanced 🦚" - }, - "class_type": "Power KSampler Advanced (PPF Noise)", "inputs": { - "add_noise": "enable", - "alpha_exponent": 1, - "boost_leading_sigma": "false", - "cfg": 1.6, - "ch_settings": [ - "15", - 0 - ], - "denoise": 1, - "enable_denoise": "false", + "seed": 484066073734968, + "steps": 8, + "cfg": 1.8, + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "start_at_step": 0, "end_at_step": 10000, + "enable_denoise": "false", + "denoise": 1, + "add_noise": "enable", + "return_with_leftover_noise": "enable", + "noise_type": "brownian_fractal", + "noise_blending": "cuberp", + "noise_mode": "additive", + "scale": 1, + "alpha_exponent": 1, + "modulator": 1, + "sigma_tolerance": 0.5, + "boost_leading_sigma": "false", "guide_use_noise": "true", - "latent_image": [ - "11", - 0 - ], "model": [ "4", 0 ], - "modulator": 1, + "positive": [ + "20", + 0 + ], "negative": [ "7", 0 ], - "noise_blending": "cuberp", - "noise_mode": "additive", - "noise_type": "brownian_fractal", - "positive": [ - "20", + "latent_image": [ + "11", 0 ], "ppf_settings": [ "14", 0 ], - "return_with_leftover_noise": "disable", - "sampler_name": "dpmpp_2m_sde", - "scale": 1, - "scheduler": "karras", - "seed": 697312143874418, - "sigma_tolerance": 0.5, - "start_at_step": 0, - "steps": 8 + "ch_settings": [ + "15", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" } }, "14": { - "_meta": { - "title": "Perlin Power Fractal Settings 🦚" - }, - "class_type": "Perlin Power Fractal Settings (PPF Noise)", "inputs": { "X": 0, "Y": 0, "Z": 0, - "brightness": 0, - "contrast": 0, "evolution": 0, - "exponent": 4, "frame": 0, - "lacunarity": 2, - "octaves": 8, - "persistence": 1.5, - "scale": 5 + "scale": 5, + "octaves": 5, + "persistence": 4, + "lacunarity": 5, + "exponent": 4.28, + "brightness": -0.3, + "contrast": -0.2 + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" } }, "15": { - "_meta": { - "title": "Cross-Hatch Power Fractal Settings 🦚" + "inputs": { + "frequency": 332.65500000000003, + "octaves": 32, + "persistence": 1.616, + "num_colors": 256, + "color_tolerance": 0.05, + "angle_degrees": 180, + "brightness": -0.5, + "contrast": -0.05, + "blur": 1.3 }, "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", - "inputs": { - "angle_degrees": 45, - "blur": 2.5, - "brightness": 0, - "color_tolerance": 0.05, - "contrast": 0, - "frequency": 320, - "num_colors": 16, - "octaves": 12, - "persistence": 1.5 + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" } }, "20": { - "_meta": { - "title": "Conditioning (Combine)" - }, - "class_type": "ConditioningCombine", "inputs": { "conditioning_1": [ "6", @@ -134,177 +178,157 @@ "21", 0 ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" } }, "21": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", "inputs": { + "text": "API_SPrompt", "clip": [ "4", 1 - ], - "text": "API_SPrompt" + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" } }, "22": { - "_meta": { - "title": "Ultimate SD Upscale" - }, - "class_type": "UltimateSDUpscale", "inputs": { - "cfg": 8, + "upscale_by": 2, + "seed": 589846903558615, + "steps": 20, + "cfg": 1.6, + "sampler_name": "heun", + "scheduler": "sgm_uniform", "denoise": 0.21, + "mode_type": "Linear", + "tile_width": 512, + "tile_height": 512, + "mask_blur": 8, + "tile_padding": 32, + "seam_fix_mode": "Band Pass", + "seam_fix_denoise": 1, + "seam_fix_width": 64, + "seam_fix_mask_blur": 8, + "seam_fix_padding": 16, "force_uniform_tiles": true, + "tiled_decode": true, "image": [ - "8", + "38", 0 ], - "mask_blur": 8, - "mode_type": "Linear", "model": [ "4", 0 ], - "negative": [ - "23", - 0 - ], "positive": [ "6", 0 ], - "sampler_name": "euler", - "scheduler": "normal", - "seam_fix_denoise": 1, - "seam_fix_mask_blur": 8, - "seam_fix_mode": "None", - "seam_fix_padding": 16, - "seam_fix_width": 64, - "seed": 470914682435746, - "steps": 20, - "tile_height": 512, - "tile_padding": 32, - "tile_width": 512, - "tiled_decode": false, - "upscale_by": 2, - "upscale_model": [ - "24", + "negative": [ + "23", 0 ], "vae": [ "4", 2 - ] - } - }, - "23": { - "_meta": { - "title": "ConditioningZeroOut" - }, - "class_type": "ConditioningZeroOut", - "inputs": { - "conditioning": [ - "7", - 0 - ] - } - }, - "24": { - "_meta": { - "title": "Load Upscale Model" - }, - "class_type": "UpscaleModelLoader", - "inputs": { - "model_name": "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth" - } - }, - "26": { - "_meta": { - "title": "Upscale Image (using Model)" - }, - "class_type": "ImageUpscaleWithModel", - "inputs": { - "image": [ - "22", - 0 ], "upscale_model": [ "24", 0 ] + }, + "class_type": "UltimateSDUpscale", + "_meta": { + "title": "Ultimate SD Upscale" + } + }, + "23": { + "inputs": { + "conditioning": [ + "7", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "24": { + "inputs": { + "model_name": "RealESRGAN_x4plus.pth" + }, + "class_type": "UpscaleModelLoader", + "_meta": { + "title": "Load Upscale Model" + } + }, + "26": { + "inputs": { + "upscale_model": [ + "24", + 0 + ], + "image": [ + "22", + 0 + ] + }, + "class_type": "ImageUpscaleWithModel", + "_meta": { + "title": "Upscale Image (using Model)" } }, "27": { - "_meta": { - "title": "Image Resize by Factor" - }, - "class_type": "JWImageResizeByFactor", "inputs": { "factor": 0.5, + "interpolation_mode": "bicubic", "image": [ "30", 0 - ], - "interpolation_mode": "bicubic" + ] + }, + "class_type": "JWImageResizeByFactor", + "_meta": { + "title": "Image Resize by Factor" } }, "30": { - "_meta": { - "title": "ImageBlur" - }, - "class_type": "ImageBlur", "inputs": { "blur_radius": 3, + "sigma": 1.5, "image": [ "26", 0 - ], - "sigma": 1.5 + ] + }, + "class_type": "ImageBlur", + "_meta": { + "title": "ImageBlur" } }, - "4": { - "_meta": { - "title": "Load Checkpoint" - }, - "class_type": "CheckpointLoaderSimple", + "36": { "inputs": { - "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + "mode": "bicubic", + "factor": 1.25, + "align": "true", + "samples": [ + "13", + 0 + ] + }, + "class_type": "Latent Upscale by Factor (WAS)", + "_meta": { + "title": "Latent Upscale by Factor (WAS)" } }, - "6": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "API_PPrompt" - } - }, - "7": { - "_meta": { - "title": "CLIP Text Encode (Prompt)" - }, - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "API_NPrompt" - } - }, - "8": { - "_meta": { - "title": "VAE Decode" - }, - "class_type": "VAEDecode", + "38": { "inputs": { "samples": [ "13", @@ -314,19 +338,10 @@ "4", 2 ] - } - }, - "9": { - "_meta": { - "title": "Save Image" }, - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "API_", - "images": [ - "27", - 0 - ] + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" } } } \ No newline at end of file diff --git a/sijapi/routers/asr.py b/sijapi/routers/asr.py index bb82eec..f7f157d 100644 --- a/sijapi/routers/asr.py +++ b/sijapi/routers/asr.py @@ -23,7 +23,6 @@ import multiprocessing import asyncio import subprocess import tempfile - from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, ASR_DIR, WHISPER_CPP_MODELS, GARBAGE_COLLECTION_INTERVAL, GARBAGE_TTL, WHISPER_CPP_DIR, MAX_CPU_CORES diff --git a/sijapi/routers/calendar.py b/sijapi/routers/calendar.py index 86bbe0d..d3aac21 100644 --- a/sijapi/routers/calendar.py +++ b/sijapi/routers/calendar.py @@ -17,7 +17,7 @@ from datetime import datetime, timedelta from Foundation import NSDate, NSRunLoop import EventKit as EK from sijapi import ICAL_TOGGLE, ICALENDARS, MS365_TOGGLE, MS365_CLIENT_ID, MS365_SECRET, MS365_AUTHORITY_URL, MS365_SCOPE, MS365_REDIRECT_PATH, MS365_TOKEN_PATH -from sijapi.utilities import localize_datetime, localize_datetime +from sijapi.routers.locate import localize_datetime from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL calendar = APIRouter() @@ -215,8 +215,8 @@ def datetime_to_nsdate(dt: datetime) -> NSDate: @calendar.get("/events") async def get_events_endpoint(start_date: str, end_date: str): - start_dt = localize_datetime(start_date) - end_dt = localize_datetime(end_date) + start_dt = await localize_datetime(start_date) + end_dt = await localize_datetime(end_date) datetime.strptime(start_date, "%Y-%m-%d") or datetime.now() end_dt = datetime.strptime(end_date, "%Y-%m-%d") or datetime.now() response = await get_events(start_dt, end_dt) @@ -342,8 +342,8 @@ async def get_ms365_events(start_date: datetime, end_date: datetime): async def parse_calendar_for_day(range_start: datetime, range_end: datetime, events: List[Dict[str, Any]]): - range_start = localize_datetime(range_start) - range_end = localize_datetime(range_end) + range_start = await localize_datetime(range_start) + range_end = await localize_datetime(range_end) event_list = [] for event in events: @@ -362,13 +362,13 @@ async def parse_calendar_for_day(range_start: datetime, range_end: datetime, eve INFO(f"End date string not a dict") try: - start_date = localize_datetime(start_str) if start_str else None + start_date = await localize_datetime(start_str) if start_str else None except (ValueError, TypeError) as e: ERR(f"Invalid start date format: {start_str}, error: {e}") continue try: - end_date = localize_datetime(end_str) if end_str else None + end_date = await localize_datetime(end_str) if end_str else None except (ValueError, TypeError) as e: ERR(f"Invalid end date format: {end_str}, error: {e}") continue @@ -377,13 +377,13 @@ async def parse_calendar_for_day(range_start: datetime, range_end: datetime, eve if start_date: # Ensure start_date is timezone-aware - start_date = localize_datetime(start_date) + start_date = await localize_datetime(start_date) # If end_date is not provided, assume it's the same as start_date if not end_date: end_date = start_date else: - end_date = localize_datetime(end_date) + end_date = await localize_datetime(end_date) # Check if the event overlaps with the given range if (start_date < range_end) and (end_date > range_start): diff --git a/sijapi/routers/email.py b/sijapi/routers/email.py index d35e0ca..23c8ba1 100644 --- a/sijapi/routers/email.py +++ b/sijapi/routers/email.py @@ -10,29 +10,38 @@ from pathlib import Path from shutil import move import tempfile import re -import ssl -from smtplib import SMTP_SSL +from smtplib import SMTP_SSL, SMTP from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage +import ssl from datetime import datetime as dt_datetime from pydantic import BaseModel from typing import List, Optional, Any import yaml from typing import List, Dict, Optional from pydantic import BaseModel - +from sijapi import DEBUG, ERR, LLM_SYS_MSG +from datetime import datetime as dt_datetime +from typing import Dict from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi import PODCAST_DIR, DEFAULT_VOICE, TZ, EMAIL_ACCOUNTS, EmailAccount, IMAPConfig, SMTPConfig -from sijapi.routers import summarize, tts, llm, sd -from sijapi.utilities import clean_text, assemble_journal_path, localize_datetime, extract_text, prefix_lines -from sijapi.classes import EmailAccount, IncomingEmail, EmailContact +from sijapi import PODCAST_DIR, DEFAULT_VOICE, EMAIL_CONFIG +from sijapi.routers import tts, llm, sd, locate +from sijapi.utilities import clean_text, assemble_journal_path, extract_text, prefix_lines +from sijapi.classes import EmailAccount, IMAPConfig, SMTPConfig, IncomingEmail, EmailContact email = APIRouter(tags=["private"]) +def load_email_accounts(yaml_path: str) -> List[EmailAccount]: + with open(yaml_path, 'r') as file: + config = yaml.safe_load(file) + return [EmailAccount(**account) for account in config['accounts']] + + def get_account_by_email(email: str) -> Optional[EmailAccount]: - for account in EMAIL_ACCOUNTS: + email_accounts = load_email_accounts(EMAIL_CONFIG) + for account in email_accounts: if account.imap.username.lower() == email.lower(): return account return None @@ -54,6 +63,18 @@ def get_imap_connection(account: EmailAccount): ssl=account.imap.encryption == 'SSL', starttls=account.imap.encryption == 'STARTTLS') +def get_smtp_connection(account: EmailAccount): + context = ssl._create_unverified_context() + + if account.smtp.encryption == 'SSL': + return SMTP_SSL(account.smtp.host, account.smtp.port, context=context) + elif account.smtp.encryption == 'STARTTLS': + smtp = SMTP(account.smtp.host, account.smtp.port) + smtp.starttls(context=context) + return smtp + else: + return SMTP(account.smtp.host, account.smtp.port) + def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) -> List[Dict]: matching_profiles = [] @@ -72,7 +93,7 @@ def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) -> 'USER_FULLNAME': account.fullname, 'RESPONSE_STYLE': profile.style, 'AUTORESPONSE_CONTEXT': profile.context, - 'IMG_GEN_PROMPT': profile.img_gen_prompt, + 'IMG_GEN_PROMPT': profile.image_prompt, 'USER_BIO': account.bio }) @@ -80,21 +101,44 @@ def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) -> async def generate_auto_response_body(e: IncomingEmail, profile: Dict) -> str: - age = dt_datetime.now(TZ) - e.datetime_received - prompt = f''' -Please generate a personalized auto-response to the following email. The email is from {e.sender} and was sent {age} ago with the subject line "{e.subject}." You are auto-responding on behalf of {profile['USER_FULLNAME']}, who is described by the following short bio (strictly for your context -- do not recite this in the response): "{profile['USER_BIO']}." {profile['USER_FULLNAME']} is unable to respond personally, because {profile['AUTORESPONSE_CONTEXT']}. Everything from here to ~~//END//~~ is the email body. + now = await locate.localize_datetime(dt_datetime.now()) + then = await locate.localize_datetime(e.datetime_received) + age = now - then + usr_prompt = f''' +Generate a personalized auto-response to the following email: +From: {e.sender} +Sent: {age} ago +Subject: "{e.subject}" +Body: {e.body} -~~//END//~~ -Keep your auto-response {profile['RESPONSE_STYLE']} and to the point, but do aim to make it responsive specifically to the sender's inquiry. - ''' + +Respond on behalf of {profile['USER_FULLNAME']}, who is unable to respond personally because {profile['AUTORESPONSE_CONTEXT']}. +Keep the response {profile['RESPONSE_STYLE']} and to the point, but responsive to the sender's inquiry. +Do not mention or recite this context information in your response. +''' + + sys_prompt = f"You are an AI assistant helping {profile['USER_FULLNAME']} with email responses. {profile['USER_FULLNAME']} is described as: {profile['USER_BIO']}" try: - response = await llm.query_ollama(prompt, 400) - return response + response = await llm.query_ollama(usr_prompt, sys_prompt, 400) + DEBUG(f"query_ollama response: {response}") + + if isinstance(response, str): + return response + elif isinstance(response, dict): + if "message" in response and "content" in response["message"]: + return response["message"]["content"] + else: + ERR(f"Unexpected response structure from query_ollama: {response}") + else: + ERR(f"Unexpected response type from query_ollama: {type(response)}") + + # If we reach here, we couldn't extract a valid response + raise ValueError("Could not extract valid response from query_ollama") + except Exception as e: ERR(f"Error generating auto-response: {str(e)}") - return "Thank you for your email. Unfortunately, an error occurred while generating the auto-response. We apologize for any inconvenience." - + return f"Thank you for your email regarding '{e.subject}'. We are currently experiencing technical difficulties with our auto-response system. We will review your email and respond as soon as possible. We apologize for any inconvenience." def clean_email_content(html_content): @@ -123,115 +167,113 @@ async def extract_attachments(attachments) -> List[str]: return attachment_texts -async def process_unread_emails(summarize_emails: bool = True, podcast: bool = True): + +async def process_account(account: EmailAccount): while True: - for account in EMAIL_ACCOUNTS: + start_time = dt_datetime.now() + try: DEBUG(f"Connecting to {account.name} to check for unread emails...") - try: - with get_imap_connection(account) as inbox: - DEBUG(f"Connected to {account.name}, checking for unread emails now...") - unread_messages = inbox.messages(unread=True) - for uid, message in unread_messages: - recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to] - this_email = IncomingEmail( - sender=message.sent_from[0]['email'], - datetime_received=localize_datetime(message.date), - recipients=recipients, - subject=message.subject, - body=clean_email_content(message.body['html'][0]) if message.body['html'] else clean_email_content(message.body['plain'][0]) or "", - attachments=message.attachments - ) - - DEBUG(f"\n\nProcessing email for account {account.name}: {this_email.subject}\n\n") - - md_path, md_relative = assemble_journal_path(this_email.datetime_received, "Emails", this_email.subject, ".md") - tts_path, tts_relative = assemble_journal_path(this_email.datetime_received, "Emails", this_email.subject, ".wav") - if summarize_emails: - email_content = f'At {this_email.datetime_received}, {this_email.sender} sent an email with the subject line "{this_email.subject}". The email in its entirety reads: \n\n{this_email.body}\n"' - if this_email.attachments: - attachment_texts = await extract_attachments(this_email.attachments) - email_content += "\n—--\n" + "\n—--\n".join([f"Attachment: {text}" for text in attachment_texts]) - - summary = await summarize.summarize_text(email_content) - await tts.local_tts(text_content = summary, speed = 1.1, voice = DEFAULT_VOICE, podcast = podcast, output_path = tts_path) - - if podcast: - if PODCAST_DIR.exists(): - tts.copy_to_podcast_dir(tts_path) - else: - ERR(f"PODCAST_DIR does not exist: {PODCAST_DIR}") - - save_email_as_markdown(this_email, summary, md_path, tts_relative) - DEBUG(f"Email '{this_email.subject}' saved to {md_relative}.") - else: - save_email_as_markdown(this_email, None, md_path, None) - - matching_profiles = get_matching_autoresponders(this_email, account) - - for profile in matching_profiles: - DEBUG(f"Auto-responding to {this_email.subject} with profile: {profile['USER_FULLNAME']}") - auto_response_subject = f"Auto-Response Re: {this_email.subject}" - auto_response_body = await generate_auto_response_body(this_email, profile) - DEBUG(f"Auto-response: {auto_response_body}") - await send_auto_response(this_email.sender, auto_response_subject, auto_response_body, profile, account) - + with get_imap_connection(account) as inbox: + DEBUG(f"Connected to {account.name}, checking for unread emails now...") + unread_messages = inbox.messages(unread=True) + for uid, message in unread_messages: + recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to] + localized_datetime = await locate.localize_datetime(message.date) + this_email = IncomingEmail( + sender=message.sent_from[0]['email'], + datetime_received=localized_datetime, + recipients=recipients, + subject=message.subject, + body=clean_email_content(message.body['html'][0]) if message.body['html'] else clean_email_content(message.body['plain'][0]) or "", + attachments=message.attachments + ) + DEBUG(f"\n\nProcessing email for account {account.name}: {this_email.subject}\n\n") + save_success = await save_email(this_email, account) + respond_success = await autorespond(this_email, account) + if save_success and respond_success: inbox.mark_seen(uid) - - await asyncio.sleep(30) - except Exception as e: - ERR(f"An error occurred for account {account.name}: {e}") - await asyncio.sleep(30) + except Exception as e: + ERR(f"An error occurred for account {account.name}: {e}") + + # Calculate the time taken for processing + processing_time = (dt_datetime.now() - start_time).total_seconds() + + # Calculate the remaining time to wait + wait_time = max(0, account.refresh - processing_time) + + # Wait for the remaining time + await asyncio.sleep(wait_time) +async def process_all_accounts(): + email_accounts = load_email_accounts(EMAIL_CONFIG) + tasks = [asyncio.create_task(process_account(account)) for account in email_accounts] + await asyncio.gather(*tasks) -def save_email_as_markdown(email: IncomingEmail, summary: str, md_path: Path, tts_path: Path): + +async def save_email(this_email: IncomingEmail, account: EmailAccount): + try: + md_path, md_relative = assemble_journal_path(this_email.datetime_received, "Emails", this_email.subject, ".md") + tts_path, tts_relative = assemble_journal_path(this_email.datetime_received, "Emails", this_email.subject, ".wav") + summary = "" + if account.summarize == True: + email_content = f'At {this_email.datetime_received}, {this_email.sender} sent an email with the subject line "{this_email.subject}". The email in its entirety reads: \n\n{this_email.body}\n"' + if this_email.attachments: + attachment_texts = await extract_attachments(this_email.attachments) + email_content += "\n—--\n" + "\n—--\n".join([f"Attachment: {text}" for text in attachment_texts]) + summary = await llm.summarize_text(email_content) + await tts.local_tts(text_content = summary, speed = 1.1, voice = DEFAULT_VOICE, podcast = account.podcast, output_path = tts_path) + summary = prefix_lines(summary, '> ') + + # Create the markdown content + markdown_content = f'''--- + date: {email.datetime_received.strftime('%Y-%m-%d')} + tags: + - email + --- + | | | | + | --: | :--: | :--: | + | *received* | **{email.datetime_received.strftime('%B %d, %Y at %H:%M:%S %Z')}** | | + | *from* | **[[{email.sender}]]** | | + | *to* | {', '.join([f'**[[{recipient}]]**' for recipient in email.recipients])} | | + | *subject* | **{email.subject}** | | ''' -Saves an email as a markdown file in the specified directory. -Args: - email (IncomingEmail): The email object containing email details. - summary (str): The summary of the email. - tts_path (str): The path to the text-to-speech audio file. + + if summary: + markdown_content += f''' + > [!summary] Summary + > {summary} ''' - DEBUG(f"Saving email to {md_path}...") - # Sanitize filename to avoid issues with filesystems - filename = f"{email.datetime_received.strftime('%Y%m%d%H%M%S')}_{email.subject.replace('/', '-')}.md".replace(':', '-').replace(' ', '_') + + if tts_path.exists(): + markdown_content += f''' + ![[{tts_path}]] + ''' + + markdown_content += f''' + --- + {email.body} + ''' + + with open(md_path, 'w', encoding='utf-8') as md_file: + md_file.write(markdown_content) - summary = prefix_lines(summary, '> ') - # Create the markdown content - markdown_content = f'''--- -date: {email.datetime_received.strftime('%Y-%m-%d')} -tags: - - email ---- -| | | | -| --: | :--: | :--: | -| *received* | **{email.datetime_received.strftime('%B %d, %Y at %H:%M:%S %Z')}** | | -| *from* | **[[{email.sender}]]** | | -| *to* | {', '.join([f'**[[{recipient}]]**' for recipient in email.recipients])} | | -| *subject* | **{email.subject}** | | -''' + DEBUG(f"Saved markdown to {md_path}") + + return True - if summary: - markdown_content += f''' -> [!summary] Summary -> {summary} -''' - - if tts_path: - markdown_content += f''' -![[{tts_path}]] -''' - - markdown_content += f''' ---- -{email.body} -''' - - with open(md_path, 'w', encoding='utf-8') as md_file: - md_file.write(markdown_content) - - DEBUG(f"Saved markdown to {md_path}") + except Exception as e: + ERR(f"Exception: {e}") + return False +async def autorespond(this_email: IncomingEmail, account: EmailAccount): + matching_profiles = get_matching_autoresponders(this_email, account) + for profile in matching_profiles: + DEBUG(f"Auto-responding to {this_email.subject} with profile: {profile['USER_FULLNAME']}") + auto_response_subject = f"Auto-Response Re: {this_email.subject}" + auto_response_body = await generate_auto_response_body(this_email, profile) + DEBUG(f"Auto-response: {auto_response_body}") + await send_auto_response(this_email.sender, auto_response_subject, auto_response_body, profile, account) async def send_auto_response(to_email, subject, body, profile, account): DEBUG(f"Sending auto response to {to_email}...") @@ -243,35 +285,24 @@ async def send_auto_response(to_email, subject, body, profile, account): message.attach(MIMEText(body, 'plain')) if profile['IMG_GEN_PROMPT']: - jpg_path = sd.workflow(profile['IMG_GEN_PROMPT'], earlyout=False, downscale_to_fit=True) + jpg_path = await sd.workflow(profile['IMG_GEN_PROMPT'], earlyout=False, downscale_to_fit=True) if jpg_path and os.path.exists(jpg_path): with open(jpg_path, 'rb') as img_file: img = MIMEImage(img_file.read(), name=os.path.basename(jpg_path)) message.attach(img) - context = ssl._create_unverified_context() - with SMTP_SSL(account.smtp.host, account.smtp.port, context=context) as server: + with get_smtp_connection(account) as server: server.login(account.smtp.username, account.smtp.password) server.send_message(message) INFO(f"Auto-response sent to {to_email} concerning {subject} from account {account.name}") + return True except Exception as e: ERR(f"Error in preparing/sending auto-response from account {account.name}: {e}") - raise e - - + return False @email.on_event("startup") async def startup_event(): - asyncio.create_task(process_unread_emails()) - - - - - - #### - - - + asyncio.create_task(process_all_accounts()) \ No newline at end of file diff --git a/sijapi/routers/llm.py b/sijapi/routers/llm.py index b9e23cd..7338c49 100644 --- a/sijapi/routers/llm.py +++ b/sijapi/routers/llm.py @@ -1,10 +1,9 @@ #routers/llm.py -from fastapi import APIRouter, HTTPException, Request, Response -from fastapi.responses import StreamingResponse, JSONResponse -from starlette.responses import StreamingResponse +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 from dateutil import parser -from typing import List, Dict, Any, Union +from typing import List, Dict, Any, Union, Optional from pydantic import BaseModel, root_validator, ValidationError import aiofiles import os @@ -17,21 +16,20 @@ import base64 from pathlib import Path import ollama from ollama import AsyncClient as Ollama, list as OllamaList -import aiofiles import time import asyncio -from pathlib import Path -from fastapi import FastAPI, Request, HTTPException, APIRouter -from fastapi.responses import JSONResponse, StreamingResponse -from dotenv import load_dotenv -from sijapi import BASE_DIR, DATA_DIR, LOGS_DIR, CONFIG_DIR, LLM_SYS_MSG, DEFAULT_LLM, DEFAULT_VISION, REQUESTS_DIR, OBSIDIAN_CHROMADB_COLLECTION, OBSIDIAN_VAULT_DIR, DOC_DIR, OPENAI_API_KEY -from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi.utilities import convert_to_unix_time, sanitize_filename +import tempfile +import shutil +import html2text +import markdown +from sijapi import LLM_SYS_MSG, DEFAULT_LLM, DEFAULT_VISION, REQUESTS_DIR, OBSIDIAN_CHROMADB_COLLECTION, OBSIDIAN_VAULT_DIR, DOC_DIR, OPENAI_API_KEY, DEBUG, INFO, WARN, ERR, CRITICAL, 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.asr import transcribe_audio + llm = APIRouter() - - # Initialize chromadb client client = chromadb.Client() OBSIDIAN_CHROMADB_COLLECTION = client.create_collection("obsidian") @@ -80,11 +78,11 @@ async def generate_response(prompt: str): return {"response": output['response']} -async def query_ollama(usr: str, sys: str = LLM_SYS_MSG, max_tokens: int = 200): +async def query_ollama(usr: str, sys: str = LLM_SYS_MSG, model: str = DEFAULT_LLM, max_tokens: int = 200): messages = [{"role": "system", "content": sys}, {"role": "user", "content": usr}] LLM = Ollama() - response = await LLM.chat(model=DEFAULT_LLM, messages=messages, options={"num_predict": max_tokens}) + response = await LLM.chat(model=model, messages=messages, options={"num_predict": max_tokens}) DEBUG(response) if "message" in response: @@ -482,3 +480,186 @@ def gpt4v(image_base64, prompt_sys: str, prompt_usr: str, max_tokens: int = 150) try_again = gpt4v(image_base64, prompt_sys, prompt_usr, max_tokens) return try_again + + +@llm.get("/summarize") +async def summarize_get(text: str = Form(None), instruction: str = Form(SUMMARY_INSTRUCT)): + summarized_text = await summarize_text(text, instruction) + return summarized_text + +@llm.post("/summarize") +async def summarize_post(file: Optional[UploadFile] = File(None), text: Optional[str] = Form(None), instruction: str = Form(SUMMARY_INSTRUCT)): + text_content = text if text else await extract_text(file) + summarized_text = await summarize_text(text_content, instruction) + return summarized_text + +@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)): + + 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( + text: str, + instruction: str = SUMMARY_INSTRUCT, + voice: Optional[str] = DEFAULT_VOICE, + speed: float = 1.1, + podcast: bool = False, + LLM: Ollama = None +): + 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 = sanitize_filename(filename) + filename = ' '.join(filename.split()[:5]) + timestamp = dt_datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{timestamp}{filename}.wav" + + background_tasks = BackgroundTasks() + final_output_path = await generate_speech(background_tasks, summarized_text, voice, "xtts", speed=speed, podcast=podcast, title=filename) + DEBUG(f"summary_tts completed with final_output_path: {final_output_path}") + return final_output_path + + +async def get_title(text: str, LLM: Ollama() = None): + LLM = LLM if LLM else Ollama() + title = await process_chunk("Generate a title for this text", text, 1, 1, 12, LLM) + 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. + """ + 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 + chunks = [] + for i in range(0, len(words), adjusted_chunk_size - adjusted_overlap): + DEBUG(f"We are on iteration # {i} if split_text_into_chunks.") + chunk = ' '.join(words[i:i + adjusted_chunk_size]) + chunks.append(chunk) + return chunks + + +def calculate_max_tokens(text: str) -> int: + tokens_count = max(1, int(len(text.split()) * SUMMARY_TPW)) # Ensure at least 1 + return min(tokens_count // 4, SUMMARY_CHUNK_SIZE) + + +async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], background_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") + + _, file_ext = os.path.splitext(file_path) + file_ext = file_ext.lower() + text_content = "" + + 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) + + if background_tasks and 'temp_file_path' in locals(): + background_tasks.add_task(os.remove, temp_file_path) + elif 'temp_file_path' in locals(): + os.remove(temp_file_path) + + 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_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 + 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 + + 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) + ]) + + concatenated_summary = ' '.join(summaries) + + if total_parts > 1: + concatenated_summary = await process_chunk(instruction, concatenated_summary, 1, 1) + + return concatenated_summary + +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. + """ + + 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 + + DEBUG(f"Summarizing part {part} of {total_parts}: Max_tokens: {max_tokens}") + + if part and total_parts > 1: + prompt = f"{instruction}. Part {part} of {total_parts}:\n{text}" + else: + prompt = f"{instruction}:\n\n{text}" + + DEBUG(f"Starting LLM.generate for part {part} of {total_parts}") + response = await LLM.generate( + model=SUMMARY_MODEL, + prompt=prompt, + stream=False, + options={'num_predict': max_tokens, 'temperature': 0.6} + ) + + text_response = response['response'] + DEBUG(f"Completed LLM.generate for part {part} of {total_parts}") + + return text_response + +async def title_and_summary(extracted_text: str): + title = await get_title(extracted_text) + processed_title = title.split("\n")[-1] + processed_title = processed_title.split("\r")[-1] + processed_title = sanitize_filename(processed_title) + summary = await summarize_text(extracted_text) + + return processed_title, summary \ No newline at end of file diff --git a/sijapi/routers/locate.py b/sijapi/routers/locate.py index 3a734fe..bb3ad8c 100644 --- a/sijapi/routers/locate.py +++ b/sijapi/routers/locate.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException, Query from fastapi.responses import HTMLResponse, JSONResponse import requests -import json +import yaml import time import pytz import traceback @@ -9,74 +9,41 @@ from datetime import datetime, timezone from typing import Union, List import asyncio import pytz +import aiohttp import folium import time as timer +from dateutil.parser import parse as dateutil_parse from pathlib import Path from pydantic import BaseModel from typing import Optional, Any, Dict, List, Union from datetime import datetime, timedelta, time -from sijapi import LOCATION_OVERRIDES, TZ -from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi.utilities import get_db_connection, haversine, localize_datetime +from sijapi import NAMED_LOCATIONS, TZ, DynamicTZ +from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, DB +from sijapi.classes import Location +from sijapi.utilities import haversine # from osgeo import gdal # import elevation + locate = APIRouter() - -from pydantic import BaseModel -from typing import Optional, List, Dict, Any -from datetime import datetime -import requests - -class Location(BaseModel): - latitude: float - longitude: float - datetime: datetime - elevation: Optional[float] = None - altitude: Optional[float] = None - zip: Optional[str] = None - street: Optional[str] = None - city: Optional[str] = None - state: Optional[str] = None - country: Optional[str] = None - context: Optional[Dict[str, Any]] = None - class_: Optional[str] = None - type: Optional[str] = None - name: Optional[str] = None - display_name: Optional[str] = None - boundingbox: Optional[List[str]] = None - amenity: Optional[str] = None - house_number: Optional[str] = None - road: Optional[str] = None - quarter: Optional[str] = None - neighbourhood: Optional[str] = None - suburb: Optional[str] = None - county: Optional[str] = None - country_code: Optional[str] = None - - class Config: - json_encoders = { - datetime: lambda dt: dt.isoformat(), - } - -def reverse_geocode(latitude: float, longitude: float) -> Optional[Location]: +async def reverse_geocode(latitude: float, longitude: float) -> Optional[Location]: url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={latitude}&lon={longitude}" INFO(f"Calling Nominatim API at {url}") headers = { 'User-Agent': 'sij.law/1.0 (sij@sij.law)', # replace with your app name and email } try: - response = requests.get(url, headers=headers) - response.raise_for_status() # Raise an exception for unsuccessful requests - data = response.json() + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + response.raise_for_status() + data = await response.json() address = data.get("address", {}) - location = Location( latitude=float(data.get("lat", latitude)), longitude=float(data.get("lon", longitude)), - datetime=datetime.now(), # You might want to adjust this based on your needs + datetime=datetime.now(timezone.utc), zip=address.get("postcode"), street=address.get("road"), city=address.get("city"), @@ -97,12 +64,9 @@ def reverse_geocode(latitude: float, longitude: float) -> Optional[Location]: county=address.get("county"), country_code=address.get("country_code") ) - INFO(f"Created Location object: {location}") - return location - - except requests.exceptions.RequestException as e: + except aiohttp.ClientError as e: ERR(f"Error: {e}") return None @@ -116,66 +80,99 @@ async def geocode(zip_code: Optional[str] = None, latitude: Optional[float] = No try: # Establish the database connection - conn = get_db_connection() + async with DB.get_connection() as conn: + + # Build the SQL query based on the provided parameters + query = "SELECT id, street, city, state, country, latitude, longitude, zip, elevation, datetime, date, ST_Distance(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)) AS distance FROM Locations" + + conditions = [] + params = [] + + if latitude is not None and longitude is not None: + conditions.append("ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326), 50000)") # 50 km radius + params.extend([longitude, latitude]) + + if zip_code: + conditions.append("zip = $3 AND country = $4") + params.extend([zip_code, country_code]) + + if city and state: + conditions.append("city ILIKE $5 AND state ILIKE $6 AND country = $7") + params.extend([city, state, country_code]) + + if conditions: + query += " WHERE " + " OR ".join(conditions) + + query += " ORDER BY distance LIMIT 1;" + + DEBUG(f"Executing query: {query} with params: {params}") + + # Execute the query with the provided parameters + result = await conn.fetchrow(query, *params) + + # Close the connection + await conn.close() + + if result: + location_info = Location( + latitude=result['latitude'], + longitude=result['longitude'], + datetime=result.get['datetime'], + zip=result['zip'], + street=result.get('street', ''), + city=result['city'], + state=result['state'], + country=result['country'], + elevation=result.get('elevation', 0), + distance=result.get('distance') + ) + DEBUG(f"Found location: {location_info}") + return location_info + else: + DEBUG("No location found with provided parameters.") + return Location() - # Build the SQL query based on the provided parameters - query = "SELECT id, street, city, state, country, latitude, longitude, zip, elevation, datetime, date, ST_Distance(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)) AS distance FROM Locations" - - conditions = [] - params = [] - - if latitude is not None and longitude is not None: - conditions.append("ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326), 50000)") # 50 km radius - params.extend([longitude, latitude]) - - if zip_code: - conditions.append("zip = $3 AND country = $4") - params.extend([zip_code, country_code]) - - if city and state: - conditions.append("city ILIKE $5 AND state ILIKE $6 AND country = $7") - params.extend([city, state, country_code]) - - if conditions: - query += " WHERE " + " OR ".join(conditions) - - query += " ORDER BY distance LIMIT 1;" - - DEBUG(f"Executing query: {query} with params: {params}") - - # Execute the query with the provided parameters - result = await conn.fetchrow(query, *params) - - # Close the connection - await conn.close() - - if result: - location_info = Location( - latitude=result['latitude'], - longitude=result['longitude'], - datetime=result.get['datetime'], - zip=result['zip'], - street=result.get('street', ''), - city=result['city'], - state=result['state'], - country=result['country'], - elevation=result.get('elevation', 0), - distance=result.get('distance') - ) - DEBUG(f"Found location: {location_info}") - return location_info - else: - DEBUG("No location found with provided parameters.") - return Location() - except Exception as e: ERR(f"Error occurred: {e}") raise Exception("An error occurred while processing your request") + +async def localize_datetime(dt, fetch_loc: bool = False): + initial_dt = dt + + if fetch_loc: + loc = await get_last_location() + tz = await DynamicTZ.get_current(loc) + else: + tz = await DynamicTZ.get_last() + + try: + if isinstance(dt, str): + dt = dateutil_parse(dt) + DEBUG(f"{initial_dt} was a string so we attempted converting to datetime. Result: {dt}") + + if isinstance(dt, datetime): + DEBUG(f"{dt} is a datetime object, so we will ensure it is tz-aware.") + if dt.tzinfo is None: + dt = dt.replace(tzinfo=TZ) + # DEBUG(f"{dt} should now be tz-aware. Returning it now.") + return dt + else: + # DEBUG(f"{dt} already was tz-aware. Returning it now.") + return dt + else: + ERR(f"Conversion failed") + raise TypeError("Conversion failed") + except Exception as e: + ERR(f"Error parsing datetime: {e}") + raise TypeError("Input must be a string or datetime object") + + + def find_override_locations(lat: float, lon: float) -> Optional[str]: # Load the JSON file - with open(LOCATION_OVERRIDES, 'r') as file: - locations = json.load(file) + with open(NAMED_LOCATIONS, 'r') as file: + locations = yaml.safe_load(file) closest_location = None closest_distance = float('inf') @@ -227,118 +224,120 @@ def get_elevation(latitude, longitude): return None + async def fetch_locations(start: datetime, end: datetime = None) -> List[Location]: - start_datetime = localize_datetime(start) - + start_datetime = await localize_datetime(start) if end is None: - end_datetime = localize_datetime(start_datetime.replace(hour=23, minute=59, second=59)) + end_datetime = await localize_datetime(start_datetime.replace(hour=23, minute=59, second=59)) else: - end_datetime = localize_datetime(end) - if start_datetime.time() == datetime.min.time() and end.time() == datetime.min.time(): - end_datetime = end_datetime.replace(hour=23, minute=59, second=59) - + end_datetime = await localize_datetime(end) + + if start_datetime.time() == datetime.min.time() and end_datetime.time() == datetime.min.time(): + end_datetime = end_datetime.replace(hour=23, minute=59, second=59) + DEBUG(f"Fetching locations between {start_datetime} and {end_datetime}") - conn = await get_db_connection() - locations = [] - - # Check for records within the specified datetime range - range_locations = await conn.fetch(''' - SELECT id, datetime, + + async with DB.get_connection() as conn: + locations = [] + # Check for records within the specified datetime range + range_locations = await conn.fetch(''' + SELECT id, datetime, ST_X(ST_AsText(location)::geometry) AS longitude, ST_Y(ST_AsText(location)::geometry) AS latitude, ST_Z(ST_AsText(location)::geometry) AS elevation, city, state, zip, street, action, device_type, device_model, device_name, device_os - FROM locations - WHERE datetime >= $1 AND datetime <= $2 - ORDER BY datetime DESC - ''', start_datetime.replace(tzinfo=None), end_datetime.replace(tzinfo=None)) - - DEBUG(f"Range locations query returned: {range_locations}") - locations.extend(range_locations) - - if not locations and (end is None or start_datetime.date() == end.date()): - location_data = await conn.fetchrow(''' - SELECT id, datetime, + FROM locations + WHERE datetime >= $1 AND datetime <= $2 + ORDER BY datetime DESC + ''', start_datetime.replace(tzinfo=None), end_datetime.replace(tzinfo=None)) + + DEBUG(f"Range locations query returned: {range_locations}") + locations.extend(range_locations) + + if not locations and (end is None or start_datetime.date() == end_datetime.date()): + location_data = await conn.fetchrow(''' + SELECT id, datetime, ST_X(ST_AsText(location)::geometry) AS longitude, ST_Y(ST_AsText(location)::geometry) AS latitude, ST_Z(ST_AsText(location)::geometry) AS elevation, city, state, zip, street, action, device_type, device_model, device_name, device_os - FROM locations - WHERE datetime < $1 - ORDER BY datetime DESC - LIMIT 1 - ''', start_datetime.replace(tzinfo=None)) - - DEBUG(f"Fallback query returned: {location_data}") - if location_data: - locations.append(location_data) - - await conn.close() + FROM locations + WHERE datetime < $1 + ORDER BY datetime DESC + LIMIT 1 + ''', start_datetime.replace(tzinfo=None)) + + DEBUG(f"Fallback query returned: {location_data}") + if location_data: + locations.append(location_data) DEBUG(f"Locations found: {locations}") # Sort location_data based on the datetime field in descending order sorted_locations = sorted(locations, key=lambda x: x['datetime'], reverse=True) - + # Create Location objects directly from the location data - location_objects = [Location( - latitude=loc['latitude'], - longitude=loc['longitude'], - datetime=loc['datetime'], - elevation=loc.get('elevation'), - city=loc.get('city'), - state=loc.get('state'), - zip=loc.get('zip'), - street=loc.get('street'), - context={ - 'action': loc.get('action'), - 'device_type': loc.get('device_type'), - 'device_model': loc.get('device_model'), - 'device_name': loc.get('device_name'), - 'device_os': loc.get('device_os') - } - ) for loc in sorted_locations if loc['latitude'] is not None and loc['longitude'] is not None] + location_objects = [ + Location( + latitude=loc['latitude'], + longitude=loc['longitude'], + datetime=loc['datetime'], + elevation=loc.get('elevation'), + city=loc.get('city'), + state=loc.get('state'), + zip=loc.get('zip'), + street=loc.get('street'), + context={ + 'action': loc.get('action'), + 'device_type': loc.get('device_type'), + 'device_model': loc.get('device_model'), + 'device_name': loc.get('device_name'), + 'device_os': loc.get('device_os') + } + ) for loc in sorted_locations if loc['latitude'] is not None and loc['longitude'] is not None + ] return location_objects if location_objects else [] # Function to fetch the last location before the specified datetime async def fetch_last_location_before(datetime: datetime) -> Optional[Location]: - datetime = localize_datetime(datetime) + datetime = await localize_datetime(datetime) DEBUG(f"Fetching last location before {datetime}") - conn = await get_db_connection() - location_data = await conn.fetchrow(''' - SELECT id, datetime, - ST_X(ST_AsText(location)::geometry) AS longitude, - ST_Y(ST_AsText(location)::geometry) AS latitude, - ST_Z(ST_AsText(location)::geometry) AS elevation, - city, state, zip, street, country, - action - FROM locations - WHERE datetime < $1 - ORDER BY datetime DESC - LIMIT 1 - ''', datetime.replace(tzinfo=None)) - - await conn.close() + async with DB.get_connection() as conn: - if location_data: - DEBUG(f"Last location found: {location_data}") - return Location(**location_data) - else: - DEBUG("No location found before the specified datetime") - return None + location_data = await conn.fetchrow(''' + SELECT id, datetime, + ST_X(ST_AsText(location)::geometry) AS longitude, + ST_Y(ST_AsText(location)::geometry) AS latitude, + ST_Z(ST_AsText(location)::geometry) AS elevation, + city, state, zip, street, country, + action + FROM locations + WHERE datetime < $1 + ORDER BY datetime DESC + LIMIT 1 + ''', datetime.replace(tzinfo=None)) + + await conn.close() + + if location_data: + DEBUG(f"Last location found: {location_data}") + return Location(**location_data) + else: + DEBUG("No location found before the specified datetime") + return None @locate.get("/map/start_date={start_date_str}&end_date={end_date_str}", response_class=HTMLResponse) async def generate_map_endpoint(start_date_str: str, end_date_str: str): try: - start_date = localize_datetime(start_date_str) - end_date = localize_datetime(end_date_str) + start_date = await localize_datetime(start_date_str) + end_date = await localize_datetime(end_date_str) except ValueError: raise HTTPException(status_code=400, detail="Invalid date format") @@ -349,7 +348,7 @@ async def generate_map_endpoint(start_date_str: str, end_date_str: str): @locate.get("/map", response_class=HTMLResponse) async def generate_alltime_map_endpoint(): try: - start_date = localize_datetime(datetime.fromisoformat("2022-01-01")) + start_date = await localize_datetime(datetime.fromisoformat("2022-01-01")) end_date = localize_datetime(datetime.now()) except ValueError: raise HTTPException(status_code=400, detail="Invalid date format") @@ -387,59 +386,59 @@ async def generate_map(start_date: datetime, end_date: datetime): async def post_location(location: Location): DEBUG(f"post_location called with {location.datetime}") - conn = await get_db_connection() - try: - context = location.context or {} - action = context.get('action', 'manual') - device_type = context.get('device_type', 'Unknown') - device_model = context.get('device_model', 'Unknown') - device_name = context.get('device_name', 'Unknown') - device_os = context.get('device_os', 'Unknown') - - # Parse and localize the datetime - localized_datetime = localize_datetime(location.datetime) - - await conn.execute(''' - INSERT INTO locations (datetime, location, city, state, zip, street, action, device_type, device_model, device_name, device_os) - VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3, $4), 4326), $5, $6, $7, $8, $9, $10, $11, $12, $13) - ''', localized_datetime, location.longitude, location.latitude, location.elevation, location.city, location.state, location.zip, location.street, action, device_type, device_model, device_name, device_os) - await conn.close() - INFO(f"Successfully posted location: {location.latitude}, {location.longitude} on {localized_datetime}") - return { - 'datetime': localized_datetime, - 'latitude': location.latitude, - 'longitude': location.longitude, - 'city': location.city, - 'state': location.state, - 'zip': location.zip, - 'street': location.street, - 'elevation': location.elevation, - 'action': action, - 'device_type': device_type, - 'device_model': device_model, - 'device_name': device_name, - 'device_os': device_os - } - except Exception as e: - ERR(f"Error posting location {e}") - ERR(traceback.format_exc()) - return None + + async with DB.get_connection() as conn: + try: + context = location.context or {} + action = context.get('action', 'manual') + device_type = context.get('device_type', 'Unknown') + device_model = context.get('device_model', 'Unknown') + device_name = context.get('device_name', 'Unknown') + device_os = context.get('device_os', 'Unknown') + + # Parse and localize the datetime + localized_datetime = await localize_datetime(location.datetime) + + await conn.execute(''' + INSERT INTO locations (datetime, location, city, state, zip, street, action, device_type, device_model, device_name, device_os) + VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3, $4), 4326), $5, $6, $7, $8, $9, $10, $11, $12, $13) + ''', localized_datetime, location.longitude, location.latitude, location.elevation, location.city, location.state, location.zip, location.street, action, device_type, device_model, device_name, device_os) + await conn.close() + INFO(f"Successfully posted location: {location.latitude}, {location.longitude} on {localized_datetime}") + return { + 'datetime': localized_datetime, + 'latitude': location.latitude, + 'longitude': location.longitude, + 'city': location.city, + 'state': location.state, + 'zip': location.zip, + 'street': location.street, + 'elevation': location.elevation, + 'action': action, + 'device_type': device_type, + 'device_model': device_model, + 'device_name': device_name, + 'device_os': device_os + } + except Exception as e: + ERR(f"Error posting location {e}") + ERR(traceback.format_exc()) + return None + @locate.post("/locate") async def post_locate_endpoint(locations: Union[Location, List[Location]]): responses = [] - if isinstance(locations, Location): locations = [locations] - + for location in locations: if not location.datetime: - current_time = datetime.now(timezone.utc) - location.datetime = current_time.isoformat() - + location.datetime = datetime.now(timezone.utc).isoformat() + if not location.elevation: - location.elevation = location.altitude if location.altitude else get_elevation(location.latitude, location.longitude) - + location.elevation = location.altitude if location.altitude else await get_elevation(location.latitude, location.longitude) + # Ensure context is a dictionary with default values if not provided if not location.context: location.context = { @@ -449,42 +448,57 @@ async def post_locate_endpoint(locations: Union[Location, List[Location]]): "device_name": "Unknown", "device_os": "Unknown" } - + DEBUG(f"datetime before localization: {location.datetime}") # Convert datetime string to timezone-aware datetime object - location.datetime = localize_datetime(location.datetime) + location.datetime = await localize_datetime(location.datetime) DEBUG(f"datetime after localization: {location.datetime}") - + + # Perform reverse geocoding + geocoded_location = await reverse_geocode(location.latitude, location.longitude) + if geocoded_location: + # Update location with geocoded information + for field in location.__fields__: + if getattr(location, field) is None: + setattr(location, field, getattr(geocoded_location, field)) + location_entry = await post_location(location) if location_entry: responses.append({"location_data": location_entry}) # Add weather data if necessary - - await asyncio.sleep(0.1) # Use asyncio.sleep for async compatibility - + return {"message": "Locations and weather updated", "results": responses} +# Assuming post_location and get_elevation are async functions. If not, they should be modified to be async as well. -# GET endpoint to fetch the last location before the specified datetime -# @locate.get("/last_location", response_model=Union[Location, Dict[str, str]]) -@locate.get("/locate", response_model=List[Location]) -async def get_last_location() -> JSONResponse: + +async def get_last_location() -> Optional[Location]: query_datetime = datetime.now(TZ) DEBUG(f"Query_datetime: {query_datetime}") + location = await fetch_last_location_before(query_datetime) + if location: DEBUG(f"location: {location}") - location_dict = location.model_dump() # use model_dump instead of dict + return location + + return None + +@locate.get("/locate", response_model=Location) +async def get_last_location_endpoint() -> JSONResponse: + location = await get_last_location() + + if location: + location_dict = location.model_dump() location_dict["datetime"] = location.datetime.isoformat() return JSONResponse(content=location_dict) else: - return JSONResponse(content={"message": "No location found before the specified datetime"}, status_code=404) - + raise HTTPException(status_code=404, detail="No location found before the specified datetime") @locate.get("/locate/{datetime_str}", response_model=List[Location]) async def get_locate(datetime_str: str, all: bool = False): try: - date_time = localize_datetime(datetime_str) + date_time = await localize_datetime(datetime_str) except ValueError as e: ERR(f"Invalid datetime string provided: {datetime_str}") return ["ERROR: INVALID DATETIME PROVIDED. USE YYYYMMDDHHmmss or YYYYMMDD format."] diff --git a/sijapi/routers/note.py b/sijapi/routers/note.py index 996c603..f83157a 100644 --- a/sijapi/routers/note.py +++ b/sijapi/routers/note.py @@ -17,13 +17,12 @@ from requests.adapters import HTTPAdapter import re import os from datetime import timedelta, datetime, time as dt_time, date as dt_date -from sijapi.utilities import localize_datetime from fastapi import HTTPException, status from pathlib import Path from fastapi import APIRouter, Query, HTTPException from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, INFO from sijapi import YEAR_FMT, MONTH_FMT, DAY_FMT, DAY_SHORT_FMT, OBSIDIAN_VAULT_DIR, OBSIDIAN_RESOURCES_DIR, BASE_URL, OBSIDIAN_BANNER_SCENE, DEFAULT_11L_VOICE, DEFAULT_VOICE, TZ -from sijapi.routers import tts, time, sd, locate, weather, asr, calendar, summarize +from sijapi.routers import tts, llm, time, sd, locate, weather, asr, calendar from sijapi.routers.locate import Location from sijapi.utilities import assemble_journal_path, convert_to_12_hour_format, sanitize_filename, convert_degrees_to_cardinal, HOURLY_COLUMNS_MAPPING @@ -39,7 +38,7 @@ async def build_daily_note_range_endpoint(dt_start: str, dt_end: str): results = [] current_date = start_date while current_date <= end_date: - formatted_date = localize_datetime(current_date) + formatted_date = await locate.localize_datetime(current_date) result = await build_daily_note(formatted_date) results.append(result) current_date += timedelta(days=1) @@ -58,7 +57,7 @@ Obsidian helper. Takes a datetime and creates a new daily note. Note: it uses th header = f"# [[{day_before}|← ]] {formatted_day} [[{day_after}| →]]\n\n" places = await locate.fetch_locations(date_time) - location = locate.reverse_geocode(places[0].latitude, places[0].longitude) + location = await locate.reverse_geocode(places[0].latitude, places[0].longitude) timeslips = await build_daily_timeslips(date_time) @@ -271,9 +270,9 @@ async def process_document( with open(file_path, 'wb') as f: f.write(document_content) - parsed_content = await summarize.extract_text(file_path) # Ensure extract_text is awaited + parsed_content = await llm.extract_text(file_path) # Ensure extract_text is awaited - llm_title, summary = await summarize.title_and_summary(parsed_content) + llm_title, summary = await llm.title_and_summary(parsed_content) try: readable_title = sanitize_filename(title if title else document.filename) @@ -342,7 +341,7 @@ async def process_article( timestamp = datetime.now().strftime('%b %d, %Y at %H:%M') - parsed_content = parse_article(url, source) + parsed_content = await parse_article(url, source) if parsed_content is None: return {"error": "Failed to retrieve content"} @@ -350,7 +349,7 @@ async def process_article( markdown_filename, relative_path = assemble_journal_path(datetime.now(), subdir="Articles", filename=readable_title, extension=".md") try: - summary = await summarize.summarize_text(parsed_content["content"], "Summarize the provided text. Respond with the summary and nothing else. Do not otherwise acknowledge the request. Just provide the requested summary.") + summary = await llm.summarize_text(parsed_content["content"], "Summarize the provided text. Respond with the summary and nothing else. Do not otherwise acknowledge the request. Just provide the requested summary.") summary = summary.replace('\n', ' ') # Remove line breaks if tts_mode == "full" or tts_mode == "content": @@ -427,7 +426,7 @@ tags: raise HTTPException(status_code=500, detail=str(e)) -def parse_article(url: str, source: Optional[str] = None): +async def parse_article(url: str, source: Optional[str] = None): source = source if source else trafilatura.fetch_url(url) traf = trafilatura.extract_metadata(filecontent=source, default_url=url) @@ -442,7 +441,12 @@ def parse_article(url: str, source: Optional[str] = None): title = np3k.title or traf.title authors = np3k.authors or traf.author authors = authors if isinstance(authors, List) else [authors] - date = np3k.publish_date or localize_datetime(traf.date) + date = np3k.publish_date or traf.date + try: + date = await locate.localize_datetime(date) + except: + DEBUG(f"Failed to localize {date}") + date = await locate.localize_datetime(datetime.now()) excerpt = np3k.meta_description or traf.description content = trafilatura.extract(source, output_format="markdown", include_comments=False) or np3k.text image = np3k.top_image or traf.image @@ -474,7 +478,7 @@ async def process_archive( timestamp = datetime.now().strftime('%b %d, %Y at %H:%M') - parsed_content = parse_article(url, source) + parsed_content = await parse_article(url, source) if parsed_content is None: return {"error": "Failed to retrieve content"} content = parsed_content["content"] @@ -635,7 +639,7 @@ async def banner_endpoint(dt: str, location: str = None, mood: str = None, other Endpoint (POST) that generates a new banner image for the Obsidian daily note for a specified date, taking into account optional additional information, then updates the frontmatter if necessary. ''' DEBUG(f"banner_endpoint requested with date: {dt} ({type(dt)})") - date_time = localize_datetime(dt) + date_time = await locate.localize_datetime(dt) DEBUG(f"date_time after localization: {date_time} ({type(date_time)})") jpg_path = await generate_banner(date_time, location, mood=mood, other_context=other_context) return jpg_path @@ -643,7 +647,7 @@ async def banner_endpoint(dt: str, location: str = None, mood: str = None, other async def generate_banner(dt, location: Location = None, forecast: str = None, mood: str = None, other_context: str = None): DEBUG(f"Location: {location}, forecast: {forecast}, mood: {mood}, other_context: {other_context}") - date_time = localize_datetime(dt) + date_time = await locate.localize_datetime(dt) DEBUG(f"generate_banner called with date_time: {date_time}") destination_path, local_path = assemble_journal_path(date_time, filename="Banner", extension=".jpg", no_timestamp = True) DEBUG(f"destination path generated: {destination_path}") @@ -699,7 +703,7 @@ async def note_weather_get( ): try: - date_time = datetime.now() if date == "0" else localize_datetime(date) + date_time = datetime.now() if date == "0" else locate.localize_datetime(date) DEBUG(f"date: {date} .. date_time: {date_time}") content = await update_dn_weather(date_time) #, lat, lon) return JSONResponse(content={"forecast": content}, status_code=200) @@ -714,7 +718,7 @@ async def note_weather_get( @note.post("/update/note/{date}") async def post_update_daily_weather_and_calendar_and_timeslips(date: str) -> PlainTextResponse: - date_time = localize_datetime(date) + date_time = await locate.localize_datetime(date) await update_dn_weather(date_time) await update_daily_note_events(date_time) await build_daily_timeslips(date_time) @@ -1091,7 +1095,7 @@ async def format_events_as_markdown(event_data: Dict[str, Union[str, List[Dict[s # description = remove_characters(description) # description = remove_characters(description) if len(description) > 150: - description = await summarize.summarize_text(description, length_override=150) + description = await llm.summarize_text(description, length_override=150) event_markdown += f"\n * {description}" event_markdown += f"\n " @@ -1117,7 +1121,7 @@ async def format_events_as_markdown(event_data: Dict[str, Union[str, List[Dict[s @note.get("/note/events", response_class=PlainTextResponse) async def note_events_endpoint(date: str = Query(None)): - date_time = localize_datetime(date) if date else datetime.now(TZ) + date_time = await locate.localize_datetime(date) if date else datetime.now(TZ) response = await update_daily_note_events(date_time) return PlainTextResponse(content=response, status_code=200) diff --git a/sijapi/routers/sd.py b/sijapi/routers/sd.py index cc2404f..641f7b5 100644 --- a/sijapi/routers/sd.py +++ b/sijapi/routers/sd.py @@ -16,6 +16,7 @@ from PIL import Image from pathlib import Path import uuid import json +import yaml import ipaddress import socket import subprocess @@ -228,30 +229,29 @@ def get_return_path(destination_path): else: return str(destination_path) -# This allows selected scenes by name def get_scene(scene): with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file: - SD_CONFIG = json.load(SD_CONFIG_file) + SD_CONFIG = yaml.safe_load(SD_CONFIG_file) for scene_data in SD_CONFIG['scenes']: if scene_data['scene'] == scene: return scene_data - return None + return None -# This returns the scene with the most trigger words present in the provided prompt, or otherwise if none match it returns the first scene in the array - meaning the first should be considered the default scene. +# This returns the scene with the most trigger words present in the provided prompt, +# or otherwise if none match it returns the first scene in the array - +# meaning the first should be considered the default scene. 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 = json.load(SD_CONFIG_file) + SD_CONFIG = yaml.safe_load(SD_CONFIG_file) for sc in SD_CONFIG['scenes']: count = sum(1 for trigger in sc['triggers'] if trigger in prompt_lower) if count > max_count: max_count = count scene_data = sc - return scene_data if scene_data else SD_CONFIG['scenes'][0] # fall back on first scene, which should be an appropriate default scene. - - + return scene_data if scene_data else SD_CONFIG['scenes'][0] # fall back on first scene, which should be an appropriate default scene. diff --git a/sijapi/routers/serve.py b/sijapi/routers/serve.py index f9271c1..8ea6cc3 100644 --- a/sijapi/routers/serve.py +++ b/sijapi/routers/serve.py @@ -14,7 +14,8 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from pathlib import Path from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi.utilities import bool_convert, sanitize_filename, assemble_journal_path, localize_datetime +from sijapi.utilities import bool_convert, sanitize_filename, assemble_journal_path +from sijapi.routers.locate import localize_datetime from sijapi import DATA_DIR, SD_IMAGE_DIR, PUBLIC_KEY, OBSIDIAN_VAULT_DIR serve = APIRouter(tags=["public"]) @@ -50,7 +51,7 @@ def is_valid_date(date_str: str) -> bool: @serve.get("/notes/{file_path:path}") async def get_file(file_path: str): try: - date_time = localize_datetime(file_path); + date_time = await localize_datetime(file_path); absolute_path, local_path = assemble_journal_path(date_time, no_timestamp = True) except ValueError as e: DEBUG(f"Unable to parse {file_path} as a date, now trying to use it as a local path") diff --git a/sijapi/routers/summarize.py b/sijapi/routers/summarize.py deleted file mode 100644 index b00de5f..0000000 --- a/sijapi/routers/summarize.py +++ /dev/null @@ -1,211 +0,0 @@ -from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, UploadFile -from fastapi.responses import FileResponse -from pathlib import Path -import tempfile -import filetype -import shutil -import os -import re -from os.path import basename, splitext -from datetime import datetime -from typing import Optional, Union, List -from PyPDF2 import PdfReader -from pdfminer.high_level import extract_text as pdfminer_extract_text -import pytesseract -from pdf2image import convert_from_path -import asyncio -import html2text -import markdown -from ollama import Client, AsyncClient -from docx import Document - -from sijapi.routers.tts import generate_speech -from sijapi.routers.asr import transcribe_audio -from sijapi.utilities import 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, f -from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi import DEFAULT_VOICE, SUMMARY_INSTRUCT, SUMMARY_CHUNK_SIZE, SUMMARY_TPW, SUMMARY_CHUNK_OVERLAP, SUMMARY_LENGTH_RATIO, SUMMARY_TOKEN_LIMIT, SUMMARY_MIN_LENGTH, SUMMARY_MIN_LENGTH, SUMMARY_MODEL - -summarize = APIRouter(tags=["trusted", "private"]) - -@summarize.get("/summarize") -async def summarize_get(text: str = Form(None), instruction: str = Form(SUMMARY_INSTRUCT)): - summarized_text = await summarize_text(text, instruction) - return summarized_text - -@summarize.post("/summarize") -async def summarize_post(file: Optional[UploadFile] = File(None), text: Optional[str] = Form(None), instruction: str = Form(SUMMARY_INSTRUCT)): - text_content = text if text else await extract_text(file) - summarized_text = await summarize_text(text_content, instruction) - return summarized_text - -@summarize.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)): - - 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( - text: str, - instruction: str = SUMMARY_INSTRUCT, - voice: Optional[str] = DEFAULT_VOICE, - speed: float = 1.1, - podcast: bool = False, - LLM: AsyncClient = None -): - LLM = LLM if LLM else AsyncClient() - 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 = sanitize_filename(filename) - filename = ' '.join(filename.split()[:5]) - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{timestamp}{filename}.wav" - - background_tasks = BackgroundTasks() - final_output_path = await generate_speech(background_tasks, summarized_text, voice, "xtts", speed=speed, podcast=podcast, title=filename) - DEBUG(f"summary_tts completed with final_output_path: {final_output_path}") - return final_output_path - - - -async def get_title(text: str, LLM: AsyncClient() = None): - LLM = LLM if LLM else AsyncClient() - title = await process_chunk("Generate a title for this text", text, 1, 1, 12, LLM) - 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. - """ - 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 - chunks = [] - for i in range(0, len(words), adjusted_chunk_size - adjusted_overlap): - DEBUG(f"We are on iteration # {i} if split_text_into_chunks.") - chunk = ' '.join(words[i:i + adjusted_chunk_size]) - chunks.append(chunk) - return chunks - - -def calculate_max_tokens(text: str) -> int: - tokens_count = max(1, int(len(text.split()) * SUMMARY_TPW)) # Ensure at least 1 - return min(tokens_count // 4, SUMMARY_CHUNK_SIZE) - - -async def extract_text(file: Union[UploadFile, bytes, bytearray, str, Path], background_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") - - _, file_ext = os.path.splitext(file_path) - file_ext = file_ext.lower() - text_content = "" - - 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) - - if background_tasks and 'temp_file_path' in locals(): - background_tasks.add_task(os.remove, temp_file_path) - elif 'temp_file_path' in locals(): - os.remove(temp_file_path) - - return text_content - -async def summarize_text(text: str, instruction: str = SUMMARY_INSTRUCT, length_override: int = None, length_quotient: float = SUMMARY_LENGTH_RATIO, LLM: AsyncClient = 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 AsyncClient() - - chunked_text = split_text_into_chunks(text) - total_parts = max(1, len(chunked_text)) # Ensure at least 1 - - 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 - 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 - - 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) - ]) - - concatenated_summary = ' '.join(summaries) - - if total_parts > 1: - concatenated_summary = await process_chunk(instruction, concatenated_summary, 1, 1) - - return concatenated_summary - -async def process_chunk(instruction: str, text: str, part: int, total_parts: int, max_tokens: Optional[int] = None, LLM: AsyncClient = None) -> str: - """ - Process a portion of text using the ollama library asynchronously. - """ - - LLM = LLM if LLM else AsyncClient() - - 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 - - DEBUG(f"Summarizing part {part} of {total_parts}: Max_tokens: {max_tokens}") - - if part and total_parts > 1: - prompt = f"{instruction}. Part {part} of {total_parts}:\n{text}" - else: - prompt = f"{instruction}:\n\n{text}" - - DEBUG(f"Starting LLM.generate for part {part} of {total_parts}") - response = await LLM.generate( - model=SUMMARY_MODEL, - prompt=prompt, - stream=False, - options={'num_predict': max_tokens, 'temperature': 0.6} - ) - - text_response = response['response'] - DEBUG(f"Completed LLM.generate for part {part} of {total_parts}") - - return text_response - -async def title_and_summary(extracted_text: str): - title = await get_title(extracted_text) - processed_title = title.split("\n")[-1] - processed_title = processed_title.split("\r")[-1] - processed_title = sanitize_filename(processed_title) - summary = await summarize_text(extracted_text) - - return processed_title, summary \ No newline at end of file diff --git a/sijapi/routers/time.py b/sijapi/routers/time.py index ec2aa24..ccb936c 100644 --- a/sijapi/routers/time.py +++ b/sijapi/routers/time.py @@ -17,7 +17,6 @@ from fastapi import APIRouter, UploadFile, File, Response, Header, Query, Depend from fastapi.responses import FileResponse, JSONResponse from pydantic import BaseModel, Field from datetime import datetime, timedelta -from sijapi.utilities import localize_datetime from decimal import Decimal, ROUND_UP from typing import Optional, List, Dict, Union, Tuple from collections import defaultdict @@ -25,6 +24,7 @@ from dotenv import load_dotenv from traceback import format_exc from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL from sijapi import HOME_DIR, TIMING_API_KEY, TIMING_API_URL +from sijapi.routers.locate import localize_datetime ### INITIALIZATIONS ### time = APIRouter(tags=["private"]) @@ -100,8 +100,8 @@ def truncate_project_title(title): async def fetch_and_prepare_timing_data(start: datetime, end: Optional[datetime] = None) -> List[Dict]: - # start_date = localize_datetime(start) - # end_date = localize_datetime(end) if end else None + # start_date = await localize_datetime(start) + # end_date = await localize_datetime(end) if end else None # Adjust the start date to include the day before and format the end date start_date_adjusted = (start - timedelta(days=1)).strftime("%Y-%m-%dT00:00:00") end_date_formatted = f"{datetime.strftime(end, '%Y-%m-%d')}T23:59:59" if end else f"{datetime.strftime(start, '%Y-%m-%d')}T23:59:59" @@ -315,8 +315,8 @@ async def get_timing_markdown3( ): # Fetch and process timing data - start = localize_datetime(start_date) - end = localize_datetime(end_date) if end_date else None + start = await localize_datetime(start_date) + end = await localize_datetime(end_date) if end_date else None timing_data = await fetch_and_prepare_timing_data(start, end) # Retain these for processing Markdown data with the correct timezone @@ -375,8 +375,8 @@ async def get_timing_markdown( start: str = Query(..., regex=r"\d{4}-\d{2}-\d{2}"), end: Optional[str] = Query(None, regex=r"\d{4}-\d{2}-\d{2}") ): - start_date = localize_datetime(start) - end_date = localize_datetime(end) + start_date = await localize_datetime(start) + end_date = await localize_datetime(end) markdown_formatted_data = await process_timing_markdown(start_date, end_date) return Response(content=markdown_formatted_data, media_type="text/markdown") @@ -444,8 +444,8 @@ async def get_timing_json( ): # Fetch and process timing data - start = localize_datetime(start_date) - end = localize_datetime(end_date) + start = await localize_datetime(start_date) + end = await localize_datetime(end_date) timing_data = await fetch_and_prepare_timing_data(start, end) # Convert processed data to the required JSON structure diff --git a/sijapi/routers/tts.py b/sijapi/routers/tts.py index 3d89f6e..2eca2d2 100644 --- a/sijapi/routers/tts.py +++ b/sijapi/routers/tts.py @@ -273,7 +273,17 @@ async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None) return select_voice(DEFAULT_VOICE) -async def local_tts(text_content: str, speed: float, voice: str, voice_file = None, podcast: bool = False, background_tasks: BackgroundTasks = None, title: str = None, output_path: Optional[Path] = None) -> str: + +async def local_tts( + text_content: str, + speed: float, + voice: str, + voice_file = None, + podcast: bool = False, + background_tasks: BackgroundTasks = None, + title: str = None, + output_path: Optional[Path] = None +) -> str: if output_path: file_path = Path(output_path) else: @@ -286,27 +296,47 @@ async def local_tts(text_content: str, speed: float, voice: str, voice_file = No file_path.parent.mkdir(parents=True, exist_ok=True) voice_file_path = await get_voice_file_path(voice, voice_file) - XTTS = TTS(model_name=MODEL_NAME).to(DEVICE) + + # Initialize TTS model in a separate thread + XTTS = await asyncio.to_thread(TTS, model_name=MODEL_NAME) + await asyncio.to_thread(XTTS.to, DEVICE) + segments = split_text(text_content) combined_audio = AudioSegment.silent(duration=0) for i, segment in enumerate(segments): segment_file_path = TTS_SEGMENTS_DIR / f"segment_{i}.wav" DEBUG(f"Segment file path: {segment_file_path}") - segment_file = await asyncio.to_thread(XTTS.tts_to_file, text=segment, speed=speed, file_path=str(segment_file_path), speaker_wav=[voice_file_path], language="en") - DEBUG(f"Segment file generated: {segment_file}") - combined_audio += AudioSegment.from_wav(str(segment_file)) - # Delete the segment file immediately after adding it to the combined audio - segment_file_path.unlink() + + # Run TTS in a separate thread + await asyncio.to_thread( + XTTS.tts_to_file, + text=segment, + speed=speed, + file_path=str(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)) + combined_audio += segment_audio + # Delete the segment file + await asyncio.to_thread(segment_file_path.unlink) + + # Export the combined audio in a separate thread if podcast: podcast_file_path = PODCAST_DIR / file_path.name - combined_audio.export(podcast_file_path, format="wav") + await asyncio.to_thread(combined_audio.export, podcast_file_path, format="wav") + + await asyncio.to_thread(combined_audio.export, file_path, format="wav") - combined_audio.export(file_path, format="wav") return str(file_path) + async def stream_tts(text_content: str, speed: float, voice: str, voice_file) -> StreamingResponse: voice_file_path = await get_voice_file_path(voice, voice_file) segments = split_text(text_content) diff --git a/sijapi/routers/weather.py b/sijapi/routers/weather.py index fd6b0f2..a2a825f 100644 --- a/sijapi/routers/weather.py +++ b/sijapi/routers/weather.py @@ -7,10 +7,9 @@ from typing import Dict from datetime import datetime from shapely.wkb import loads from binascii import unhexlify -from sijapi.utilities import localize_datetime from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi import VISUALCROSSING_API_KEY, TZ -from sijapi.utilities import get_db_connection, haversine +from sijapi import VISUALCROSSING_API_KEY, TZ, DB +from sijapi.utilities import haversine from sijapi.routers import locate weather = APIRouter() @@ -25,7 +24,7 @@ async def get_weather(date_time: datetime, latitude: float, longitude: float): try: DEBUG(f"Daily weather data from db: {daily_weather_data}") last_updated = str(daily_weather_data['DailyWeather'].get('last_updated')) - last_updated = localize_datetime(last_updated) + last_updated = await locate.localize_datetime(last_updated) stored_loc_data = unhexlify(daily_weather_data['DailyWeather'].get('location')) stored_loc = loads(stored_loc_data) stored_lat = stored_loc.y @@ -84,182 +83,180 @@ async def get_weather(date_time: datetime, latitude: float, longitude: float): async def store_weather_to_db(date_time: datetime, weather_data: dict): - conn = await get_db_connection() + async with DB.get_connection() as conn: + try: + day_data = weather_data.get('days')[0] + DEBUG(f"day_data.get('sunrise'): {day_data.get('sunrise')}") + + # Handle preciptype and stations as PostgreSQL arrays + preciptype_array = day_data.get('preciptype', []) or [] + stations_array = day_data.get('stations', []) or [] + + date_str = date_time.strftime("%Y-%m-%d") + + # Get location details from weather data if available + longitude = weather_data.get('longitude') + latitude = weather_data.get('latitude') + elevation = locate.get_elevation(latitude, longitude) # 152.4 # default until we add a geocoder that can look up actual elevation; weather_data.get('elevation') # assuming 'elevation' key, replace if different + location_point = f"POINTZ({longitude} {latitude} {elevation})" if longitude and latitude and elevation else None + + # Correct for the datetime objects + day_data['datetime'] = await locate.localize_datetime(day_data.get('datetime')) #day_data.get('datetime')) + day_data['sunrise'] = day_data['datetime'].replace(hour=int(day_data.get('sunrise').split(':')[0]), minute=int(day_data.get('sunrise').split(':')[1])) + day_data['sunset'] = day_data['datetime'].replace(hour=int(day_data.get('sunset').split(':')[0]), minute=int(day_data.get('sunset').split(':')[1])) + + daily_weather_params = ( + day_data.get('sunrise'), day_data.get('sunriseEpoch'), + day_data.get('sunset'), day_data.get('sunsetEpoch'), + day_data.get('description'), day_data.get('tempmax'), + day_data.get('tempmin'), day_data.get('uvindex'), + day_data.get('winddir'), day_data.get('windspeed'), + day_data.get('icon'), datetime.now(), + day_data.get('datetime'), day_data.get('datetimeEpoch'), + day_data.get('temp'), day_data.get('feelslikemax'), + day_data.get('feelslikemin'), day_data.get('feelslike'), + day_data.get('dew'), day_data.get('humidity'), + day_data.get('precip'), day_data.get('precipprob'), + day_data.get('precipcover'), preciptype_array, + day_data.get('snow'), day_data.get('snowdepth'), + day_data.get('windgust'), day_data.get('pressure'), + day_data.get('cloudcover'), day_data.get('visibility'), + day_data.get('solarradiation'), day_data.get('solarenergy'), + day_data.get('severerisk', 0), day_data.get('moonphase'), + day_data.get('conditions'), stations_array, day_data.get('source'), + location_point + ) + except Exception as e: + ERR(f"Failed to prepare database query in store_weather_to_db! {e}") - try: - day_data = weather_data.get('days')[0] - DEBUG(f"day_data.get('sunrise'): {day_data.get('sunrise')}") - - # Handle preciptype and stations as PostgreSQL arrays - preciptype_array = day_data.get('preciptype', []) or [] - stations_array = day_data.get('stations', []) or [] - - date_str = date_time.strftime("%Y-%m-%d") - - # Get location details from weather data if available - longitude = weather_data.get('longitude') - latitude = weather_data.get('latitude') - elevation = locate.get_elevation(latitude, longitude) # 152.4 # default until we add a geocoder that can look up actual elevation; weather_data.get('elevation') # assuming 'elevation' key, replace if different - location_point = f"POINTZ({longitude} {latitude} {elevation})" if longitude and latitude and elevation else None - - # Correct for the datetime objects - day_data['datetime'] = localize_datetime(day_data.get('datetime')) #day_data.get('datetime')) - day_data['sunrise'] = day_data['datetime'].replace(hour=int(day_data.get('sunrise').split(':')[0]), minute=int(day_data.get('sunrise').split(':')[1])) - day_data['sunset'] = day_data['datetime'].replace(hour=int(day_data.get('sunset').split(':')[0]), minute=int(day_data.get('sunset').split(':')[1])) - - daily_weather_params = ( - day_data.get('sunrise'), day_data.get('sunriseEpoch'), - day_data.get('sunset'), day_data.get('sunsetEpoch'), - day_data.get('description'), day_data.get('tempmax'), - day_data.get('tempmin'), day_data.get('uvindex'), - day_data.get('winddir'), day_data.get('windspeed'), - day_data.get('icon'), datetime.now(), - day_data.get('datetime'), day_data.get('datetimeEpoch'), - day_data.get('temp'), day_data.get('feelslikemax'), - day_data.get('feelslikemin'), day_data.get('feelslike'), - day_data.get('dew'), day_data.get('humidity'), - day_data.get('precip'), day_data.get('precipprob'), - day_data.get('precipcover'), preciptype_array, - day_data.get('snow'), day_data.get('snowdepth'), - day_data.get('windgust'), day_data.get('pressure'), - day_data.get('cloudcover'), day_data.get('visibility'), - day_data.get('solarradiation'), day_data.get('solarenergy'), - day_data.get('severerisk', 0), day_data.get('moonphase'), - day_data.get('conditions'), stations_array, day_data.get('source'), - location_point - ) - except Exception as e: - ERR(f"Failed to prepare database query in store_weather_to_db! {e}") - - try: - daily_weather_query = ''' - INSERT INTO DailyWeather ( - sunrise, sunriseEpoch, sunset, sunsetEpoch, description, - tempmax, tempmin, uvindex, winddir, windspeed, icon, last_updated, - datetime, datetimeEpoch, temp, feelslikemax, feelslikemin, feelslike, - dew, humidity, precip, precipprob, precipcover, preciptype, - snow, snowdepth, windgust, pressure, cloudcover, visibility, - solarradiation, solarenergy, severerisk, moonphase, conditions, - stations, source, location - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38) - RETURNING id - ''' - - # Debug logs for better insights - # DEBUG("Executing query: %s", daily_weather_query) - # DEBUG("With parameters: %s", daily_weather_params) - - # Execute the query to insert daily weather data - async with conn.transaction(): - daily_weather_id = await conn.fetchval(daily_weather_query, *daily_weather_params) - - - if 'hours' in day_data: - for hour_data in day_data['hours']: - try: - await asyncio.sleep(0.1) - # hour_data['datetime'] = parse_date(hour_data.get('datetime')) - hour_timestamp = date_str + ' ' + hour_data['datetime'] - hour_data['datetime'] = localize_datetime(hour_timestamp) - DEBUG(f"Processing hours now...") - # DEBUG(f"Processing {hour_data['datetime']}") - - hour_preciptype_array = hour_data.get('preciptype', []) or [] - hour_stations_array = hour_data.get('stations', []) or [] - hourly_weather_params = ( - daily_weather_id, - hour_data['datetime'], - hour_data.get('datetimeEpoch'), - hour_data['temp'], - hour_data['feelslike'], - hour_data['humidity'], - hour_data['dew'], - hour_data['precip'], - hour_data['precipprob'], - hour_preciptype_array, - hour_data['snow'], - hour_data['snowdepth'], - hour_data['windgust'], - hour_data['windspeed'], - hour_data['winddir'], - hour_data['pressure'], - hour_data['cloudcover'], - hour_data['visibility'], - hour_data['solarradiation'], - hour_data['solarenergy'], - hour_data['uvindex'], - hour_data.get('severerisk', 0), - hour_data['conditions'], - hour_data['icon'], - hour_stations_array, - hour_data.get('source', ''), - ) + try: + daily_weather_query = ''' + INSERT INTO DailyWeather ( + sunrise, sunriseEpoch, sunset, sunsetEpoch, description, + tempmax, tempmin, uvindex, winddir, windspeed, icon, last_updated, + datetime, datetimeEpoch, temp, feelslikemax, feelslikemin, feelslike, + dew, humidity, precip, precipprob, precipcover, preciptype, + snow, snowdepth, windgust, pressure, cloudcover, visibility, + solarradiation, solarenergy, severerisk, moonphase, conditions, + stations, source, location + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38) + RETURNING id + ''' + + # Debug logs for better insights + # DEBUG("Executing query: %s", daily_weather_query) + # DEBUG("With parameters: %s", daily_weather_params) + # Execute the query to insert daily weather data + async with conn.transaction(): + daily_weather_id = await conn.fetchval(daily_weather_query, *daily_weather_params) + + + if 'hours' in day_data: + for hour_data in day_data['hours']: try: - hourly_weather_query = ''' - INSERT INTO HourlyWeather (daily_weather_id, datetime, datetimeEpoch, temp, feelslike, humidity, dew, precip, precipprob, - preciptype, snow, snowdepth, windgust, windspeed, winddir, pressure, cloudcover, visibility, solarradiation, solarenergy, - uvindex, severerisk, conditions, icon, stations, source) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26) - RETURNING id - ''' - # Debug logs for better insights - # DEBUG("Executing query: %s", hourly_weather_query) - # DEBUG("With parameters: %s", hourly_weather_params) + await asyncio.sleep(0.1) + # hour_data['datetime'] = parse_date(hour_data.get('datetime')) + hour_timestamp = date_str + ' ' + hour_data['datetime'] + hour_data['datetime'] = await locate.localize_datetime(hour_timestamp) + DEBUG(f"Processing hours now...") + # DEBUG(f"Processing {hour_data['datetime']}") + + hour_preciptype_array = hour_data.get('preciptype', []) or [] + hour_stations_array = hour_data.get('stations', []) or [] + hourly_weather_params = ( + daily_weather_id, + hour_data['datetime'], + hour_data.get('datetimeEpoch'), + hour_data['temp'], + hour_data['feelslike'], + hour_data['humidity'], + hour_data['dew'], + hour_data['precip'], + hour_data['precipprob'], + hour_preciptype_array, + hour_data['snow'], + hour_data['snowdepth'], + hour_data['windgust'], + hour_data['windspeed'], + hour_data['winddir'], + hour_data['pressure'], + hour_data['cloudcover'], + hour_data['visibility'], + hour_data['solarradiation'], + hour_data['solarenergy'], + hour_data['uvindex'], + hour_data.get('severerisk', 0), + hour_data['conditions'], + hour_data['icon'], + hour_stations_array, + hour_data.get('source', ''), + ) + + try: + hourly_weather_query = ''' + INSERT INTO HourlyWeather (daily_weather_id, datetime, datetimeEpoch, temp, feelslike, humidity, dew, precip, precipprob, + preciptype, snow, snowdepth, windgust, windspeed, winddir, pressure, cloudcover, visibility, solarradiation, solarenergy, + uvindex, severerisk, conditions, icon, stations, source) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26) + RETURNING id + ''' + # Debug logs for better insights + # DEBUG("Executing query: %s", hourly_weather_query) + # DEBUG("With parameters: %s", hourly_weather_params) + + # Execute the query to insert hourly weather data + async with conn.transaction(): + hourly_weather_id = await conn.fetchval(hourly_weather_query, *hourly_weather_params) + # ERR(f"\n{hourly_weather_id}") + + except Exception as e: + ERR(f"EXCEPTION: {e}") - # Execute the query to insert hourly weather data - async with conn.transaction(): - hourly_weather_id = await conn.fetchval(hourly_weather_query, *hourly_weather_params) - # ERR(f"\n{hourly_weather_id}") - except Exception as e: ERR(f"EXCEPTION: {e}") - except Exception as e: - ERR(f"EXCEPTION: {e}") - - return "SUCCESS" - - except Exception as e: - ERR(f"Error in dailyweather storage: {e}") + return "SUCCESS" + + except Exception as e: + ERR(f"Error in dailyweather storage: {e}") async def get_weather_from_db(date_time: datetime, latitude: float, longitude: float): - conn = await get_db_connection() + async with DB.get_connection() as conn: + query_date = date_time.date() + try: + # Query to get daily weather data + query = ''' + SELECT DW.* FROM DailyWeather DW + WHERE DW.datetime::date = $1 + AND ST_DWithin(DW.location::geography, ST_MakePoint($2,$3)::geography, 8046.72) + ORDER BY ST_Distance(DW.location, ST_MakePoint($4, $5)::geography) ASC + LIMIT 1 + ''' - query_date = date_time.date() - try: - # Query to get daily weather data - query = ''' - SELECT DW.* FROM DailyWeather DW - WHERE DW.datetime::date = $1 - AND ST_DWithin(DW.location::geography, ST_MakePoint($2,$3)::geography, 8046.72) - ORDER BY ST_Distance(DW.location, ST_MakePoint($4, $5)::geography) ASC - LIMIT 1 - ''' + daily_weather_data = await conn.fetchrow(query, query_date, longitude, latitude, longitude, latitude) - daily_weather_data = await conn.fetchrow(query, query_date, longitude, latitude, longitude, latitude) + if daily_weather_data is None: + DEBUG(f"No daily weather data retrieved from database.") + return None + # else: + # DEBUG(f"Daily_weather_data: {daily_weather_data}") + # Query to get hourly weather data + query = ''' + SELECT HW.* FROM HourlyWeather HW + WHERE HW.daily_weather_id = $1 + ''' + hourly_weather_data = await conn.fetch(query, daily_weather_data['id']) - if daily_weather_data is None: - DEBUG(f"No daily weather data retrieved from database.") - return None - # else: - # DEBUG(f"Daily_weather_data: {daily_weather_data}") - # Query to get hourly weather data - query = ''' - SELECT HW.* FROM HourlyWeather HW - WHERE HW.daily_weather_id = $1 - ''' - hourly_weather_data = await conn.fetch(query, daily_weather_data['id']) - - day: Dict = { - 'DailyWeather': dict(daily_weather_data), - 'HourlyWeather': [dict(row) for row in hourly_weather_data], - } - # DEBUG(f"day: {day}") - return day - except Exception as e: - ERR(f"Unexpected error occurred: {e}") + day: Dict = { + 'DailyWeather': dict(daily_weather_data), + 'HourlyWeather': [dict(row) for row in hourly_weather_data], + } + # DEBUG(f"day: {day}") + return day + except Exception as e: + ERR(f"Unexpected error occurred: {e}") diff --git a/sijapi/utilities.py b/sijapi/utilities.py index 83b48ec..a89fae0 100644 --- a/sijapi/utilities.py +++ b/sijapi/utilities.py @@ -17,6 +17,8 @@ from datetime import datetime, date, time from typing import Optional, Union, Tuple import asyncio from PIL import Image +import pandas as pd +from scipy.spatial import cKDTree from dateutil.parser import parse as dateutil_parse from docx import Document import asyncpg @@ -24,7 +26,7 @@ from sshtunnel import SSHTunnelForwarder from fastapi import Depends, HTTPException, Request, UploadFile from fastapi.security.api_key import APIKeyHeader from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL -from sijapi import DB, GLOBAL_API_KEY, DB, DB_HOST, DB_PORT, DB_USER, DB_PASS, TZ, YEAR_FMT, MONTH_FMT, DAY_FMT, DAY_SHORT_FMT, OBSIDIAN_VAULT_DIR, ALLOWED_FILENAME_CHARS, MAX_FILENAME_LENGTH +from sijapi import GLOBAL_API_KEY, YEAR_FMT, MONTH_FMT, DAY_FMT, DAY_SHORT_FMT, OBSIDIAN_VAULT_DIR, ALLOWED_FILENAME_CHARS, MAX_FILENAME_LENGTH api_key_header = APIKeyHeader(name="Authorization") @@ -141,64 +143,38 @@ def sanitize_filename(text, max_length=MAX_FILENAME_LENGTH): """Sanitize a string to be used as a safe filename while protecting the file extension.""" DEBUG(f"Filename before sanitization: {text}") - # Replace multiple spaces with a single space and remove other whitespace text = re.sub(r'\s+', ' ', text) - - # Remove any non-word characters except space, dot, and hyphen sanitized = re.sub(ALLOWED_FILENAME_CHARS, '', text) - - # Remove leading/trailing spaces sanitized = sanitized.strip() - - # Split the filename into base name and extension base_name, extension = os.path.splitext(sanitized) - # Calculate the maximum length for the base name max_base_length = max_length - len(extension) - - # Truncate the base name if necessary if len(base_name) > max_base_length: base_name = base_name[:max_base_length].rstrip() - - # Recombine the base name and extension final_filename = base_name + extension - # In case the extension itself is too long, truncate the entire filename - if len(final_filename) > max_length: - final_filename = final_filename[:max_length] - DEBUG(f"Filename after sanitization: {final_filename}") return final_filename - def check_file_name(file_name, max_length=255): """Check if the file name needs sanitization based on the criteria of the second sanitize_filename function.""" - DEBUG(f"Checking filename: {file_name}") needs_sanitization = False - # Check for length if len(file_name) > max_length: - DEBUG(f"Filename exceeds maximum length of {max_length}") + DEBUG(f"Filename exceeds maximum length of {max_length}: {file_name}") needs_sanitization = True - - # Check for non-word characters (except space, dot, and hyphen) if re.search(ALLOWED_FILENAME_CHARS, file_name): - DEBUG("Filename contains non-word characters (except space, dot, and hyphen)") + DEBUG(f"Filename contains non-word characters (except space, dot, and hyphen): {file_name}") needs_sanitization = True - - # Check for multiple consecutive spaces if re.search(r'\s{2,}', file_name): - DEBUG("Filename contains multiple consecutive spaces") + DEBUG(f"Filename contains multiple consecutive spaces: {file_name}") needs_sanitization = True - - # Check for leading/trailing spaces if file_name != file_name.strip(): - DEBUG("Filename has leading or trailing spaces") + DEBUG(f"Filename has leading or trailing spaces: {file_name}") needs_sanitization = True - DEBUG(f"Filename {'needs' if needs_sanitization else 'does not need'} sanitization") return needs_sanitization @@ -381,49 +357,6 @@ def convert_to_unix_time(iso_date_str): return int(dt.timestamp()) -async def get_db_connection(): - conn = await asyncpg.connect( - database=DB, - user=DB_USER, - password=DB_PASS, - host=DB_HOST, - port=DB_PORT - ) - return conn - -temp = """ -def get_db_connection_ssh(ssh: bool = True): - if ssh: - with SSHTunnelForwarder( - (DB_SSH, 22), - DB_SSH_USER=DB_SSH_USER, - DB_SSH_PASS=DB_SSH_PASS, - remote_bind_address=DB_SSH, - local_bind_address=(DB_HOST, DB_PORT) - ) as tunnel: conn = psycopg2.connect( - dbname=DB, - user=DB_USER, - password=DB_PASS, - host=DB_HOST, - port=DB_PORT - ) - else: - conn = psycopg2.connect( - dbname=DB, - user=DB_USER, - password=DB_PASS, - host=DB_HOST, - port=DB_PORT - ) - - return conn -""" - -def db_localized(): - # ssh = True if TS_IP == DB_SSH else False - return get_db_connection() - - def haversine(lat1, lon1, lat2, lon2): """ Calculate the great circle distance between two points on the earth specified in decimal degrees. """ lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) @@ -445,30 +378,6 @@ def convert_degrees_to_cardinal(d): return dirs[ix % len(dirs)] -def localize_datetime(dt): - initial_dt = dt - try: - if isinstance(dt, str): - dt = dateutil_parse(dt) - DEBUG(f"{initial_dt} was a string so we attempted converting to datetime. Result: {dt}") - - - if isinstance(dt, datetime): - DEBUG(f"{dt} is a datetime object, so we will ensure it is tz-aware.") - if dt.tzinfo is None: - dt = dt.replace(tzinfo=TZ) - # DEBUG(f"{dt} should now be tz-aware. Returning it now.") - return dt - else: - # DEBUG(f"{dt} already was tz-aware. Returning it now.") - return dt - else: - ERR(f"Conversion failed") - raise TypeError("Conversion failed") - except Exception as e: - ERR(f"Error parsing datetime: {e}") - raise TypeError("Input must be a string or datetime object") - HOURLY_COLUMNS_MAPPING = { "12am": "00:00:00", @@ -531,4 +440,22 @@ def resize_and_convert_image(image_path, max_size=2160, quality=80): img.save(img_byte_arr, format='JPEG', quality=quality) img_byte_arr = img_byte_arr.getvalue() - return img_byte_arr \ No newline at end of file + return img_byte_arr + + +def load_geonames_data(path: str): + columns = ['geonameid', 'name', 'asciiname', 'alternatenames', + 'latitude', 'longitude', 'feature_class', 'feature_code', + 'country_code', 'cc2', 'admin1_code', 'admin2_code', 'admin3_code', + 'admin4_code', 'population', 'elevation', 'dem', 'timezone', 'modification_date'] + + data = pd.read_csv( + path, + sep='\t', + header=None, + names=columns, + low_memory=False + ) + + return data +