Auto-update: Tue Jun 25 16:59:10 PDT 2024

This commit is contained in:
sanj 2024-06-25 16:59:10 -07:00
parent 5ebde8faa6
commit cd6ba72022
26 changed files with 1944 additions and 2467 deletions

View file

@ -12,7 +12,7 @@ from typing import List, Optional
import traceback import traceback
import logging import logging
from .logs import Logger 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 # from sijapi.config.config import load_config
# cfg = load_config() # cfg = load_config()
@ -43,6 +43,7 @@ os.makedirs(LOGS_DIR, exist_ok=True)
load_dotenv(ENV_PATH) load_dotenv(ENV_PATH)
### API essentials ### API essentials
DB = Database.from_env()
ROUTERS = os.getenv('ROUTERS', '').split(',') ROUTERS = os.getenv('ROUTERS', '').split(',')
PUBLIC_SERVICES = os.getenv('PUBLIC_SERVICES', '').split(',') PUBLIC_SERVICES = os.getenv('PUBLIC_SERVICES', '').split(',')
GLOBAL_API_KEY = os.getenv("GLOBAL_API_KEY") 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" 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 ### LOCATE AND WEATHER LOCALIZATIONS
USER_FULLNAME = os.getenv('USER_FULLNAME') USER_FULLNAME = os.getenv('USER_FULLNAME')
USER_BIO = os.getenv('USER_BIO') USER_BIO = os.getenv('USER_BIO')
TZ = tz.gettz(os.getenv("TZ", "America/Los_Angeles"))
HOME_ZIP = os.getenv("HOME_ZIP") # unimplemented HOME_ZIP = os.getenv("HOME_ZIP") # unimplemented
LOCATION_OVERRIDES = DATA_DIR / "loc_overrides.json" NAMED_LOCATIONS = CONFIG_DIR / "named-locations.yaml"
LOCATIONS_CSV = DATA_DIR / "US.csv"
# DB = DATA_DIR / "weatherlocate.db" # deprecated # DB = DATA_DIR / "weatherlocate.db" # deprecated
VISUALCROSSING_BASE_URL = os.getenv("VISUALCROSSING_BASE_URL", "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline") VISUALCROSSING_BASE_URL = os.getenv("VISUALCROSSING_BASE_URL", "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline")
VISUALCROSSING_API_KEY = os.getenv("VISUALCROSSING_API_KEY") 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 ### Obsidian & notes
ALLOWED_FILENAME_CHARS = r'[^\w \.-]' 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_DIR = Path(os.getenv('COMFYUI_DIR'))
COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output' COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output'
COMFYUI_LAUNCH_CMD = os.getenv('COMFYUI_LAUNCH_CMD', 'mamba activate comfyui && python main.py') 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 ### Summarization
SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens 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) os.makedirs(TTS_DIR, exist_ok=True)
VOICE_DIR = TTS_DIR / 'voices' VOICE_DIR = TTS_DIR / 'voices'
os.makedirs(VOICE_DIR, exist_ok=True) 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) os.makedirs(PODCAST_DIR, exist_ok=True)
TTS_OUTPUT_DIR = TTS_DIR / 'outputs' TTS_OUTPUT_DIR = TTS_DIR / 'outputs'
os.makedirs(TTS_OUTPUT_DIR, exist_ok=True) 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? ICS_PATH = DATA_DIR / 'calendar.ics' # deprecated now, but maybe revive?
ICALENDARS = os.getenv('ICALENDARS', 'NULL,VOID').split(',') 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_CONFIG = CONFIG_DIR / "email.yaml"
EMAIL_ACCOUNTS = load_email_accounts(EMAIL_CONFIG)
AUTORESPOND = True AUTORESPOND = True
### Courtlistener & other webhooks ### Courtlistener & other webhooks

View file

@ -1,6 +1,65 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional, Any from typing import List, Optional, Any, Tuple, Dict, Union, Tuple
from datetime import datetime 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): class AutoResponder(BaseModel):
name: str name: str
@ -8,7 +67,7 @@ class AutoResponder(BaseModel):
context: str context: str
whitelist: List[str] whitelist: List[str]
blacklist: List[str] blacklist: List[str]
img_gen_prompt: Optional[str] = None image_prompt: Optional[str] = None
class IMAPConfig(BaseModel): class IMAPConfig(BaseModel):
username: str username: str
@ -26,20 +85,131 @@ class SMTPConfig(BaseModel):
class EmailAccount(BaseModel): class EmailAccount(BaseModel):
name: str name: str
refresh: int
fullname: Optional[str] fullname: Optional[str]
bio: Optional[str] bio: Optional[str]
summarize: bool = False
podcast: bool = False
imap: IMAPConfig imap: IMAPConfig
smtp: SMTPConfig smtp: SMTPConfig
autoresponders: Optional[List[AutoResponder]] autoresponders: Optional[List[AutoResponder]]
class EmailContact(BaseModel): class EmailContact(BaseModel):
email: str email: str
name: str name: Optional[str] = None
class IncomingEmail(BaseModel): class IncomingEmail(BaseModel):
sender: str sender: str
recipients: List[EmailContact]
datetime_received: datetime datetime_received: datetime
recipients: List[EmailContact]
subject: str subject: str
body: str body: str
attachments: Optional[List[Any]] = None 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

View file

@ -96,7 +96,7 @@ TRUSTED_SUBNETS=127.0.0.1/32,10.13.37.0/24,100.64.64.0/24
# ────────── # ──────────
# #
#─── router selection: ──────────────────────────────────────────────────────────── #─── 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 UNLOADED=ig
#─── notes: ────────────────────────────────────────────────────────────────────── #─── notes: ──────────────────────────────────────────────────────────────────────
# #
@ -218,18 +218,18 @@ TAILSCALE_API_KEY=¿SECRET? # <--- enter your own TS API key
# ░░ ░ ░T̷ O̷ G̷ E̷ T̷ H̷ ░ R̷. ░ ░ ░ ░ ░ # ░░ ░ ░T̷ O̷ G̷ E̷ T̷ H̷ ░ R̷. ░ ░ ░ ░ ░
# #
#─── frag, or weat,and locate modules:── . #─── frag, or weat,and locate modules:── .
DB=db DB_NAME=db
# #
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=5432 DB_PORT=5432
# R E A L T I G H T. # R E A L T I G H T.
DB_USER=postgres DB_USER=postgres
DB_PASS=¿SECRET? # <--- enter your own Postgres password' DB_PASSWORD=¿SECRET? # <--- enter your own Postgres password'
# Y E A H . . . # Y E A H . . .
DB_SSH=100.64.64.15 DB_SSH=100.64.64.15
# . . . 𝙹 𝚄 𝚂 𝚃 𝙻 𝙸 𝙺 𝙴 𝚃 𝙷 𝙰 𝚃. # . . . 𝙹 𝚄 𝚂 𝚃 𝙻 𝙸 𝙺 𝙴 𝚃 𝙷 𝙰 𝚃.
DB_SSH_USER=sij 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 ? 𝕰 𝖅 - 𝕻 𝖅 #─── notes: ────────────────────────────────────────────────── S E E ? 𝕰 𝖅 - 𝕻 𝖅
# #
# DB, DB_HOST, DB_PORT, DB_USER, and DB_PASS should specify those respective # DB, DB_HOST, DB_PORT, DB_USER, and DB_PASS should specify those respective

View file

@ -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@'

View file

@ -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|>",
"</s>"
],
"suffix": "</s><|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]",
"</s>"
],
"suffix": "[/INST]\n",
"sysPrefix": "",
"sysSuffix": "\n<s>"
},
"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": [
"</s>",
"USER:",
"SYSTEM:"
],
"suffix": "</s>\nASSISTANT: ",
"sysPrefix": "SYSTEM: ",
"sysSuffix": "\n"
},
"Zephyr": {
"models": [
"zephyr"
],
"prefix": " ",
"stops": [
"</s>"
],
"suffix": "</s>\n ",
"sysPrefix": " ",
"sysSuffix": "</s>\n"
},
"default": {
"prefix": "\n### Instruction:\n",
"stops": [
"### Instruction"
],
"suffix": "\n### Response:\n",
"sysPrefix": "### System\n",
"sysSuffix": "\n"
}
}

View file

@ -3,19 +3,19 @@
{ {
"scene": "default", "scene": "default",
"triggers": [""], "triggers": [""],
"API_PPrompt": "(Highly-detailed) image of ", "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_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": "`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_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_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: ", "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", "scene": "wallpaper",
"triggers": ["wallpaper"], "triggers": ["wallpaper"],
"API_PPrompt": "Stunning widescreen image of ", "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_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": "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_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_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.", "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"}] "workflows": [{"workflow": "wallpaper.json", "size": "1024x640"}]
@ -29,7 +29,7 @@
], ],
"API_PPrompt": "Highly-detailed portrait photo of ", "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_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_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: ", "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: ",
"workflows": [ "workflows": [
@ -38,55 +38,6 @@
"size": "768x1024" "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"
}
]
} }
] ]
} }

View file

@ -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

View file

@ -1,8 +0,0 @@
[
{
"name": "Echo Valley Ranch",
"latitude": 42.8098216,
"longitude": -123.049396,
"radius": 1.5
}
]

View file

@ -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 🦚"
}
}
}

461
sijapi/data/sd/workflows/default.json Executable file → Normal file
View file

@ -1,298 +1,281 @@
{ {
"10": { "4": {
"_meta": {
"title": "Power KSampler Advanced 🦚"
},
"class_type": "Power KSampler Advanced (PPF Noise)",
"inputs": { "inputs": {
"add_noise": "enable", "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors"
"alpha_exponent": 1, },
"boost_leading_sigma": "false", "class_type": "CheckpointLoaderSimple",
"cfg": 4.5, "_meta": {
"ch_settings": [ "title": "Load Checkpoint"
"12", }
},
"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 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, "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", "guide_use_noise": "true",
"latent_image": [
"13",
0
],
"model": [ "model": [
"4", "4",
0 0
], ],
"modulator": 1, "positive": [
"20",
0
],
"negative": [ "negative": [
"7", "7",
0 0
], ],
"noise_blending": "hslerp", "latent_image": [
"noise_mode": "additive", "11",
"noise_type": "vanilla_comfy", 0
"positive": [ ],
"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", "6",
0 0
], ],
"ppf_settings": [ "conditioning_2": [
"11", "21",
0 0
], ]
"return_with_leftover_noise": "disable", },
"sampler_name": "dpmpp_2m_sde", "class_type": "ConditioningCombine",
"scale": 1, "_meta": {
"scheduler": "karras", "title": "Conditioning (Combine)"
"seed": 301923985151711,
"sigma_tolerance": 0.5,
"start_at_step": 0,
"steps": 20
} }
}, },
"11": { "21": {
"_meta": {
"title": "Perlin Power Fractal Settings 🦚"
},
"class_type": "Perlin Power Fractal Settings (PPF Noise)",
"inputs": { "inputs": {
"X": 0, "text": "API_SPrompt",
"Y": 0, "clip": [
"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": [
"4", "4",
2 1
], ]
"persistence": 1.5, },
"ppf_settings": [ "class_type": "CLIPTextEncode",
"11", "_meta": {
0 "title": "CLIP Text Encode (Prompt)"
],
"resampling": "nearest-exact",
"scale": 5,
"seed": 961984691493347,
"width": 1023
} }
}, },
"23": { "23": {
"_meta": {
"title": "Ultimate SD Upscale"
},
"class_type": "UltimateSDUpscale",
"inputs": { "inputs": {
"cfg": 7.5, "conditioning": [
"denoise": 0.32, "7",
"force_uniform_tiles": true,
"image": [
"8",
0 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": { "24": {
"_meta": {
"title": "Load Checkpoint"
},
"class_type": "CheckpointLoaderSimple",
"inputs": { "inputs": {
"ckpt_name": "SD1.5/realisticVisionV60B1_v51VAE.safetensors" "model_name": "RealESRGAN_x4plus.pth"
}
},
"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"
}, },
"class_type": "UpscaleModelLoader", "class_type": "UpscaleModelLoader",
"inputs": { "_meta": {
"model_name": "4x-UltraSharp.pth" "title": "Load Upscale Model"
} }
}, },
"34": { "26": {
"_meta": {
"title": "Save Image"
},
"class_type": "SaveImage",
"inputs": { "inputs": {
"filename_prefix": "API_", "upscale_model": [
"images": [ "24",
"23", 0
],
"image": [
"38",
0 0
] ]
},
"class_type": "ImageUpscaleWithModel",
"_meta": {
"title": "Upscale Image (using Model)"
} }
}, },
"36": { "27": {
"_meta": {
"title": "Save Image"
},
"class_type": "SaveImage",
"inputs": { "inputs": {
"filename_prefix": "Pre_", "factor": 0.5,
"images": [ "interpolation_mode": "bicubic",
"8", "image": [
"30",
0 0
] ]
},
"class_type": "JWImageResizeByFactor",
"_meta": {
"title": "Image Resize by Factor"
} }
}, },
"4": { "30": {
"_meta": {
"title": "Load Checkpoint"
},
"class_type": "CheckpointLoaderSimple",
"inputs": { "inputs": {
"ckpt_name": "Other/playgroundv2.safetensors" "blur_radius": 3,
"sigma": 1.5,
"image": [
"26",
0
]
},
"class_type": "ImageBlur",
"_meta": {
"title": "ImageBlur"
} }
}, },
"6": { "38": {
"_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",
"inputs": { "inputs": {
"samples": [ "samples": [
"10", "13",
0 0
], ],
"vae": [ "vae": [
"4", "4",
2 2
] ]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
} }
} }
} }

709
sijapi/data/sd/workflows/landscape.json Executable file → Normal file
View file

@ -1,456 +1,347 @@
{ {
"11": { "4": {
"inputs": {
"ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": { "_meta": {
"title": "CLIP Text Encode (Prompt)" "title": "Load Checkpoint"
}
},
"6": {
"inputs": {
"text": "API_PPrompt",
"clip": [
"4",
1
]
}, },
"class_type": "CLIPTextEncode", "class_type": "CLIPTextEncode",
"inputs": { "_meta": {
"clip": [ "title": "CLIP Text Encode (Prompt)"
"12",
1
],
"text": [
"25",
0
]
} }
}, },
"12": { "7": {
"_meta": {
"title": "Load LoRA"
},
"class_type": "LoraLoader",
"inputs": { "inputs": {
"text": "API_NPrompt",
"clip": [ "clip": [
"4", "4",
1 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": { "9": {
"_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",
"inputs": { "inputs": {
"filename_prefix": "API_", "filename_prefix": "API_",
"images": [ "images": [
"52", "27",
0 0
] ]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
} }
}, },
"4": { "11": {
"_meta": {
"title": "Load Checkpoint"
},
"class_type": "CheckpointLoaderSimple",
"inputs": { "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": { "13": {
"_meta": {
"title": "Upscale Model Loader"
},
"class_type": "Upscale Model Loader",
"inputs": { "inputs": {
"model_name": "RealESRGAN_x2plus.pth" "seed": 484066073734968,
} "steps": 8,
}, "cfg": 1.8,
"52": { "sampler_name": "dpmpp_2m_sde",
"_meta": { "scheduler": "karras",
"title": "Ultimate SD Upscale" "start_at_step": 0,
}, "end_at_step": 10000,
"class_type": "UltimateSDUpscale", "enable_denoise": "false",
"inputs": { "denoise": 1,
"cfg": 8, "add_noise": "enable",
"denoise": 0.24, "return_with_leftover_noise": "enable",
"force_uniform_tiles": true, "noise_type": "brownian_fractal",
"image": [ "noise_blending": "cuberp",
"28", "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 0
], ],
"mask_blur": 8, "positive": [
"mode_type": "Linear", "20",
"model": [
"12",
0 0
], ],
"negative": [ "negative": [
"7", "7",
0 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", "21",
0 0
], ]
"sampler_name": "dpmpp_2m_sde", },
"scheduler": "karras", "class_type": "ConditioningCombine",
"seam_fix_denoise": 1, "_meta": {
"seam_fix_mask_blur": 8, "title": "Conditioning (Combine)"
"seam_fix_mode": "None", }
"seam_fix_padding": 16, },
"seam_fix_width": 64, "21": {
"seed": 1041855229054013, "inputs": {
"steps": 16, "text": "API_SPrompt",
"tile_height": [ "clip": [
"53", "4",
0 1
], ]
"tile_padding": 32, },
"tile_width": [ "class_type": "CLIPTextEncode",
"53", "_meta": {
0 "title": "CLIP Text Encode (Prompt)"
], }
"tiled_decode": true, },
"22": {
"inputs": {
"upscale_by": 2, "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": [ "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 0
], ],
"vae": [ "vae": [
"4", "4",
2 2
] ]
}
},
"53": {
"_meta": {
"title": "Integer"
}, },
"class_type": "JWInteger", "class_type": "VAEDecode",
"inputs": {
"value": 768
}
},
"54": {
"_meta": { "_meta": {
"title": "AnyAspectRatio" "title": "VAE Decode"
},
"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
]
} }
} }
} }

View file

@ -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)"
}
}
}

View file

@ -1,130 +1,174 @@
{ {
"11": { "4": {
"_meta": {
"title": "Perlin Power Fractal Noise 🦚"
},
"class_type": "Perlin Power Fractal Latent (PPF Noise)",
"inputs": { "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, "X": 0,
"Y": 0, "Y": 0,
"Z": 0, "Z": 0,
"batch_size": 1, "evolution": 0.1,
"brightness": 0, "frame": 1,
"clamp_max": 1, "scale": 13.1,
"clamp_min": 0,
"contrast": 0,
"device": "cpu",
"evolution": 0,
"exponent": 4,
"frame": 0,
"height": 1025,
"lacunarity": 2,
"octaves": 8, "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": [ "optional_vae": [
"4", "4",
2 2
], ]
"persistence": 1.5, },
"resampling": "nearest-exact", "class_type": "Perlin Power Fractal Latent (PPF Noise)",
"scale": 5, "_meta": {
"seed": 490162938389882, "title": "Perlin Power Fractal Noise 🦚"
"width": 1023
} }
}, },
"13": { "13": {
"_meta": {
"title": "Power KSampler Advanced 🦚"
},
"class_type": "Power KSampler Advanced (PPF Noise)",
"inputs": { "inputs": {
"add_noise": "enable", "seed": 484066073734968,
"alpha_exponent": 1, "steps": 8,
"boost_leading_sigma": "false", "cfg": 1.8,
"cfg": 1.6, "sampler_name": "dpmpp_2m_sde",
"ch_settings": [ "scheduler": "karras",
"15", "start_at_step": 0,
0
],
"denoise": 1,
"enable_denoise": "false",
"end_at_step": 10000, "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", "guide_use_noise": "true",
"latent_image": [
"11",
0
],
"model": [ "model": [
"4", "4",
0 0
], ],
"modulator": 1, "positive": [
"20",
0
],
"negative": [ "negative": [
"7", "7",
0 0
], ],
"noise_blending": "cuberp", "latent_image": [
"noise_mode": "additive", "11",
"noise_type": "brownian_fractal",
"positive": [
"20",
0 0
], ],
"ppf_settings": [ "ppf_settings": [
"14", "14",
0 0
], ],
"return_with_leftover_noise": "disable", "ch_settings": [
"sampler_name": "dpmpp_2m_sde", "15",
"scale": 1, 0
"scheduler": "karras", ]
"seed": 697312143874418, },
"sigma_tolerance": 0.5, "class_type": "Power KSampler Advanced (PPF Noise)",
"start_at_step": 0, "_meta": {
"steps": 8 "title": "Power KSampler Advanced 🦚"
} }
}, },
"14": { "14": {
"_meta": {
"title": "Perlin Power Fractal Settings 🦚"
},
"class_type": "Perlin Power Fractal Settings (PPF Noise)",
"inputs": { "inputs": {
"X": 0, "X": 0,
"Y": 0, "Y": 0,
"Z": 0, "Z": 0,
"brightness": 0,
"contrast": 0,
"evolution": 0, "evolution": 0,
"exponent": 4,
"frame": 0, "frame": 0,
"lacunarity": 2, "scale": 5,
"octaves": 8, "octaves": 5,
"persistence": 1.5, "persistence": 4,
"scale": 5 "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": { "15": {
"_meta": { "inputs": {
"title": "Cross-Hatch Power Fractal Settings 🦚" "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)", "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)",
"inputs": { "_meta": {
"angle_degrees": 45, "title": "Cross-Hatch Power Fractal Settings 🦚"
"blur": 2.5,
"brightness": 0,
"color_tolerance": 0.05,
"contrast": 0,
"frequency": 320,
"num_colors": 16,
"octaves": 12,
"persistence": 1.5
} }
}, },
"20": { "20": {
"_meta": {
"title": "Conditioning (Combine)"
},
"class_type": "ConditioningCombine",
"inputs": { "inputs": {
"conditioning_1": [ "conditioning_1": [
"6", "6",
@ -134,177 +178,157 @@
"21", "21",
0 0
] ]
},
"class_type": "ConditioningCombine",
"_meta": {
"title": "Conditioning (Combine)"
} }
}, },
"21": { "21": {
"_meta": {
"title": "CLIP Text Encode (Prompt)"
},
"class_type": "CLIPTextEncode",
"inputs": { "inputs": {
"text": "API_SPrompt",
"clip": [ "clip": [
"4", "4",
1 1
], ]
"text": "API_SPrompt" },
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
} }
}, },
"22": { "22": {
"_meta": {
"title": "Ultimate SD Upscale"
},
"class_type": "UltimateSDUpscale",
"inputs": { "inputs": {
"cfg": 8, "upscale_by": 2,
"seed": 589846903558615,
"steps": 20,
"cfg": 1.6,
"sampler_name": "heun",
"scheduler": "sgm_uniform",
"denoise": 0.21, "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, "force_uniform_tiles": true,
"tiled_decode": true,
"image": [ "image": [
"8", "38",
0 0
], ],
"mask_blur": 8,
"mode_type": "Linear",
"model": [ "model": [
"4", "4",
0 0
], ],
"negative": [
"23",
0
],
"positive": [ "positive": [
"6", "6",
0 0
], ],
"sampler_name": "euler", "negative": [
"scheduler": "normal", "23",
"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",
0 0
], ],
"vae": [ "vae": [
"4", "4",
2 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": [ "upscale_model": [
"24", "24",
0 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": { "27": {
"_meta": {
"title": "Image Resize by Factor"
},
"class_type": "JWImageResizeByFactor",
"inputs": { "inputs": {
"factor": 0.5, "factor": 0.5,
"interpolation_mode": "bicubic",
"image": [ "image": [
"30", "30",
0 0
], ]
"interpolation_mode": "bicubic" },
"class_type": "JWImageResizeByFactor",
"_meta": {
"title": "Image Resize by Factor"
} }
}, },
"30": { "30": {
"_meta": {
"title": "ImageBlur"
},
"class_type": "ImageBlur",
"inputs": { "inputs": {
"blur_radius": 3, "blur_radius": 3,
"sigma": 1.5,
"image": [ "image": [
"26", "26",
0 0
], ]
"sigma": 1.5 },
"class_type": "ImageBlur",
"_meta": {
"title": "ImageBlur"
} }
}, },
"4": { "36": {
"_meta": {
"title": "Load Checkpoint"
},
"class_type": "CheckpointLoaderSimple",
"inputs": { "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": { "38": {
"_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",
"inputs": { "inputs": {
"samples": [ "samples": [
"13", "13",
@ -314,19 +338,10 @@
"4", "4",
2 2
] ]
}
},
"9": {
"_meta": {
"title": "Save Image"
}, },
"class_type": "SaveImage", "class_type": "VAEDecode",
"inputs": { "_meta": {
"filename_prefix": "API_", "title": "VAE Decode"
"images": [
"27",
0
]
} }
} }
} }

View file

@ -23,7 +23,6 @@ import multiprocessing
import asyncio import asyncio
import subprocess import subprocess
import tempfile 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 from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, ASR_DIR, WHISPER_CPP_MODELS, GARBAGE_COLLECTION_INTERVAL, GARBAGE_TTL, WHISPER_CPP_DIR, MAX_CPU_CORES

View file

@ -17,7 +17,7 @@ from datetime import datetime, timedelta
from Foundation import NSDate, NSRunLoop from Foundation import NSDate, NSRunLoop
import EventKit as EK 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 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 from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL
calendar = APIRouter() calendar = APIRouter()
@ -215,8 +215,8 @@ def datetime_to_nsdate(dt: datetime) -> NSDate:
@calendar.get("/events") @calendar.get("/events")
async def get_events_endpoint(start_date: str, end_date: str): async def get_events_endpoint(start_date: str, end_date: str):
start_dt = localize_datetime(start_date) start_dt = await localize_datetime(start_date)
end_dt = localize_datetime(end_date) end_dt = await localize_datetime(end_date)
datetime.strptime(start_date, "%Y-%m-%d") or datetime.now() datetime.strptime(start_date, "%Y-%m-%d") or datetime.now()
end_dt = datetime.strptime(end_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) 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]]): async def parse_calendar_for_day(range_start: datetime, range_end: datetime, events: List[Dict[str, Any]]):
range_start = localize_datetime(range_start) range_start = await localize_datetime(range_start)
range_end = localize_datetime(range_end) range_end = await localize_datetime(range_end)
event_list = [] event_list = []
for event in events: 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") INFO(f"End date string not a dict")
try: 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: except (ValueError, TypeError) as e:
ERR(f"Invalid start date format: {start_str}, error: {e}") ERR(f"Invalid start date format: {start_str}, error: {e}")
continue continue
try: 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: except (ValueError, TypeError) as e:
ERR(f"Invalid end date format: {end_str}, error: {e}") ERR(f"Invalid end date format: {end_str}, error: {e}")
continue continue
@ -377,13 +377,13 @@ async def parse_calendar_for_day(range_start: datetime, range_end: datetime, eve
if start_date: if start_date:
# Ensure start_date is timezone-aware # 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 end_date is not provided, assume it's the same as start_date
if not end_date: if not end_date:
end_date = start_date end_date = start_date
else: else:
end_date = localize_datetime(end_date) end_date = await localize_datetime(end_date)
# Check if the event overlaps with the given range # Check if the event overlaps with the given range
if (start_date < range_end) and (end_date > range_start): if (start_date < range_end) and (end_date > range_start):

View file

@ -10,29 +10,38 @@ from pathlib import Path
from shutil import move from shutil import move
import tempfile import tempfile
import re import re
import ssl from smtplib import SMTP_SSL, SMTP
from smtplib import SMTP_SSL
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import ssl
from datetime import datetime as dt_datetime from datetime import datetime as dt_datetime
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional, Any from typing import List, Optional, Any
import yaml import yaml
from typing import List, Dict, Optional from typing import List, Dict, Optional
from pydantic import BaseModel 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 DEBUG, INFO, WARN, ERR, CRITICAL
from sijapi import PODCAST_DIR, DEFAULT_VOICE, TZ, EMAIL_ACCOUNTS, EmailAccount, IMAPConfig, SMTPConfig from sijapi import PODCAST_DIR, DEFAULT_VOICE, EMAIL_CONFIG
from sijapi.routers import summarize, tts, llm, sd from sijapi.routers import tts, llm, sd, locate
from sijapi.utilities import clean_text, assemble_journal_path, localize_datetime, extract_text, prefix_lines from sijapi.utilities import clean_text, assemble_journal_path, extract_text, prefix_lines
from sijapi.classes import EmailAccount, IncomingEmail, EmailContact from sijapi.classes import EmailAccount, IMAPConfig, SMTPConfig, IncomingEmail, EmailContact
email = APIRouter(tags=["private"]) 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]: 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(): if account.imap.username.lower() == email.lower():
return account return account
return None return None
@ -54,6 +63,18 @@ def get_imap_connection(account: EmailAccount):
ssl=account.imap.encryption == 'SSL', ssl=account.imap.encryption == 'SSL',
starttls=account.imap.encryption == 'STARTTLS') 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]: def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) -> List[Dict]:
matching_profiles = [] matching_profiles = []
@ -72,7 +93,7 @@ def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) ->
'USER_FULLNAME': account.fullname, 'USER_FULLNAME': account.fullname,
'RESPONSE_STYLE': profile.style, 'RESPONSE_STYLE': profile.style,
'AUTORESPONSE_CONTEXT': profile.context, 'AUTORESPONSE_CONTEXT': profile.context,
'IMG_GEN_PROMPT': profile.img_gen_prompt, 'IMG_GEN_PROMPT': profile.image_prompt,
'USER_BIO': account.bio '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: async def generate_auto_response_body(e: IncomingEmail, profile: Dict) -> str:
age = dt_datetime.now(TZ) - e.datetime_received now = await locate.localize_datetime(dt_datetime.now())
prompt = f''' then = await locate.localize_datetime(e.datetime_received)
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. 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} {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: try:
response = await llm.query_ollama(prompt, 400) response = await llm.query_ollama(usr_prompt, sys_prompt, 400)
return response 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: except Exception as e:
ERR(f"Error generating auto-response: {str(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): def clean_email_content(html_content):
@ -123,115 +167,113 @@ async def extract_attachments(attachments) -> List[str]:
return attachment_texts return attachment_texts
async def process_unread_emails(summarize_emails: bool = True, podcast: bool = True):
async def process_account(account: EmailAccount):
while True: while True:
for account in EMAIL_ACCOUNTS: start_time = dt_datetime.now()
try:
DEBUG(f"Connecting to {account.name} to check for unread emails...") DEBUG(f"Connecting to {account.name} to check for unread emails...")
try: with get_imap_connection(account) as inbox:
with get_imap_connection(account) as inbox: DEBUG(f"Connected to {account.name}, checking for unread emails now...")
DEBUG(f"Connected to {account.name}, checking for unread emails now...") unread_messages = inbox.messages(unread=True)
unread_messages = inbox.messages(unread=True) for uid, message in unread_messages:
for uid, message in unread_messages: recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to]
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( this_email = IncomingEmail(
sender=message.sent_from[0]['email'], sender=message.sent_from[0]['email'],
datetime_received=localize_datetime(message.date), datetime_received=localized_datetime,
recipients=recipients, recipients=recipients,
subject=message.subject, subject=message.subject,
body=clean_email_content(message.body['html'][0]) if message.body['html'] else clean_email_content(message.body['plain'][0]) or "", body=clean_email_content(message.body['html'][0]) if message.body['html'] else clean_email_content(message.body['plain'][0]) or "",
attachments=message.attachments attachments=message.attachments
) )
DEBUG(f"\n\nProcessing email for account {account.name}: {this_email.subject}\n\n")
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)
md_path, md_relative = assemble_journal_path(this_email.datetime_received, "Emails", this_email.subject, ".md") if save_success and respond_success:
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)
inbox.mark_seen(uid) inbox.mark_seen(uid)
except Exception as e:
await asyncio.sleep(30) ERR(f"An error occurred for account {account.name}: {e}")
except Exception as e:
ERR(f"An error occurred for account {account.name}: {e}") # Calculate the time taken for processing
await asyncio.sleep(30) 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: if summary:
email (IncomingEmail): The email object containing email details. markdown_content += f'''
summary (str): The summary of the email. > [!summary] Summary
tts_path (str): The path to the text-to-speech audio file. > {summary}
''' '''
DEBUG(f"Saving email to {md_path}...")
# Sanitize filename to avoid issues with filesystems if tts_path.exists():
filename = f"{email.datetime_received.strftime('%Y%m%d%H%M%S')}_{email.subject.replace('/', '-')}.md".replace(':', '-').replace(' ', '_') 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, '> ') DEBUG(f"Saved markdown to {md_path}")
# Create the markdown content
markdown_content = f'''--- return True
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}** | |
'''
if summary: except Exception as e:
markdown_content += f''' ERR(f"Exception: {e}")
> [!summary] Summary return False
> {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}")
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): async def send_auto_response(to_email, subject, body, profile, account):
DEBUG(f"Sending auto response to {to_email}...") 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')) message.attach(MIMEText(body, 'plain'))
if profile['IMG_GEN_PROMPT']: 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): if jpg_path and os.path.exists(jpg_path):
with open(jpg_path, 'rb') as img_file: with open(jpg_path, 'rb') as img_file:
img = MIMEImage(img_file.read(), name=os.path.basename(jpg_path)) img = MIMEImage(img_file.read(), name=os.path.basename(jpg_path))
message.attach(img) message.attach(img)
context = ssl._create_unverified_context() with get_smtp_connection(account) as server:
with SMTP_SSL(account.smtp.host, account.smtp.port, context=context) as server:
server.login(account.smtp.username, account.smtp.password) server.login(account.smtp.username, account.smtp.password)
server.send_message(message) server.send_message(message)
INFO(f"Auto-response sent to {to_email} concerning {subject} from account {account.name}") INFO(f"Auto-response sent to {to_email} concerning {subject} from account {account.name}")
return True
except Exception as e: except Exception as e:
ERR(f"Error in preparing/sending auto-response from account {account.name}: {e}") ERR(f"Error in preparing/sending auto-response from account {account.name}: {e}")
raise e return False
@email.on_event("startup") @email.on_event("startup")
async def startup_event(): async def startup_event():
asyncio.create_task(process_unread_emails()) asyncio.create_task(process_all_accounts())
####

View file

@ -1,10 +1,9 @@
#routers/llm.py #routers/llm.py
from fastapi import APIRouter, HTTPException, Request, Response from fastapi import APIRouter, HTTPException, Request, Response, BackgroundTasks, File, Form, UploadFile
from fastapi.responses import StreamingResponse, JSONResponse from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from starlette.responses import StreamingResponse
from datetime import datetime as dt_datetime from datetime import datetime as dt_datetime
from dateutil import parser 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 from pydantic import BaseModel, root_validator, ValidationError
import aiofiles import aiofiles
import os import os
@ -17,21 +16,20 @@ import base64
from pathlib import Path from pathlib import Path
import ollama import ollama
from ollama import AsyncClient as Ollama, list as OllamaList from ollama import AsyncClient as Ollama, list as OllamaList
import aiofiles
import time import time
import asyncio import asyncio
from pathlib import Path import tempfile
from fastapi import FastAPI, Request, HTTPException, APIRouter import shutil
from fastapi.responses import JSONResponse, StreamingResponse import html2text
from dotenv import load_dotenv import markdown
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 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 import DEBUG, INFO, WARN, ERR, CRITICAL 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.utilities import convert_to_unix_time, sanitize_filename from sijapi.routers.tts import generate_speech
from sijapi.routers.asr import transcribe_audio
llm = APIRouter() llm = APIRouter()
# Initialize chromadb client # Initialize chromadb client
client = chromadb.Client() client = chromadb.Client()
OBSIDIAN_CHROMADB_COLLECTION = client.create_collection("obsidian") OBSIDIAN_CHROMADB_COLLECTION = client.create_collection("obsidian")
@ -80,11 +78,11 @@ async def generate_response(prompt: str):
return {"response": output['response']} 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}, messages = [{"role": "system", "content": sys},
{"role": "user", "content": usr}] {"role": "user", "content": usr}]
LLM = Ollama() 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) DEBUG(response)
if "message" in 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) try_again = gpt4v(image_base64, prompt_sys, prompt_usr, max_tokens)
return try_again 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

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
import requests import requests
import json import yaml
import time import time
import pytz import pytz
import traceback import traceback
@ -9,74 +9,41 @@ from datetime import datetime, timezone
from typing import Union, List from typing import Union, List
import asyncio import asyncio
import pytz import pytz
import aiohttp
import folium import folium
import time as timer import time as timer
from dateutil.parser import parse as dateutil_parse
from pathlib import Path from pathlib import Path
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any, Dict, List, Union from typing import Optional, Any, Dict, List, Union
from datetime import datetime, timedelta, time from datetime import datetime, timedelta, time
from sijapi import LOCATION_OVERRIDES, TZ from sijapi import NAMED_LOCATIONS, TZ, DynamicTZ
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, DB
from sijapi.utilities import get_db_connection, haversine, localize_datetime from sijapi.classes import Location
from sijapi.utilities import haversine
# from osgeo import gdal # from osgeo import gdal
# import elevation # import elevation
locate = APIRouter() locate = APIRouter()
async def reverse_geocode(latitude: float, longitude: float) -> Optional[Location]:
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]:
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={latitude}&lon={longitude}" url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={latitude}&lon={longitude}"
INFO(f"Calling Nominatim API at {url}") INFO(f"Calling Nominatim API at {url}")
headers = { headers = {
'User-Agent': 'sij.law/1.0 (sij@sij.law)', # replace with your app name and email 'User-Agent': 'sij.law/1.0 (sij@sij.law)', # replace with your app name and email
} }
try: try:
response = requests.get(url, headers=headers) async with aiohttp.ClientSession() as session:
response.raise_for_status() # Raise an exception for unsuccessful requests async with session.get(url, headers=headers) as response:
data = response.json() response.raise_for_status()
data = await response.json()
address = data.get("address", {}) address = data.get("address", {})
location = Location( location = Location(
latitude=float(data.get("lat", latitude)), latitude=float(data.get("lat", latitude)),
longitude=float(data.get("lon", longitude)), 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"), zip=address.get("postcode"),
street=address.get("road"), street=address.get("road"),
city=address.get("city"), city=address.get("city"),
@ -97,12 +64,9 @@ def reverse_geocode(latitude: float, longitude: float) -> Optional[Location]:
county=address.get("county"), county=address.get("county"),
country_code=address.get("country_code") country_code=address.get("country_code")
) )
INFO(f"Created Location object: {location}") INFO(f"Created Location object: {location}")
return location return location
except aiohttp.ClientError as e:
except requests.exceptions.RequestException as e:
ERR(f"Error: {e}") ERR(f"Error: {e}")
return None return None
@ -116,66 +80,99 @@ async def geocode(zip_code: Optional[str] = None, latitude: Optional[float] = No
try: try:
# Establish the database connection # 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: except Exception as e:
ERR(f"Error occurred: {e}") ERR(f"Error occurred: {e}")
raise Exception("An error occurred while processing your request") 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]: def find_override_locations(lat: float, lon: float) -> Optional[str]:
# Load the JSON file # Load the JSON file
with open(LOCATION_OVERRIDES, 'r') as file: with open(NAMED_LOCATIONS, 'r') as file:
locations = json.load(file) locations = yaml.safe_load(file)
closest_location = None closest_location = None
closest_distance = float('inf') closest_distance = float('inf')
@ -227,118 +224,120 @@ def get_elevation(latitude, longitude):
return None return None
async def fetch_locations(start: datetime, end: datetime = None) -> List[Location]: 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: 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: else:
end_datetime = localize_datetime(end) end_datetime = await 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) 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}") DEBUG(f"Fetching locations between {start_datetime} and {end_datetime}")
conn = await get_db_connection()
locations = [] async with DB.get_connection() as conn:
locations = []
# Check for records within the specified datetime range # Check for records within the specified datetime range
range_locations = await conn.fetch(''' range_locations = await conn.fetch('''
SELECT id, datetime, SELECT id, datetime,
ST_X(ST_AsText(location)::geometry) AS longitude, ST_X(ST_AsText(location)::geometry) AS longitude,
ST_Y(ST_AsText(location)::geometry) AS latitude, ST_Y(ST_AsText(location)::geometry) AS latitude,
ST_Z(ST_AsText(location)::geometry) AS elevation, ST_Z(ST_AsText(location)::geometry) AS elevation,
city, state, zip, street, city, state, zip, street,
action, device_type, device_model, device_name, device_os action, device_type, device_model, device_name, device_os
FROM locations FROM locations
WHERE datetime >= $1 AND datetime <= $2 WHERE datetime >= $1 AND datetime <= $2
ORDER BY datetime DESC ORDER BY datetime DESC
''', start_datetime.replace(tzinfo=None), end_datetime.replace(tzinfo=None)) ''', start_datetime.replace(tzinfo=None), end_datetime.replace(tzinfo=None))
DEBUG(f"Range locations query returned: {range_locations}") DEBUG(f"Range locations query returned: {range_locations}")
locations.extend(range_locations) locations.extend(range_locations)
if not locations and (end is None or start_datetime.date() == end.date()): if not locations and (end is None or start_datetime.date() == end_datetime.date()):
location_data = await conn.fetchrow(''' location_data = await conn.fetchrow('''
SELECT id, datetime, SELECT id, datetime,
ST_X(ST_AsText(location)::geometry) AS longitude, ST_X(ST_AsText(location)::geometry) AS longitude,
ST_Y(ST_AsText(location)::geometry) AS latitude, ST_Y(ST_AsText(location)::geometry) AS latitude,
ST_Z(ST_AsText(location)::geometry) AS elevation, ST_Z(ST_AsText(location)::geometry) AS elevation,
city, state, zip, street, city, state, zip, street,
action, device_type, device_model, device_name, device_os action, device_type, device_model, device_name, device_os
FROM locations FROM locations
WHERE datetime < $1 WHERE datetime < $1
ORDER BY datetime DESC ORDER BY datetime DESC
LIMIT 1 LIMIT 1
''', start_datetime.replace(tzinfo=None)) ''', start_datetime.replace(tzinfo=None))
DEBUG(f"Fallback query returned: {location_data}") DEBUG(f"Fallback query returned: {location_data}")
if location_data: if location_data:
locations.append(location_data) locations.append(location_data)
await conn.close()
DEBUG(f"Locations found: {locations}") DEBUG(f"Locations found: {locations}")
# Sort location_data based on the datetime field in descending order # Sort location_data based on the datetime field in descending order
sorted_locations = sorted(locations, key=lambda x: x['datetime'], reverse=True) sorted_locations = sorted(locations, key=lambda x: x['datetime'], reverse=True)
# Create Location objects directly from the location data # Create Location objects directly from the location data
location_objects = [Location( location_objects = [
latitude=loc['latitude'], Location(
longitude=loc['longitude'], latitude=loc['latitude'],
datetime=loc['datetime'], longitude=loc['longitude'],
elevation=loc.get('elevation'), datetime=loc['datetime'],
city=loc.get('city'), elevation=loc.get('elevation'),
state=loc.get('state'), city=loc.get('city'),
zip=loc.get('zip'), state=loc.get('state'),
street=loc.get('street'), zip=loc.get('zip'),
context={ street=loc.get('street'),
'action': loc.get('action'), context={
'device_type': loc.get('device_type'), 'action': loc.get('action'),
'device_model': loc.get('device_model'), 'device_type': loc.get('device_type'),
'device_name': loc.get('device_name'), 'device_model': loc.get('device_model'),
'device_os': loc.get('device_os') '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] }
) for loc in sorted_locations if loc['latitude'] is not None and loc['longitude'] is not None
]
return location_objects if location_objects else [] return location_objects if location_objects else []
# Function to fetch the last location before the specified datetime # Function to fetch the last location before the specified datetime
async def fetch_last_location_before(datetime: datetime) -> Optional[Location]: 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}") DEBUG(f"Fetching last location before {datetime}")
conn = await get_db_connection()
location_data = await conn.fetchrow(''' async with DB.get_connection() as conn:
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: location_data = await conn.fetchrow('''
DEBUG(f"Last location found: {location_data}") SELECT id, datetime,
return Location(**location_data) ST_X(ST_AsText(location)::geometry) AS longitude,
else: ST_Y(ST_AsText(location)::geometry) AS latitude,
DEBUG("No location found before the specified datetime") ST_Z(ST_AsText(location)::geometry) AS elevation,
return None 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) @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): async def generate_map_endpoint(start_date_str: str, end_date_str: str):
try: try:
start_date = localize_datetime(start_date_str) start_date = await localize_datetime(start_date_str)
end_date = localize_datetime(end_date_str) end_date = await localize_datetime(end_date_str)
except ValueError: except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format") 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) @locate.get("/map", response_class=HTMLResponse)
async def generate_alltime_map_endpoint(): async def generate_alltime_map_endpoint():
try: 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()) end_date = localize_datetime(datetime.now())
except ValueError: except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format") 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): async def post_location(location: Location):
DEBUG(f"post_location called with {location.datetime}") DEBUG(f"post_location called with {location.datetime}")
conn = await get_db_connection()
try: async with DB.get_connection() as conn:
context = location.context or {} try:
action = context.get('action', 'manual') context = location.context or {}
device_type = context.get('device_type', 'Unknown') action = context.get('action', 'manual')
device_model = context.get('device_model', 'Unknown') device_type = context.get('device_type', 'Unknown')
device_name = context.get('device_name', 'Unknown') device_model = context.get('device_model', 'Unknown')
device_os = context.get('device_os', '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) # 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) await conn.execute('''
VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3, $4), 4326), $5, $6, $7, $8, $9, $10, $11, $12, $13) INSERT INTO locations (datetime, location, city, state, zip, street, action, device_type, device_model, device_name, device_os)
''', 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) VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3, $4), 4326), $5, $6, $7, $8, $9, $10, $11, $12, $13)
await conn.close() ''', 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)
INFO(f"Successfully posted location: {location.latitude}, {location.longitude} on {localized_datetime}") await conn.close()
return { INFO(f"Successfully posted location: {location.latitude}, {location.longitude} on {localized_datetime}")
'datetime': localized_datetime, return {
'latitude': location.latitude, 'datetime': localized_datetime,
'longitude': location.longitude, 'latitude': location.latitude,
'city': location.city, 'longitude': location.longitude,
'state': location.state, 'city': location.city,
'zip': location.zip, 'state': location.state,
'street': location.street, 'zip': location.zip,
'elevation': location.elevation, 'street': location.street,
'action': action, 'elevation': location.elevation,
'device_type': device_type, 'action': action,
'device_model': device_model, 'device_type': device_type,
'device_name': device_name, 'device_model': device_model,
'device_os': device_os 'device_name': device_name,
} 'device_os': device_os
except Exception as e: }
ERR(f"Error posting location {e}") except Exception as e:
ERR(traceback.format_exc()) ERR(f"Error posting location {e}")
return None ERR(traceback.format_exc())
return None
@locate.post("/locate") @locate.post("/locate")
async def post_locate_endpoint(locations: Union[Location, List[Location]]): async def post_locate_endpoint(locations: Union[Location, List[Location]]):
responses = [] responses = []
if isinstance(locations, Location): if isinstance(locations, Location):
locations = [locations] locations = [locations]
for location in locations: for location in locations:
if not location.datetime: if not location.datetime:
current_time = datetime.now(timezone.utc) location.datetime = datetime.now(timezone.utc).isoformat()
location.datetime = current_time.isoformat()
if not location.elevation: 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 # Ensure context is a dictionary with default values if not provided
if not location.context: if not location.context:
location.context = { location.context = {
@ -449,42 +448,57 @@ async def post_locate_endpoint(locations: Union[Location, List[Location]]):
"device_name": "Unknown", "device_name": "Unknown",
"device_os": "Unknown" "device_os": "Unknown"
} }
DEBUG(f"datetime before localization: {location.datetime}") DEBUG(f"datetime before localization: {location.datetime}")
# Convert datetime string to timezone-aware datetime object # 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}") 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) location_entry = await post_location(location)
if location_entry: if location_entry:
responses.append({"location_data": location_entry}) # Add weather data if necessary 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} 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]]) async def get_last_location() -> Optional[Location]:
@locate.get("/locate", response_model=List[Location])
async def get_last_location() -> JSONResponse:
query_datetime = datetime.now(TZ) query_datetime = datetime.now(TZ)
DEBUG(f"Query_datetime: {query_datetime}") DEBUG(f"Query_datetime: {query_datetime}")
location = await fetch_last_location_before(query_datetime) location = await fetch_last_location_before(query_datetime)
if location: if location:
DEBUG(f"location: {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() location_dict["datetime"] = location.datetime.isoformat()
return JSONResponse(content=location_dict) return JSONResponse(content=location_dict)
else: 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]) @locate.get("/locate/{datetime_str}", response_model=List[Location])
async def get_locate(datetime_str: str, all: bool = False): async def get_locate(datetime_str: str, all: bool = False):
try: try:
date_time = localize_datetime(datetime_str) date_time = await localize_datetime(datetime_str)
except ValueError as e: except ValueError as e:
ERR(f"Invalid datetime string provided: {datetime_str}") ERR(f"Invalid datetime string provided: {datetime_str}")
return ["ERROR: INVALID DATETIME PROVIDED. USE YYYYMMDDHHmmss or YYYYMMDD format."] return ["ERROR: INVALID DATETIME PROVIDED. USE YYYYMMDDHHmmss or YYYYMMDD format."]

View file

@ -17,13 +17,12 @@ from requests.adapters import HTTPAdapter
import re import re
import os import os
from datetime import timedelta, datetime, time as dt_time, date as dt_date 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 fastapi import HTTPException, status
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Query, HTTPException from fastapi import APIRouter, Query, HTTPException
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL, INFO 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 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.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 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 = [] results = []
current_date = start_date current_date = start_date
while current_date <= end_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) result = await build_daily_note(formatted_date)
results.append(result) results.append(result)
current_date += timedelta(days=1) 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" header = f"# [[{day_before}|← ]] {formatted_day} [[{day_after}| →]]\n\n"
places = await locate.fetch_locations(date_time) 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) timeslips = await build_daily_timeslips(date_time)
@ -271,9 +270,9 @@ async def process_document(
with open(file_path, 'wb') as f: with open(file_path, 'wb') as f:
f.write(document_content) 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: try:
readable_title = sanitize_filename(title if title else document.filename) 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') 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: if parsed_content is None:
return {"error": "Failed to retrieve content"} 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") markdown_filename, relative_path = assemble_journal_path(datetime.now(), subdir="Articles", filename=readable_title, extension=".md")
try: 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 summary = summary.replace('\n', ' ') # Remove line breaks
if tts_mode == "full" or tts_mode == "content": if tts_mode == "full" or tts_mode == "content":
@ -427,7 +426,7 @@ tags:
raise HTTPException(status_code=500, detail=str(e)) 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) source = source if source else trafilatura.fetch_url(url)
traf = trafilatura.extract_metadata(filecontent=source, default_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 title = np3k.title or traf.title
authors = np3k.authors or traf.author authors = np3k.authors or traf.author
authors = authors if isinstance(authors, List) else [authors] 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 excerpt = np3k.meta_description or traf.description
content = trafilatura.extract(source, output_format="markdown", include_comments=False) or np3k.text content = trafilatura.extract(source, output_format="markdown", include_comments=False) or np3k.text
image = np3k.top_image or traf.image 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') 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: if parsed_content is None:
return {"error": "Failed to retrieve content"} return {"error": "Failed to retrieve content"}
content = parsed_content["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. 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)})") 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)})") 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) jpg_path = await generate_banner(date_time, location, mood=mood, other_context=other_context)
return jpg_path 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): 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}") 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}") 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) destination_path, local_path = assemble_journal_path(date_time, filename="Banner", extension=".jpg", no_timestamp = True)
DEBUG(f"destination path generated: {destination_path}") DEBUG(f"destination path generated: {destination_path}")
@ -699,7 +703,7 @@ async def note_weather_get(
): ):
try: 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}") DEBUG(f"date: {date} .. date_time: {date_time}")
content = await update_dn_weather(date_time) #, lat, lon) content = await update_dn_weather(date_time) #, lat, lon)
return JSONResponse(content={"forecast": content}, status_code=200) return JSONResponse(content={"forecast": content}, status_code=200)
@ -714,7 +718,7 @@ async def note_weather_get(
@note.post("/update/note/{date}") @note.post("/update/note/{date}")
async def post_update_daily_weather_and_calendar_and_timeslips(date: str) -> PlainTextResponse: 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_dn_weather(date_time)
await update_daily_note_events(date_time) await update_daily_note_events(date_time)
await build_daily_timeslips(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)
# description = remove_characters(description) # description = remove_characters(description)
if len(description) > 150: 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 * {description}"
event_markdown += f"\n " 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) @note.get("/note/events", response_class=PlainTextResponse)
async def note_events_endpoint(date: str = Query(None)): 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) response = await update_daily_note_events(date_time)
return PlainTextResponse(content=response, status_code=200) return PlainTextResponse(content=response, status_code=200)

View file

@ -16,6 +16,7 @@ from PIL import Image
from pathlib import Path from pathlib import Path
import uuid import uuid
import json import json
import yaml
import ipaddress import ipaddress
import socket import socket
import subprocess import subprocess
@ -228,30 +229,29 @@ def get_return_path(destination_path):
else: else:
return str(destination_path) return str(destination_path)
# This allows selected scenes by name
def get_scene(scene): def get_scene(scene):
with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file: 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']: for scene_data in SD_CONFIG['scenes']:
if scene_data['scene'] == scene: if scene_data['scene'] == scene:
return scene_data 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): def get_matching_scene(prompt):
prompt_lower = prompt.lower() prompt_lower = prompt.lower()
max_count = 0 max_count = 0
scene_data = None scene_data = None
with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file: 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']: for sc in SD_CONFIG['scenes']:
count = sum(1 for trigger in sc['triggers'] if trigger in prompt_lower) count = sum(1 for trigger in sc['triggers'] if trigger in prompt_lower)
if count > max_count: if count > max_count:
max_count = count max_count = count
scene_data = sc 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.

View file

@ -14,7 +14,8 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from pathlib import Path from pathlib import Path
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL 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 from sijapi import DATA_DIR, SD_IMAGE_DIR, PUBLIC_KEY, OBSIDIAN_VAULT_DIR
serve = APIRouter(tags=["public"]) serve = APIRouter(tags=["public"])
@ -50,7 +51,7 @@ def is_valid_date(date_str: str) -> bool:
@serve.get("/notes/{file_path:path}") @serve.get("/notes/{file_path:path}")
async def get_file(file_path: str): async def get_file(file_path: str):
try: 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) absolute_path, local_path = assemble_journal_path(date_time, no_timestamp = True)
except ValueError as e: except ValueError as e:
DEBUG(f"Unable to parse {file_path} as a date, now trying to use it as a local path") DEBUG(f"Unable to parse {file_path} as a date, now trying to use it as a local path")

View file

@ -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

View file

@ -17,7 +17,6 @@ from fastapi import APIRouter, UploadFile, File, Response, Header, Query, Depend
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sijapi.utilities import localize_datetime
from decimal import Decimal, ROUND_UP from decimal import Decimal, ROUND_UP
from typing import Optional, List, Dict, Union, Tuple from typing import Optional, List, Dict, Union, Tuple
from collections import defaultdict from collections import defaultdict
@ -25,6 +24,7 @@ from dotenv import load_dotenv
from traceback import format_exc from traceback import format_exc
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL
from sijapi import HOME_DIR, TIMING_API_KEY, TIMING_API_URL from sijapi import HOME_DIR, TIMING_API_KEY, TIMING_API_URL
from sijapi.routers.locate import localize_datetime
### INITIALIZATIONS ### ### INITIALIZATIONS ###
time = APIRouter(tags=["private"]) 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]: async def fetch_and_prepare_timing_data(start: datetime, end: Optional[datetime] = None) -> List[Dict]:
# start_date = localize_datetime(start) # start_date = await localize_datetime(start)
# end_date = localize_datetime(end) if end else None # end_date = await localize_datetime(end) if end else None
# Adjust the start date to include the day before and format the end date # 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") 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" 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 # Fetch and process timing data
start = localize_datetime(start_date) start = await localize_datetime(start_date)
end = localize_datetime(end_date) if end_date else None end = await localize_datetime(end_date) if end_date else None
timing_data = await fetch_and_prepare_timing_data(start, end) timing_data = await fetch_and_prepare_timing_data(start, end)
# Retain these for processing Markdown data with the correct timezone # 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}"), start: str = Query(..., regex=r"\d{4}-\d{2}-\d{2}"),
end: Optional[str] = Query(None, 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) start_date = await localize_datetime(start)
end_date = localize_datetime(end) end_date = await localize_datetime(end)
markdown_formatted_data = await process_timing_markdown(start_date, end_date) markdown_formatted_data = await process_timing_markdown(start_date, end_date)
return Response(content=markdown_formatted_data, media_type="text/markdown") return Response(content=markdown_formatted_data, media_type="text/markdown")
@ -444,8 +444,8 @@ async def get_timing_json(
): ):
# Fetch and process timing data # Fetch and process timing data
start = localize_datetime(start_date) start = await localize_datetime(start_date)
end = localize_datetime(end_date) end = await localize_datetime(end_date)
timing_data = await fetch_and_prepare_timing_data(start, end) timing_data = await fetch_and_prepare_timing_data(start, end)
# Convert processed data to the required JSON structure # Convert processed data to the required JSON structure

View file

@ -273,7 +273,17 @@ async def get_voice_file_path(voice: str = None, voice_file: UploadFile = None)
return select_voice(DEFAULT_VOICE) 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: if output_path:
file_path = Path(output_path) file_path = Path(output_path)
else: 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) file_path.parent.mkdir(parents=True, exist_ok=True)
voice_file_path = await get_voice_file_path(voice, voice_file) 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) segments = split_text(text_content)
combined_audio = AudioSegment.silent(duration=0) combined_audio = AudioSegment.silent(duration=0)
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
segment_file_path = TTS_SEGMENTS_DIR / f"segment_{i}.wav" segment_file_path = TTS_SEGMENTS_DIR / f"segment_{i}.wav"
DEBUG(f"Segment file path: {segment_file_path}") 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}") # Run TTS in a separate thread
combined_audio += AudioSegment.from_wav(str(segment_file)) await asyncio.to_thread(
# Delete the segment file immediately after adding it to the combined audio XTTS.tts_to_file,
segment_file_path.unlink() 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: if podcast:
podcast_file_path = PODCAST_DIR / file_path.name 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) return str(file_path)
async def stream_tts(text_content: str, speed: float, voice: str, voice_file) -> StreamingResponse: 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) voice_file_path = await get_voice_file_path(voice, voice_file)
segments = split_text(text_content) segments = split_text(text_content)

View file

@ -7,10 +7,9 @@ from typing import Dict
from datetime import datetime from datetime import datetime
from shapely.wkb import loads from shapely.wkb import loads
from binascii import unhexlify from binascii import unhexlify
from sijapi.utilities import localize_datetime
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL
from sijapi import VISUALCROSSING_API_KEY, TZ from sijapi import VISUALCROSSING_API_KEY, TZ, DB
from sijapi.utilities import get_db_connection, haversine from sijapi.utilities import haversine
from sijapi.routers import locate from sijapi.routers import locate
weather = APIRouter() weather = APIRouter()
@ -25,7 +24,7 @@ async def get_weather(date_time: datetime, latitude: float, longitude: float):
try: try:
DEBUG(f"Daily weather data from db: {daily_weather_data}") DEBUG(f"Daily weather data from db: {daily_weather_data}")
last_updated = str(daily_weather_data['DailyWeather'].get('last_updated')) 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_data = unhexlify(daily_weather_data['DailyWeather'].get('location'))
stored_loc = loads(stored_loc_data) stored_loc = loads(stored_loc_data)
stored_lat = stored_loc.y 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): 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: try:
day_data = weather_data.get('days')[0] daily_weather_query = '''
DEBUG(f"day_data.get('sunrise'): {day_data.get('sunrise')}") INSERT INTO DailyWeather (
sunrise, sunriseEpoch, sunset, sunsetEpoch, description,
# Handle preciptype and stations as PostgreSQL arrays tempmax, tempmin, uvindex, winddir, windspeed, icon, last_updated,
preciptype_array = day_data.get('preciptype', []) or [] datetime, datetimeEpoch, temp, feelslikemax, feelslikemin, feelslike,
stations_array = day_data.get('stations', []) or [] dew, humidity, precip, precipprob, precipcover, preciptype,
snow, snowdepth, windgust, pressure, cloudcover, visibility,
date_str = date_time.strftime("%Y-%m-%d") solarradiation, solarenergy, severerisk, moonphase, conditions,
stations, source, location
# Get location details from weather data if available ) 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)
longitude = weather_data.get('longitude') RETURNING id
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 # Debug logs for better insights
# DEBUG("Executing query: %s", daily_weather_query)
# Correct for the datetime objects # DEBUG("With parameters: %s", daily_weather_params)
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', ''),
)
# 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: try:
hourly_weather_query = ''' await asyncio.sleep(0.1)
INSERT INTO HourlyWeather (daily_weather_id, datetime, datetimeEpoch, temp, feelslike, humidity, dew, precip, precipprob, # hour_data['datetime'] = parse_date(hour_data.get('datetime'))
preciptype, snow, snowdepth, windgust, windspeed, winddir, pressure, cloudcover, visibility, solarradiation, solarenergy, hour_timestamp = date_str + ' ' + hour_data['datetime']
uvindex, severerisk, conditions, icon, stations, source) hour_data['datetime'] = await locate.localize_datetime(hour_timestamp)
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) DEBUG(f"Processing hours now...")
RETURNING id # DEBUG(f"Processing {hour_data['datetime']}")
'''
# Debug logs for better insights hour_preciptype_array = hour_data.get('preciptype', []) or []
# DEBUG("Executing query: %s", hourly_weather_query) hour_stations_array = hour_data.get('stations', []) or []
# DEBUG("With parameters: %s", hourly_weather_params) 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: except Exception as e:
ERR(f"EXCEPTION: {e}") ERR(f"EXCEPTION: {e}")
except Exception as e: return "SUCCESS"
ERR(f"EXCEPTION: {e}")
except Exception as e:
return "SUCCESS" ERR(f"Error in dailyweather storage: {e}")
except Exception as e:
ERR(f"Error in dailyweather storage: {e}")
async def get_weather_from_db(date_time: datetime, latitude: float, longitude: float): 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() daily_weather_data = await conn.fetchrow(query, query_date, longitude, latitude, longitude, latitude)
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) 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: day: Dict = {
DEBUG(f"No daily weather data retrieved from database.") 'DailyWeather': dict(daily_weather_data),
return None 'HourlyWeather': [dict(row) for row in hourly_weather_data],
# else: }
# DEBUG(f"Daily_weather_data: {daily_weather_data}") # DEBUG(f"day: {day}")
# Query to get hourly weather data return day
query = ''' except Exception as e:
SELECT HW.* FROM HourlyWeather HW ERR(f"Unexpected error occurred: {e}")
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}")

View file

@ -17,6 +17,8 @@ from datetime import datetime, date, time
from typing import Optional, Union, Tuple from typing import Optional, Union, Tuple
import asyncio import asyncio
from PIL import Image from PIL import Image
import pandas as pd
from scipy.spatial import cKDTree
from dateutil.parser import parse as dateutil_parse from dateutil.parser import parse as dateutil_parse
from docx import Document from docx import Document
import asyncpg import asyncpg
@ -24,7 +26,7 @@ from sshtunnel import SSHTunnelForwarder
from fastapi import Depends, HTTPException, Request, UploadFile from fastapi import Depends, HTTPException, Request, UploadFile
from fastapi.security.api_key import APIKeyHeader from fastapi.security.api_key import APIKeyHeader
from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL 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") 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.""" """Sanitize a string to be used as a safe filename while protecting the file extension."""
DEBUG(f"Filename before sanitization: {text}") DEBUG(f"Filename before sanitization: {text}")
# Replace multiple spaces with a single space and remove other whitespace
text = re.sub(r'\s+', ' ', text) text = re.sub(r'\s+', ' ', text)
# Remove any non-word characters except space, dot, and hyphen
sanitized = re.sub(ALLOWED_FILENAME_CHARS, '', text) sanitized = re.sub(ALLOWED_FILENAME_CHARS, '', text)
# Remove leading/trailing spaces
sanitized = sanitized.strip() sanitized = sanitized.strip()
# Split the filename into base name and extension
base_name, extension = os.path.splitext(sanitized) base_name, extension = os.path.splitext(sanitized)
# Calculate the maximum length for the base name
max_base_length = max_length - len(extension) max_base_length = max_length - len(extension)
# Truncate the base name if necessary
if len(base_name) > max_base_length: if len(base_name) > max_base_length:
base_name = base_name[:max_base_length].rstrip() base_name = base_name[:max_base_length].rstrip()
# Recombine the base name and extension
final_filename = base_name + 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}") DEBUG(f"Filename after sanitization: {final_filename}")
return final_filename return final_filename
def check_file_name(file_name, max_length=255): 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.""" """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 needs_sanitization = False
# Check for length
if len(file_name) > max_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 needs_sanitization = True
# Check for non-word characters (except space, dot, and hyphen)
if re.search(ALLOWED_FILENAME_CHARS, file_name): 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 needs_sanitization = True
# Check for multiple consecutive spaces
if re.search(r'\s{2,}', file_name): 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 needs_sanitization = True
# Check for leading/trailing spaces
if file_name != file_name.strip(): 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 needs_sanitization = True
DEBUG(f"Filename {'needs' if needs_sanitization else 'does not need'} sanitization")
return needs_sanitization return needs_sanitization
@ -381,49 +357,6 @@ def convert_to_unix_time(iso_date_str):
return int(dt.timestamp()) 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): def haversine(lat1, lon1, lat2, lon2):
""" Calculate the great circle distance between two points on the earth specified in decimal degrees. """ """ 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]) 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)] 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 = { HOURLY_COLUMNS_MAPPING = {
"12am": "00:00:00", "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.save(img_byte_arr, format='JPEG', quality=quality)
img_byte_arr = img_byte_arr.getvalue() img_byte_arr = img_byte_arr.getvalue()
return img_byte_arr 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