Auto-update: Tue Jun 25 16:59:10 PDT 2024
This commit is contained in:
parent
5ebde8faa6
commit
cd6ba72022
26 changed files with 1944 additions and 2467 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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̷. ░ ░ ░ ░ ░
|
||||||
# J U S T ░
|
# J U S T ░
|
||||||
#─── frag, or weat,and locate modules:── H O L D M Y H A N D.
|
#─── frag, or weat,and locate modules:── H O L D M Y H A N D.
|
||||||
DB=db
|
DB_NAME=db
|
||||||
#
|
#
|
||||||
DB_HOST=127.0.0.1
|
DB_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
|
||||||
|
|
70
sijapi/config/email.yaml-example
Normal file
70
sijapi/config/email.yaml-example
Normal 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@'
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
38
sijapi/config/sd.yaml-example
Normal file
38
sijapi/config/sd.yaml-example
Normal 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
|
|
@ -1,8 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "Echo Valley Ranch",
|
|
||||||
"latitude": 42.8098216,
|
|
||||||
"longitude": -123.049396,
|
|
||||||
"radius": 1.5
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -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
461
sijapi/data/sd/workflows/default.json
Executable file → Normal 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
709
sijapi/data/sd/workflows/landscape.json
Executable file → Normal 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
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
####
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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."]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue