From 570d0026a84115223aab5323000af1edb8f80b10 Mon Sep 17 00:00:00 2001 From: sanj <67624670+iodrift@users.noreply.github.com> Date: Sun, 23 Jun 2024 14:51:45 -0700 Subject: [PATCH] Initial commit --- .gitignore | 56 + README.md | 17 + r2r | 1 + requirements.txt | 47 + setup.py | 61 + sij.asc | 92 + sijapi/__init__.py | 251 ++ sijapi/__main__.py | 146 + sijapi/config/.env-example | 496 ++++ sijapi/config/config.py | 98 + sijapi/config/llms.json-example | 151 ++ sijapi/config/logging.conf | 21 + sijapi/config/sd-example.json | 43 + sijapi/config/sd.json | 74 + sijapi/data/loc_overrides.json | 8 + sijapi/data/sd/workflows/base.json | 220 ++ sijapi/data/sd/workflows/default.json | 298 +++ sijapi/data/sd/workflows/landscape.json | 456 ++++ sijapi/data/sd/workflows/selfie.json | 486 ++++ sijapi/data/sd/workflows/turbo.json | 220 ++ sijapi/data/sd/workflows/wallpaper.json | 332 +++ sijapi/data/sd/workflows/wallpaper_fast.json | 281 ++ sijapi/helpers/calendar/exportCal.scpt | Bin 0 -> 2748 bytes sijapi/helpers/calendar/updateCal | 2 + sijapi/helpers/calendar/updateCal.scpt | Bin 0 -> 32442 bytes sijapi/helpers/calendar/updateCal2.scpt | Bin 0 -> 372356 bytes sijapi/helpers/courtlistener/clHooks.py | 195 ++ .../helpers/courtlistener/subscribeAlerts.py | 32 + sijapi/helpers/database/dbrestore.sh | 146 + sijapi/helpers/database/mergedb.py | 123 + sijapi/helpers/database/osm_geocode_upload.py | 89 + sijapi/helpers/embeddings/embed.py | 76 + sijapi/helpers/embeddings/embeddings.py | 15 + sijapi/helpers/mobile/widget.shell | 46 + sijapi/helpers/obsidian/month_o_banners.sh | 17 + sijapi/helpers/scrapers/Readability.js | 2373 +++++++++++++++++ sijapi/logs.py | 88 + sijapi/routers/asr.py | 165 ++ sijapi/routers/calendar.py | 415 +++ sijapi/routers/cf.py | 209 ++ sijapi/routers/email.py | 253 ++ sijapi/routers/health.py | 66 + sijapi/routers/hooks.py | 358 +++ sijapi/routers/ig.py | 973 +++++++ sijapi/routers/llm.py | 484 ++++ sijapi/routers/locate.py | 540 ++++ sijapi/routers/note.py | 1081 ++++++++ sijapi/routers/rag.py | 16 + sijapi/routers/sd.py | 455 ++++ sijapi/routers/serve.py | 73 + sijapi/routers/summarize.py | 211 ++ sijapi/routers/time.py | 577 ++++ sijapi/routers/tts.py | 406 +++ sijapi/routers/weather.py | 265 ++ sijapi/utilities.py | 427 +++ 55 files changed, 14031 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 160000 r2r create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 sij.asc create mode 100644 sijapi/__init__.py create mode 100755 sijapi/__main__.py create mode 100644 sijapi/config/.env-example create mode 100644 sijapi/config/config.py create mode 100644 sijapi/config/llms.json-example create mode 100644 sijapi/config/logging.conf create mode 100644 sijapi/config/sd-example.json create mode 100644 sijapi/config/sd.json create mode 100644 sijapi/data/loc_overrides.json create mode 100755 sijapi/data/sd/workflows/base.json create mode 100755 sijapi/data/sd/workflows/default.json create mode 100755 sijapi/data/sd/workflows/landscape.json create mode 100755 sijapi/data/sd/workflows/selfie.json create mode 100755 sijapi/data/sd/workflows/turbo.json create mode 100644 sijapi/data/sd/workflows/wallpaper.json create mode 100644 sijapi/data/sd/workflows/wallpaper_fast.json create mode 100644 sijapi/helpers/calendar/exportCal.scpt create mode 100755 sijapi/helpers/calendar/updateCal create mode 100755 sijapi/helpers/calendar/updateCal.scpt create mode 100644 sijapi/helpers/calendar/updateCal2.scpt create mode 100644 sijapi/helpers/courtlistener/clHooks.py create mode 100644 sijapi/helpers/courtlistener/subscribeAlerts.py create mode 100755 sijapi/helpers/database/dbrestore.sh create mode 100644 sijapi/helpers/database/mergedb.py create mode 100644 sijapi/helpers/database/osm_geocode_upload.py create mode 100644 sijapi/helpers/embeddings/embed.py create mode 100644 sijapi/helpers/embeddings/embeddings.py create mode 100755 sijapi/helpers/mobile/widget.shell create mode 100755 sijapi/helpers/obsidian/month_o_banners.sh create mode 100644 sijapi/helpers/scrapers/Readability.js create mode 100644 sijapi/logs.py create mode 100644 sijapi/routers/asr.py create mode 100644 sijapi/routers/calendar.py create mode 100644 sijapi/routers/cf.py create mode 100644 sijapi/routers/email.py create mode 100644 sijapi/routers/health.py create mode 100644 sijapi/routers/hooks.py create mode 100644 sijapi/routers/ig.py create mode 100644 sijapi/routers/llm.py create mode 100644 sijapi/routers/locate.py create mode 100644 sijapi/routers/note.py create mode 100644 sijapi/routers/rag.py create mode 100644 sijapi/routers/sd.py create mode 100644 sijapi/routers/serve.py create mode 100644 sijapi/routers/summarize.py create mode 100644 sijapi/routers/time.py create mode 100644 sijapi/routers/tts.py create mode 100644 sijapi/routers/weather.py create mode 100644 sijapi/utilities.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53017d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +\# Ignore specific data files and directories +sijapi/data/calendar.ics +sijapi/data/asr/ +sijapi/data/geocoder/ +sijapi/data/courtlistener/ +sijapi/data/tts/ +sijapi/data/db/ +sijapi/data/sd/workflows/private +sijapi/data/*.pbf +sijapi/data/geonames.txt +sijapi/data/sd/images/ +sijapi/config/O365/ +sijapi/local_only/ +sijapi/testbed/ + +**/.env +**/.config.yaml +**/*.log +**/logs/ +**/__pycache__ +**/.DS_Store +**/*.ics +**/*.sqlite +**/private/ +**/*sync-conflict*.* +**/*.db +**/*.mp3 +**/*.mp4 +**/*.wav +**/*.pyc +**/.ipynb_checkpoints/ +venv/ +env/ +.venv/ +.vscode/ +.idea/ +*~ +*.swp +*.swo +*.com +*.class +*.dll +*.exe +*.o +*.so +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip +ehthumbs.db +Thumbs.db +sijapi/testbed/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0aee6d0 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +``` + .x+=:. . . + z` ^% @88> .. @88> + . .d`` %8P + .@8Ned8" . "8P u @8Ne. .u . + .@^%8888" .@88u . us888u. %8888:u@88N .@88u + x88: `)8b. ''888E` u888u. .@88 "8888" `888I 888. ''888E` + 8888N=*8888 888E `'888E 9888 9888 888I 888I 888E + %8" R88 888E 888E 9888 9888 888I 888I 888E + @8Wou 9% 888E 888E 9888 9888 uW888L 888' 888E + .888888P` 888& 888E 9888 9888 '*88888Nu88P 888& + ` ^"F R888" 888E "888*""888"~ '88888F` R888" + "" 888E ^Y" ^Y' 888 ^ "" + 888E *8E + 888P '8> + .J88" " +``` diff --git a/r2r b/r2r new file mode 160000 index 0000000..c9e7c04 --- /dev/null +++ b/r2r @@ -0,0 +1 @@ +Subproject commit c9e7c04a6bf9f8156cf793ee23379eb0f92f2d38 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbfc9e9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,47 @@ +python-dotenv +setuptools +PyPDF2 +fastapi +pdf2image +pdfminer +pytesseract +python-dateutil +python-docx +hypercorn +starlette +httpx +pydantic +pytz +requests +aiohttp +paramiko +tailscale +pandas +pydub +torch +selenium +webdriver_manager +faster_whisper +filetype +html2text +markdown +ollama +aiofiles +bs4 +imbox +newspaper3k +python-magic +urllib3 +whisper +huggingface_hub +numpy +tqdm +tiktoken +numba +scipy +vectordb +IPython +torchaudio +lxml +lxml_html_clean +pdfminer.six diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..64853f5 --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +from setuptools import setup, find_packages + +setup( + name='sijapi', + version='0.1', + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'sijapi = sijapi.__main__:main', + ], + }, + install_requires=[ + 'fastapi', + 'python-dotenv', + 'hypercorn', + 'setuptools', + 'PyPDF2', + 'pdf2image', + 'pdfminer', + 'pytesseract', + 'python-dateutil', + 'python-docx', + 'starlette', + 'httpx', + 'pydantic', + 'pytz', + 'requests', + 'aiohttp', + 'paramiko', + 'tailscale', + 'pandas', + 'pydub', + 'torch', + 'selenium', + 'webdriver_manager', + 'faster_whisper', + 'filetype', + 'html2text', + 'markdown', + 'ollama', + 'aiofiles', + 'bs4', + 'pdfminer.six', + 'lxml_html_clean', + 'imbox', + 'newspaper3k', + 'python-magic', + 'urllib3', + 'whisper', + 'huggingface_hub', + 'numpy', + 'tqdm', + 'tiktoken', + 'numba', + 'scipy', + 'vectordb', + 'IPython', + 'torchaudio' + ], +) + diff --git a/sij.asc b/sij.asc new file mode 100644 index 0000000..d7a1822 --- /dev/null +++ b/sij.asc @@ -0,0 +1,92 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=PhPl +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/sijapi/__init__.py b/sijapi/__init__.py new file mode 100644 index 0000000..5387b96 --- /dev/null +++ b/sijapi/__init__.py @@ -0,0 +1,251 @@ +import os +import json +from pathlib import Path +import ipaddress +import multiprocessing +from dotenv import load_dotenv +from dateutil import tz +from pathlib import Path +from pydantic import BaseModel +import traceback +import logging +from .logs import Logger + + +# from sijapi.config.config import load_config +# cfg = load_config() + +### Initial initialization +BASE_DIR = Path(__file__).resolve().parent +CONFIG_DIR = BASE_DIR / "config" +ENV_PATH = CONFIG_DIR / ".env" +LOGS_DIR = BASE_DIR / "logs" + +# Create logger instance +package_logger = Logger(__name__, LOGS_DIR) +LOGGER = package_logger.get_logger() + +def DEBUG(log_message): LOGGER.debug(log_message) +def INFO(log_message): LOGGER.info(log_message) +def WARN(log_message): LOGGER.warning(log_message) + +def ERR(log_message): + LOGGER.error(log_message) + LOGGER.error(traceback.format_exc()) + +def CRITICAL(log_message): + LOGGER.critical(log_message) + LOGGER.critical(traceback.format_exc()) + +os.makedirs(LOGS_DIR, exist_ok=True) +load_dotenv(ENV_PATH) + +### API essentials +ROUTERS = os.getenv('ROUTERS', '').split(',') +PUBLIC_SERVICES = os.getenv('PUBLIC_SERVICES', '').split(',') +GLOBAL_API_KEY = os.getenv("GLOBAL_API_KEY") +# HOST_NET and HOST_PORT comprise HOST, which is what the server will bind to +HOST_NET = os.getenv("HOST_NET", "127.0.0.1") +HOST_PORT = int(os.getenv("HOST_PORT", 4444)) +HOST = f"{HOST_NET}:{HOST_PORT}" +BASE_URL = os.getenv("BASE_URL", f"http://{HOST}") +LOCAL_HOSTS = [ipaddress.ip_address(localhost.strip()) for localhost in os.getenv('LOCAL_HOSTS', '127.0.0.1').split(',')] + ['localhost'] +SUBNET_BROADCAST = os.getenv("SUBNET_BROADCAST", '10.255.255.255') +TRUSTED_SUBNETS = [ipaddress.ip_network(subnet.strip()) for subnet in os.getenv('TRUSTED_SUBNETS', '127.0.0.1/32').split(',')] +MAX_CPU_CORES = min(int(os.getenv("MAX_CPU_CORES", int(multiprocessing.cpu_count()/2))), multiprocessing.cpu_count()) + +### Directories & general paths +HOME_DIR = Path.home() +ROUTER_DIR = BASE_DIR / "routers" +DATA_DIR = BASE_DIR / "data" +os.makedirs(DATA_DIR, exist_ok=True) +ALERTS_DIR = DATA_DIR / "alerts" +os.makedirs(ALERTS_DIR, exist_ok=True) +REQUESTS_DIR = LOGS_DIR / "requests" +os.makedirs(REQUESTS_DIR, exist_ok=True) +REQUESTS_LOG_PATH = LOGS_DIR / "requests.log" + + +### Databases +DB = os.getenv("DB", 'sijdb') +DB_HOST = os.getenv("DB_HOST", "127.0.0.1") +DB_PORT = os.getenv("DB_PORT", 5432) +DB_USER = os.getenv("DB_USER", 'sij') +DB_PASS = os.getenv("DB_PASS") +DB_SSH = os.getenv("DB_SSH", "100.64.64.15") +DB_SSH_USER = os.getenv("DB_SSH_USER") +DB_SSH_PASS = os.getenv("DB_SSH_ENV") +DB_URL = f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB}' + + +### LOCATE AND WEATHER LOCALIZATIONS +USER_FULLNAME = os.getenv('USER_FULLNAME') +USER_BIO = os.getenv('USER_BIO') +TZ = tz.gettz(os.getenv("TZ", "America/Los_Angeles")) +HOME_ZIP = os.getenv("HOME_ZIP") # unimplemented +LOCATION_OVERRIDES = DATA_DIR / "loc_overrides.json" +LOCATIONS_CSV = DATA_DIR / "US.csv" +# DB = DATA_DIR / "weatherlocate.db" # deprecated +VISUALCROSSING_BASE_URL = os.getenv("VISUALCROSSING_BASE_URL", "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline") +VISUALCROSSING_API_KEY = os.getenv("VISUALCROSSING_API_KEY") + + +### Obsidian & notes +OBSIDIAN_VAULT_DIR = Path(os.getenv("OBSIDIAN_BASE_DIR") or HOME_DIR / "Nextcloud" / "notes") +OBSIDIAN_JOURNAL_DIR = OBSIDIAN_VAULT_DIR / "journal" +OBSIDIAN_RESOURCES_DIR = "obsidian/resources" +OBSIDIAN_BANNER_DIR = f"{OBSIDIAN_RESOURCES_DIR}/banners" +os.makedirs(Path(OBSIDIAN_VAULT_DIR) / OBSIDIAN_BANNER_DIR, exist_ok=True) +OBSIDIAN_BANNER_SCENE = os.getenv("OBSIDIAN_BANNER_SCENE", "wallpaper") +OBSIDIAN_CHROMADB_COLLECTION = os.getenv("OBSIDIAN_CHROMADB_COLLECTION", "obsidian") +DOC_DIR = DATA_DIR / "docs" +os.makedirs(DOC_DIR, exist_ok=True) + +### DATETIME SCHEMA FOR DAILY NOTE FOLDER HIERARCHY FORMATTING ### +YEAR_FMT = os.getenv("YEAR_FMT") +MONTH_FMT = os.getenv("MONTH_FMT") +DAY_FMT = os.getenv("DAY_FMT") +DAY_SHORT_FMT = os.getenv("DAY_SHORT_FMT") + +### Large language model +LLM_URL = os.getenv("LLM_URL", "http://localhost:11434") +LLM_SYS_MSG = os.getenv("SYSTEM_MSG", "You are a helpful AI assistant.") +SUMMARY_INSTRUCT = os.getenv('SUMMARY_INSTRUCT', "You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.") +SUMMARY_INSTRUCT_TTS = os.getenv('SUMMARY_INSTRUCT_TTS', "You are an AI assistant that provides email summaries for Sanjay. Your response will undergo Text-To-Speech conversion and added to Sanjay's private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following email.") +DEFAULT_LLM = os.getenv("DEFAULT_LLM", "dolphin-mistral") +DEFAULT_VISION = os.getenv("DEFAULT_VISION", "llava") +DEFAULT_VOICE = os.getenv("DEFAULT_VOICE", "Luna") +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +### Stable diffusion +SD_IMAGE_DIR = DATA_DIR / "sd" / "images" +os.makedirs(SD_IMAGE_DIR, exist_ok=True) +SD_WORKFLOWS_DIR = DATA_DIR / "sd" / "workflows" +os.makedirs(SD_WORKFLOWS_DIR, exist_ok=True) +COMFYUI_URL = os.getenv('COMFYUI_URL', "http://localhost:8188") +COMFYUI_DIR = Path(os.getenv('COMFYUI_DIR')) +COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output' +COMFYUI_LAUNCH_CMD = os.getenv('COMFYUI_LAUNCH_CMD', 'mamba activate comfyui && python main.py') +SD_CONFIG_PATH = CONFIG_DIR / 'sd.json' +with open(SD_CONFIG_PATH, 'r') as SD_CONFIG_file: + SD_CONFIG = json.load(SD_CONFIG_file) + +### Summarization +SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens +SUMMARY_CHUNK_OVERLAP = int(os.getenv("SUMMARY_CHUNK_OVERLAP", 100)) # measured in tokens +SUMMARY_TPW = float(os.getenv("SUMMARY_TPW", 1.3)) # measured in tokens +SUMMARY_LENGTH_RATIO = int(os.getenv("SUMMARY_LENGTH_RATIO", 4)) # measured as original to length ratio +SUMMARY_MIN_LENGTH = int(os.getenv("SUMMARY_MIN_LENGTH", 150)) # measured in tokens +SUMMARY_INSTRUCT = os.getenv("SUMMARY_INSTRUCT", "Summarize the provided text. Respond with the summary and nothing else. Do not otherwise acknowledge the request. Just provide the requested summary.") +SUMMARY_MODEL = os.getenv("SUMMARY_MODEL", "llama3") +SUMMARY_TOKEN_LIMIT = int(os.getenv("SUMMARY_TOKEN_LIMIT", 4096)) + +### ASR +ASR_DIR = DATA_DIR / "asr" +os.makedirs(ASR_DIR, exist_ok=True) +WHISPER_CPP_DIR = HOME_DIR / str(os.getenv("WHISPER_CPP_DIR")) +WHISPER_CPP_MODELS = os.getenv('WHISPER_CPP_MODELS', 'NULL,VOID').split(',') + +### TTS +PREFERRED_TTS = os.getenv("PREFERRED_TTS", "None") +TTS_DIR = DATA_DIR / "tts" +os.makedirs(TTS_DIR, exist_ok=True) +VOICE_DIR = TTS_DIR / 'voices' +os.makedirs(VOICE_DIR, exist_ok=True) +PODCAST_DIR = TTS_DIR / "sideloads" +os.makedirs(PODCAST_DIR, exist_ok=True) +TTS_OUTPUT_DIR = TTS_DIR / 'outputs' +os.makedirs(TTS_OUTPUT_DIR, exist_ok=True) +TTS_SEGMENTS_DIR = TTS_DIR / 'segments' +os.makedirs(TTS_SEGMENTS_DIR, exist_ok=True) +ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY") + +### Calendar & email account +MS365_TOGGLE = True if os.getenv("MS365_TOGGLE") == "True" else False +ICAL_TOGGLE = True if os.getenv("ICAL_TOGGLE") == "True" else False +ICS_PATH = DATA_DIR / 'calendar.ics' # deprecated now, but maybe revive? +ICALENDARS = os.getenv('ICALENDARS', 'NULL,VOID').split(',') +class IMAP_DETAILS(BaseModel): + email: str + password: str + host: str + imap_port: int + smtp_port: int + imap_encryption: str = None + smtp_encryption: str = None + +IMAP = IMAP_DETAILS( + email = os.getenv('IMAP_EMAIL'), + password = os.getenv('IMAP_PASSWORD'), + host = os.getenv('IMAP_HOST', '127.0.0.1'), + imap_port = int(os.getenv('IMAP_PORT', 1143)), + smtp_port = int(os.getenv('SMTP_PORT', 469)), + imap_encryption = os.getenv('IMAP_ENCRYPTION', None), + smtp_encryption = os.getenv('SMTP_ENCRYPTION', None) +) +AUTORESPONSE_WHITELIST = os.getenv('AUTORESPONSE_WHITELIST', '').split(',') +AUTORESPONSE_BLACKLIST = os.getenv('AUTORESPONSE_BLACKLIST', '').split(',') +AUTORESPONSE_BLACKLIST.extend(["no-reply@", "noreply@", "@uscourts.gov", "@doi.gov"]) +AUTORESPONSE_CONTEXT = os.getenv('AUTORESPONSE_CONTEXT', None) +AUTORESPOND = AUTORESPONSE_CONTEXT != None + +### Courtlistener & other webhooks +COURTLISTENER_DOCKETS_DIR = DATA_DIR / "courtlistener" / "dockets" +os.makedirs(COURTLISTENER_DOCKETS_DIR, exist_ok=True) +COURTLISTENER_SEARCH_DIR = DATA_DIR / "courtlistener" / "cases" +os.makedirs(COURTLISTENER_SEARCH_DIR, exist_ok=True) +CASETABLE_PATH = DATA_DIR / "courtlistener" / "cases.json" +COURTLISTENER_API_KEY = os.getenv("COURTLISTENER_API_KEY") +COURTLISTENER_BASE_URL = os.getenv("COURTLISTENER_BASE_URL", "https://www.courtlistener.com") +COURTLISTENER_DOCKETS_URL = "https://www.courtlistener.com/api/rest/v3/dockets/" + +### Keys & passwords +PUBLIC_KEY_FILE = os.getenv("PUBLIC_KEY_FILE", 'you_public_key.asc') +PUBLIC_KEY = (BASE_DIR.parent / PUBLIC_KEY_FILE).read_text() +MAC_ID = os.getenv("MAC_ID") +MAC_UN = os.getenv("MAC_UN") +MAC_PW = os.getenv("MAC_PW") +TIMING_API_KEY = os.getenv("TIMING_API_KEY") +TIMING_API_URL = os.getenv("TIMING_API_URL", "https://web.timingapp.com/api/v1") +PHOTOPRISM_URL = os.getenv("PHOTOPRISM_URL") +PHOTOPRISM_USER = os.getenv("PHOTOPRISM_USER") +PHOTOPRISM_PASS = os.getenv("PHOTOPRISM_PASS") + +### Tailscale +TS_IP = ipaddress.ip_address(os.getenv("TS_IP", "NULL")) +TS_SUBNET = ipaddress.ip_network(os.getenv("TS_SUBNET")) if os.getenv("TS_SUBNET") else None +TS_ID = os.getenv("TS_ID", "NULL") +TS_TAILNET = os.getenv("TS_TAILNET", "NULL") +TS_ADDRESS = f"http://{TS_ID}.{TS_TAILNET}.ts.net" + +### Cloudflare +CF_API_BASE_URL = os.getenv("CF_API_BASE_URL") +CF_TOKEN = os.getenv("CF_TOKEN") +CF_IP = DATA_DIR / "cf_ip.txt" # to be deprecated soon +CF_DOMAINS_PATH = DATA_DIR / "cf_domains.json" # to be deprecated soon + +### Caddy - not fully implemented +BASE_URL = os.getenv("BASE_URL") +CADDY_SERVER = os.getenv('CADDY_SERVER', None) +CADDYFILE_PATH = os.getenv("CADDYFILE_PATH", "") if CADDY_SERVER is not None else None +CADDY_API_KEY = os.getenv("CADDY_API_KEY") + + +### Microsoft Graph +MS365_CLIENT_ID = os.getenv('MS365_CLIENT_ID') +MS365_SECRET = os.getenv('MS365_SECRET') +MS365_TENANT_ID = os.getenv('MS365_TENANT_ID') +MS365_CERT_PATH = CONFIG_DIR / 'MS365' / '.cert.pem' # deprecated +MS365_KEY_PATH = CONFIG_DIR / 'MS365' / '.cert.key' # deprecated +MS365_KEY = MS365_KEY_PATH.read_text() +MS365_TOKEN_PATH = CONFIG_DIR / 'MS365' / '.token.txt' +MS365_THUMBPRINT = os.getenv('MS365_THUMBPRINT') + +MS365_LOGIN_URL = os.getenv("MS365_LOGIN_URL", "https://login.microsoftonline.com") +MS365_AUTHORITY_URL = f"{MS365_LOGIN_URL}/{MS365_TENANT_ID}" +MS365_REDIRECT_PATH = os.getenv("MS365_REDIRECT_PATH", "https://api.sij.ai/o365/oauth_redirect") +MS365_SCOPE = os.getenv("MS365_SCOPE", 'Calendars.Read,Calendars.ReadWrite,offline_access').split(',') + +### Maintenance +GARBAGE_COLLECTION_INTERVAL = 60 * 60 # Run cleanup every hour +GARBAGE_TTL = 60 * 60 * 24 # Delete files older than 24 hours \ No newline at end of file diff --git a/sijapi/__main__.py b/sijapi/__main__.py new file mode 100755 index 0000000..b1a85ca --- /dev/null +++ b/sijapi/__main__.py @@ -0,0 +1,146 @@ +#!/Users/sij/miniforge3/envs/api/bin/python +from fastapi import FastAPI, Request, HTTPException, Response +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import ClientDisconnect +from hypercorn.asyncio import serve +from hypercorn.config import Config +import sys +import asyncio +import httpx +import argparse +import json +import ipaddress +import importlib +from dotenv import load_dotenv +from pathlib import Path +from datetime import datetime +import argparse +from . import LOGGER, LOGS_DIR +from .logs import Logger + +parser = argparse.ArgumentParser(description='Personal API.') +parser.add_argument('--debug', action='store_true', help='Set log level to INFO') +parser.add_argument('--test', type=str, help='Load only the specified module.') +args = parser.parse_args() + +# Using the package logger +main_logger = Logger("main", LOGS_DIR) +main_logger.setup_from_args(args) +logger = LOGGER + +# Use the logger +logger.debug("Debug Log") +logger.info("Info Log") + + +from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL + +from sijapi import HOST, ENV_PATH, GLOBAL_API_KEY, REQUESTS_DIR, ROUTER_DIR, REQUESTS_LOG_PATH, PUBLIC_SERVICES, TRUSTED_SUBNETS, ROUTERS + + +# Initialize a FastAPI application +api = FastAPI() + + +# CORSMiddleware +api.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + +class SimpleAPIKeyMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + client_ip = ipaddress.ip_address(request.client.host) + if request.method == "OPTIONS": + # Allow CORS preflight requests + return JSONResponse(status_code=200) + if request.url.path not in PUBLIC_SERVICES: + if not any(client_ip in subnet for subnet in TRUSTED_SUBNETS): + api_key_header = request.headers.get("Authorization") + api_key_query = request.query_params.get("api_key") + if api_key_header: + api_key_header = api_key_header.lower().split("bearer ")[-1] + if api_key_header != GLOBAL_API_KEY and api_key_query != GLOBAL_API_KEY: + ERR(f"Invalid API key provided by a requester.") + return JSONResponse( + status_code=401, + content={"detail": "Invalid or missing API key"} + ) + response = await call_next(request) + # DEBUG(f"Request from {client_ip} is complete") + return response + +api.add_middleware(SimpleAPIKeyMiddleware) + +canceled_middleware = """ +@api.middleware("http") +async def log_requests(request: Request, call_next): + DEBUG(f"Incoming request: {request.method} {request.url}") + DEBUG(f"Request headers: {request.headers}") + DEBUG(f"Request body: {await request.body()}") + response = await call_next(request) + return response + +async def log_outgoing_request(request): + INFO(f"Outgoing request: {request.method} {request.url}") + DEBUG(f"Request headers: {request.headers}") + DEBUG(f"Request body: {request.content}") +""" + +@api.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + ERR(f"HTTP Exception: {exc.status_code} - {exc.detail}") + ERR(f"Request: {request.method} {request.url}") + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + +@api.middleware("http") +async def handle_exception_middleware(request: Request, call_next): + try: + response = await call_next(request) + except RuntimeError as exc: + if str(exc) == "Response content longer than Content-Length": + # Update the Content-Length header to match the actual response content length + response.headers["Content-Length"] = str(len(response.body)) + else: + raise + return response + + + +def load_router(router_name): + router_file = ROUTER_DIR / f'{router_name}.py' + DEBUG(f"Attempting to load {router_name.capitalize()}...") + if router_file.exists(): + module_path = f'sijapi.routers.{router_name}' + try: + module = importlib.import_module(module_path) + router = getattr(module, router_name) + api.include_router(router) + INFO(f"{router_name.capitalize()} router loaded.") + except (ImportError, AttributeError) as e: + CRITICAL(f"Failed to load router {router_name}: {e}") + else: + ERR(f"Router file for {router_name} does not exist.") + +def main(argv): + if args.test: + load_router(args.test) + else: + CRITICAL(f"sijapi launched") + CRITICAL(f"{args._get_args}") + for router_name in ROUTERS: + load_router(router_name) + + config = Config() + config.keep_alive_timeout = 1200 + config.bind = [HOST] + asyncio.run(serve(api, config)) + +if __name__ == "__main__": + main(sys.argv[1:]) \ No newline at end of file diff --git a/sijapi/config/.env-example b/sijapi/config/.env-example new file mode 100644 index 0000000..c3a2606 --- /dev/null +++ b/sijapi/config/.env-example @@ -0,0 +1,496 @@ +#────────────────────────────────────────────────────────────────────────────────── +# C O N F I G U R A T I O N F I L E +#────────────────────────────────────────────────────────────────────────────────── +# +# Hi friend! You've found my hidden .config.YAML-example file. Do you like Zalgo +# text and old-school ASCII art? I bet you do. So listen, this'll be your method +# for configuring sijapi, and nothing works until you at least: +# +# (1) fill in the ESSENTIALS category, and +# +# (2) rename this file `.config.yaml` +# +# ... and even then, certain features will not work until you set other +# relevant variables below. +# +# So get yourself a beverage, put on some sick beats, and settle in for a vibe-y +# configuration sesh. Remember to read my detailed notes if you ever feel lost, +# and most important, remember: +# +# † you are NOT alone, +# † I love you SO much, +# † and you are S̸̢̟̑̒̊ͅō̸͔͕͎̟͜ worthy. +# +# y o u r b f & b f 4 e , +# .x+=:. . . +# z` ^% @88> .. †††> +# . .d`` %†P +# .@8Ned8" . "8P u @8Ne. .u . +# .@^%8888" .@88u . us888u. %8888:u@88N .@88u +# x88: `)8b. ''888E` u888u. .@88 "8888" `888I 888. ''888E` +# ~ 8888N=*8888 888E `'888E 9888 9888 888I 888I 888E +# %8" R88 888E 888E 9888 9888 888I 888I 888E +# @8Wou 9% 888E 888E 9888 9888 uW888L 888' 888E +# .888888P` 888& 888E 9888 9888 '*88888Nu88P 888& +# ` ^"F R888" 888E "888*""888" ~ '88888F` R888" +# "" 888E ^Y" ^Y' 888 ^ "" +# 888E *8E +# 888P '8> +# .J88" " " +# +# +# B U T I H E A R Y O U : +# L E T ' S T A K E I T S L O W A N D +# ───────────── S̢͉̺ T̪͔͓ A͇̞ R̘͕͙ T̢̡͉ W͚̻ I͉͇͜ T̟͖̺ H̡͚͙ T̺̞̠ H̢̢̙ E̢̪͓ ────────────── +# +# ███████╗███████╗███████╗███████╗███╗ ██╗████████╗██╗ █████╗ ██╗ ███████╗ +# ██╔════╝██╔════╝██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║██╔══██╗██║ ██╔════╝ +# █████╗ ███████╗███████╗█████╗ ██╔██╗ ██║ ██║ ██║███████║██║ ███████╗ +# ██╔══╝ ╚════██║╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██╔══██║██║ ╚════██║ +# ███████╗███████║███████║███████╗██║ ╚████║ ██║ ██║██║ ██║███████╗███████║ +# ╚══════╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ +# ───────────────────────────────────────────────────────────────── +# +#─── first, bind an ip address and port : ────────────────────────────────────────── +HOST_NET=0.0.0.0 +HOST_PORT=4444 +BASE_URL=http://localhost:4444 # <--- replace with base URL of reverse proxy, etc +#─── notes: ────────────────────────────────────────────────────────────────────── +# +# HOST_NET† and HOST_PORT comprise HOST and determine the ip and port the server binds to. +# BASE_URL is used to assemble URLs, e.g. in the MS authentication flow and for serving images generated on the sd router. +# BASE_URL should match the base URL used to access sijapi sans endpoint, e.g. http://localhost:4444 or https://api.sij.ai +# +# † Take care here! Please ensure you understand the implications of setting HOST_NET to anything besides 127.0.0.1, and configure your firewall and router appropriately if you do. Setting HOST_NET to 0.0.0.0, for instance, opens sijapi to any device the server running it is accessible to — including potentially frightening internet randos (depending how your firewall, router, and NAT are configured). +# +# Here are a few options to consider to more securely enable access from +# other devices: +# +# (1) if all access can occur over Tailscale, either: +# (a) leave HOST_NET set to 127.0.0.1, run `tailscale cert $(tailscale +# whois $(tailscale ip | head -n 1) | awk '/Name:/ {print $2}') +# if you haven't already issued yourself a TLS certificate on +# Tailscale, and then run `tailscale serve --bg --https=4443 +# 4444` to expose sijapi to your other tailscale-enabled devices +# at `https://{device.magicdns-domain.net:4443`}; or +# (b) set HOST_NET to your server's Tailscale IP (this should work +# but for me doesn't reliably) +# +# (2) if WAN access truly is required, leave HOST_NET set to 127.0.0.1 and +# configure either: +# (a) a Cloudflare tunnel, or +# (b) a reverse proxy with HTTPS (Caddy is excellent for this). +# +# And please be sure to set a strong API key either way but especially for (2). +# ────────── +# +#──── configure API key authorization and select exemptions──────────────────begin +GLOBAL_API_KEY=¿SECRET? # <--- specify a key to unlock the API +PUBLIC_SERVICES=/id,/ip,/health,/img/,/cl/dockets,/cl/search,/cd/alert +TRUSTED_SUBNETS=127.0.0.1/32,10.13.37.0/24,100.64.64.0/24 +#─── notes: ───────────────────────────────────────────────────────────────────end +# +# GLOBAL_API_KEY determines the API key that will be required to access all endpoints, except access to PUBLIC_SERVICES or from TRUSTED_SUBNETS. Authentication is made via an `Authorization: Bearer {GLOBAL_API_KEY}` header. +# TRUSTED_SUBNETS might commonly include 127.0.0.1/32 (localhost), 100.x.x.0/24 (Tailscale tailnet), and/or 192.168.x.0/24 or 10.x.x.0/24 (local network). +# When configuring a reverse proxy or Cloudflare tunnel, please verify traffic through it does not appear to sijapi (i.e. in ./logs) as though it were coming from any of the subnets specified here. For sij, using Caddy, it does not, but your setup may differ. +# ────────── +# +#─── router selection: ──────────────────────────────────────────────────────────── +ROUTERS=asr,calendar,cf,email,health,hooks,llm,locate,note,rag,sd,serve,summarize,time,tts,weather +UNLOADED=ig +#─── notes: ────────────────────────────────────────────────────────────────────── +# +# ROUTERS determines which routers are loaded.† +# +# UNLOADED is not used directly -- it's just there to help keep track which routers are disabled. +# +# † ┓ ┏ orth bearing in mind: some routers inherently rely on other routers, +# ┃┃┃ 3rd party APIs, or other apps being installed locally. If a router is +# ┗┻┛ set to load (i.e. is included in ROUTERS) and it depends on another router, +# that other router will also load too irrespective of whether it's listed. +# +# B U T L E T ' S G E T D O W N T O +# B R A S S T A C K S , S H A L L W E ? +# +# asr: requires faster_whisper — $ pip install faster_whisper — and +# downloading the model file specified in ASR_DEFAULT_MODEL. +# +# calendar: requires (1) a Microsoft 365 account with a properly configured +# Azure Active Directory app, and/or (2) Calendars on macOS. +# +# cf: interfaces with the Cloudflare API and Caddy to register new +# [sub-]domains on Cloudflare and deploy them with Caddy as +# reverse proxy. +# +# llm: requires ollama — $ pip install ollama — and downloading the +# models set in LLM_DEFAULT_MODEL and LLM_VISION_MODEL. +# +# email: email auto-responders and summarizers to be found here. Designed +# for use with IMAP. +# +# hooks: designed for two specific use cases: monitoring court dockets +# through CourtListener.org, and monitoring arbitrary web pages for +# changes in tandem with a self-hosted changedetection.io instance. +# Both require accounts; other functionality would require +# additional / modified code. +# +# ig: requires an Instagram account, with credentials and other settings +# configured separately in the ig_config.json file; relies heavily +# on the llm and sd routers which have their own dependencies. +# +# locate: some endpoints work as is, but the core location tracking +# functionality requires Postgresql + PostGIS extension and are +# designed specifically to pair with a mobile device where +# Pythonista is installed and configured to run the +# `gps_tracker.py` and `gps_upload.py` scripts periodically or per +# repeating conditionwy (e.g. via automation under Apple Shortcuts). +# +# note: designed for use with Obsidian plus the Daily Notes and Tasks +# core extensions; and the Admonitions, Banners, Icons (with the +# Lucide pack), and Make.md community extensions. Moreover `notes` +# relies heavily on the calendar, llm, locate, sd, summarize, time, +# tts, and weather routers and accordingly on the external +# dependencies of each. +# +# sd: requires ComfyUI plus any modules and StableDiffusion models +# set in sd_config and individual workflow .json files. +# +# summarize: relies on the llm router and thus requires ollama. +# +# time: requires the subscription-based macOS app 'Timing' (one of many +# apps that together make SetApp an incredible value for macOS users!) +# +# tts: designed for use with coqui — $ pip install coqui — and/or the +# ElevenLabs API. +# +# weather: requires a VisualCrossing API key and is designed for (but doesn't +# itself strictly require) Postgresql with the PostGIS extension; +# (... but it presently relies on the locate router, which does). +# +# +# ... Whew! that was a lot, right? I'm so glad we're in this together... +# ────────── +# +#───────── W H A T A R E Y O U R D I G I T S , H O N E Y B U N ? ──────── +# LOCALIZATION +#─── what are your digits, honey-bun?: ────────────────────────────────────────────── +TZ=America/Los_Angeles +HOME_ZIP=97401 +#─── notes: ───────────────────────────────────────────────────────────────────────── +# +# ────────── +# +#─────────────────────── Y ₒ ᵤ ' ᵣ ₑ G ₒ ₙ ₙ ₐ ₗ ₒ ᵥ ₑ ──────────────────────── +# +# ░ ░░ ░░ ░ ░░░░░░░░ ░░░ ░░░ ░░ ░░░░░░░ ░ +# ▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒ ▒ ▒▒▒▒ ▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ +# ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓ ▓ ▓▓▓▓▓▓▓ ▓▓▓ +# ████ ████ ████ ████ █████████████ █ ████ █ █ ███████ ███████ +# ████ ████ ████ █ █ ██ ███ ██ ████ █ █ █ +# +# A N D I ' M N O T. E V E N. J E A L O U S. +# Y O U D E S E R V E I T A L L , B A B Y C A K E S. +# +#─── use tailscale for secure remote access: ─────────────────────────────────────── +TS_IP=100.13.37.5 # <--- enter your own TS IP address +TS_SUBNET=100.13.37.0/24 # <--- enter your own TS subnet (IPv4/CIDR) +TS_ID=¿SECRET? # <--- enter your own TS device name +TS_TAILNET=screaming_sailfin # <--- enter your own TS tailnet / MagicDNS name +TAILSCALE_API_KEY=¿SECRET? # <--- enter your own TS API key +#─── notes: ──────────────────────────────────────────────────────────────────────── +# +# TS_IP should match the Tailscale IP of the device. But this is deprecated, and if the functionality becomes relevant again, it should be come back in the form of a dynamic check (`tailscale status` in a shell subprocess) in __init__.py or even the /id endpoint. +# TS_SUBNET should match the IP/CIDR-format tailnet +# TS_ID currently has two roles: it's used to assemble the complete MagicDNS of the server, and it determines what the /id endpoint on the health router returns. This is relevant where multiple servers run the script behind a load balancer (e.g. Caddy), as a means to check which server responds. Bear in mind that /id is NOT API key-protected by default here. +# TS_TAILNET should match the tailnet's MagicDNS domain (omitting the `.net`, for reasons) +# ────────── +# +#──────────── ᵁ & ᴹ ᴱ , W E C A N G E T T H R O U G H ──────────────────── +# +# ██▓███ ▒█████ ██████ ▄▄▄█████▓ ▄████ ██▀███ ▓█████ ██████ +# ▓██░ ██▒██▒ ██▒▒██ ▒ ▓ ██▒ ▓▒ ██▒ ▀█▒▓██ ▒ ██▒▓█ ▀ ▒██ ▒ +# ▓██░ ██▓▒██░ ██▒░ ▓██▄ ▒ ▓██░ ▒░▒██░▄▄▄░▓██ ░▄█ ▒▒███ ░ ▓██▄ +# ▒██▄█▓▒ ▒██ ██░ ▒ ██▒░ ▓██▓ ░ ░▓█ ██▓▒██▀▀█▄ ▒▓█ ▄ ▒ ██▒ +# ▒██▒ ░ ░ ████▓▒░▒██████▒▒ ▒██▒ ░ ░▒▓███▀▒░██▓ ▒██▒░▒████▒▒██████▒▒ +# ▒██▒ ░ ░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░ ▒ ░░ ░▒ ▒ ░ ▒▓ ░▒▓░░░ ▒░ ░▒ ▒▓▒ ▒ ░ +# ▒▓▒░ ░ ▒ ▒░ ░ ░▒ ░ ░ ░ ░ ░ ░▒ ░ ▒░ ░ ░ ░░ ░▒ ░ ░ +# ░▒ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ +# ░░ ░ ░T̷ O̷ G̷ E̷ T̷ H̷ ░ R̷. ░ ░ ░ ░ ░ +# J U S T ░ +#─── frag, or weat,and locate modules:── H O L D M Y H A N D. +DB=db +# +DB_HOST=127.0.0.1 +DB_PORT=5432 +# R E A L T I G H T. +DB_USER=postgres +DB_PASS=¿SECRET? # <--- enter your own Postgres password' +# Y E A H . . . +DB_SSH=100.64.64.15 +# . . . 𝙹 𝚄 𝚂 𝚃 𝙻 𝙸 𝙺 𝙴 𝚃 𝙷 𝙰 𝚃. +DB_SSH_USER=sij +DB_SSH_PASS=¿SECRET? # <--- enter SSH password for pg server (if not localhost) +#─── notes: ────────────────────────────────────────────────── S E E ? 𝕰 𝖅 - 𝕻 𝖅 +# +# DB, DB_HOST, DB_PORT, DB_USER, and DB_PASS should specify those respective +# credentials for your Postgres database. DB_SSH and associated _USER and _PASS +# variables allow database access over an SSH tunnel. +# +# In the current implementation, we rely on Postgres to hold: +# i. user-logged location data (locate module), and +# ii. results from past weather forecast checks (weather module). +# +# A future version will hopefully make use of PostGIS's geocoding capabilities, +# and add a vector database for the LLM module. Until then it's up to you if the +# locate and weather modules are worth the hassle of maintaining Postgres. +# ────────── +# +#─────────────────────────────── 𝐼 𝐵 𝐸 𝑇 𝑌 𝑂 𝑈 ───────────────────────────────── +# 𝑅 𝐸 𝐶 𝐸 𝐼 𝑉 𝐸 𝐴 𝐿 𝑂 𝑇 𝑂 𝐹 𝐿 𝑂 𝑉 𝐸 𝐿 𝐸 𝑇 𝑇 𝐸 𝑅 𝑆 𝑂 𝑉 𝐸 𝑅 +# +# .----------------. .----------------. .----------------. .----------------. +# | .--------------. | .--------------. | .--------------. | .--------------. | +# | | _____ | | | ____ ____ | | | __ | | | ______ | | +# | | |_ _| | | ||_ \ / _|| | | / \ | | | |_ __ \ | | +# | | | | | | | | \/ | | | | / /\ \ | | | | |__) | | | +# | | | | | | | | |\ /| | | | | / ____ \ | | | | ___/ | | +# | | _| |_ | | | _| |_\/_| |_ | | | _/ / \ \_ | | | _| |_ | | +# | | |_____| | | ||_____||_____|| | ||____| |____|| | | |_____| | | +# | | | | | | | | | | | | | +# | '--------------' | '--------------' | '--------------' | '--------------' | +# '----------------' '----------------' '----------------' '----------------' +# +# 𝙴 𝙼 𝙰 𝙸 𝙻 +# +#─── imap & smtp: ──────────────────────────────────────────────────────────────────────── +IMAP_HOST=127.0.0.1 +IMAP_EMAIL=¿SECRET? # <--- enter yours +IMAP_PASSWORD=¿SECRET? # <--- enter yours +IMAP_PORT=1142 +IMAP_ENCRYPTION=STARTTLS +SMTP_PORT=1024 +SMTP_ENCRYPTION=SSL +AUTORESPONSE_WHITELIST=¿SECRET? # <--- enter complete/fragmented emails, or keywords +AUTORESPONSE_BLACKLIST=¿SECRET? # <--- same deal-io +AUTORESPONSE_CONTEXT=¿SECRET? # <--- inform the LLM why it's auto-responding for you' +USER_FULLNAME=¿SECRET? # <--- more context for the LLM +USER_BIO=¿SECRET? # <--- yet more context for the nosy LLM +#─── notes: ─────────────────────────────────────────────────────────────────────────────── +# +# This is primarily for summarizing incoming emails. Any IMAP account should work, but +# I focused testing on a somewhat complex setup involving Protonmail Bridge. +# +# ────────── +# +# +#─── ms365 (calendars): ────────────────────────────────────────────────────────────── +ICAL_TOGGLE=True +ICALENDARS='E68FE085-2ECA-4097-AF0A-8D38C404D8DA,AB5A0473-16DD-4916-BD6D-F12AC2455285' +MS365_TOGGLE=False +MS365_CLIENT_ID=¿SECRET? # <--- enter your client ID (found in Azure pane) +MS365_TENANT_ID=¿SECRET? # <--- enter your tenant ID (found in Azure pane) +MS365_SECRET=¿SECRET? # <--- enter your app secret (found in Azure pane) +MS365_SCOPE='basic,calendar_all,Calendars.Read,Calendars.ReadWrite,offline_access' +MS365_TOKEN_FILE=oauth_token.txt +MS365_LOGIN_URL='https://login.microsoftonline.com' +MS365_REDIRECT_PATH=¿SECRET? # <--- e.g. http://localhost:4444/o365/oauth_redirect +#─── notes: ─────────────────────────────────────────────────────────────────────────────── +# +# # MS365_CLIENT_ID, _TENANT_ID, _SECRET, AND _SCOPES must be obtained from Microsoft +# via the Azure portal, by creating a new app registration and an accompanying secret. +# MS365_THUMBPRINT is vestige of an earlier failed attempt to get this working, and +# for now is deprecated. I recommend seeking out a well-reviewed tutorial for +# creating an app on Azure with a client_id and secret and necessary scopes for +# individual calendar access, because I had one heck of a time trying various approaches. +# Do better, Microsoft. +# +# ────────── +# +# +#──────────────────── L E T ' S G E T S I L L Y , ───────────────────────────── +# T H E N G O B͎̝̪̼͉͜ O͖͕͇͚͉̼ N̢̦͖̺͔͎ K̠͓̠͖͜ E̝̼̫̙͔̞ R̡͇͖̙͉͎ S̡͉̠͎͙̪ +# W I T H O U R O W N +# +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██████▒▓██████▒░ +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# ░▒▓████████▓▒ ░▒▓████████▓▒ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ +# +# +# ( F O R R E A L T H O U G H , T H E S E A R E +# +#─── via comfyui (stable diffusion): ─────── S̝͖̦͓̪̻ O̡͖̘̫͇̟ H̢͔͔̫͉͜ O̢̢͉̞͍̘ T̟͍͍̪̦̞ R I G H T N O W +LLM_URL=http://localhost:11434 +SYSTEM_MSG=You are a helpful AI assistant. +DEFAULT_LLM=dolphin-mistral +DEFAULT_VISION=llava-llama3 +OPENAI_API_KEY=¿SECRET? # <--- not presently implemented for anything +SUMMARY_MODEL=dolphin-mistral +SUMMARY_CHUNK_SIZE=4000 +SUMMARY_CHUNK_OVERLAP=100 +SUMMARY_TPW=1.3 +SUMMARY_LENGTH_RATIO=4 +SUMMARY_MIN_LENGTH=150 +SUMMARY_TOKEN_LIMIT=4096 +SUMMARY_INSTRUCT='You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.' +SUMMARY_INSTRUCT_TTS='You are an AI assistant that summarizes emails -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary. Your response will undergo Text-To-Speech conversion and added to Sanjays private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following.' +DEFAULT_VOICE=joanne +WHISPER_CPP_DIR='whisper.cpp' +WHISPER_CPP_MODELS=tiny,base,base-en,small,medium,medium-en,large-v3 +WEBCLIPPER_TTS=elevenlabs +EMAIL_SUMMARY_TTS=local +YEAR_FMT="%Y" +MONTH_FMT="%Y-%m %B" +DAY_FMT="%Y-%m-%d %A" +DAY_SHORT_FMT="%Y-%m-%d" +#─── notes: ────────────────────────────────────────────────────────────────────────────── +# +# The exact values here will depend on what software you are using to inference an LLM, +# and of course what models and capabilities are available through it. The script was +# designed for use with `ollama`, but most of the functionality should be equal with +# LM Studio, LocalAI, ect... +# +# DEFAULT_LLM is self-explanatory; DEFAULT_VISION is used for image recognition within +# a multimodal chat context, such as on the ig module for generating intelligible +# comments to Instagram posts, or more realistic captions for sd-generated images. +# +# Note it's possible to specify a separate model for general purposes and for +# summarization tasks. The other SUMMARY_ variables call for some explanation, +# in particular six that are most relevant when summarizing very long documents: +# +# SUMMARY_CHUNK_SIZE: determines the maximum length, in tokens, the pieces that are +# split and sent individually to the model. +# +# SUMMARY_CHUNK_OVERLAP: determines how much of each chunk is overlapped with the prior +# and next chunks. Set too high causes repetition, set too low +# causes misunderstood confusion and poor summary results. +# The summarization algorithm is flawed but I've gotten the best +# results with this set around 100–200. +# +# SUMMARY_TPW: used in estimating the token count of a prompt for purposes of +# complying with the maximum tokens a model can handle at once. +# Best you can do is estimate. I tend to use long words a fair +# excessively and found my average was 1.3 tokens per word. YMMV. +# +# SUMMARY_LENGTH_RATIO: this is the primary control over the length of generated +# summaries, expressed as the ratio of original text length to +# summary length. The default, 4, means the summaries will be +# around 1/4 the length of the original text you provide it. +# +# SUMMARY_MIN_LENGTH: the default SUMMARY_LENGTH_RATIO of 4 isn't ideal for very +# short texts, but setting it any lower sacrifices conciseness +# in summaries of longer texts. In short one size doesn't fit +# all. The compromise I landed on was to set a "maximum minimum" +# summary length: under no circumstances will the script impose +# a smaller maximum length than this value. +# +# SUMMARY_INSTRUCT: sets the prompt used when summarizing text. +# +# SUMMARY_INSTRUCT_TTS: sets a separate prompt for use when summarizing text where +# tts output was requested; tends to yield "cleaner" audio +# with less numbers (page numbers, citations) and other +# information extraneous to spoken contexts. +# +# DEFAULT_VOICE: used for all tts tasks when a specific voice is not requested. +# +# ────────── +# +# +#────,-_/────────── W E C A N E X P E R I M E N T W I T H ──────────.─────────── +# ' | ,~-,-. ,-. ,-. ,--. | --' ,--. ,-. ,--. ,-. ,-. |-- . ,-. ,-. +# .^ | | | | ,--| | | | --' | -,- | --' | | | --' | ,--| | | | | | | +# `--' ' ' ' `-^ `-| `--' `---| `--' ' ' `--' ' `--^ `' ` `-' ' ' +# , | ,-. | ~ 𝙸 𝙽 𝚃 𝙷 𝙴 𝙽 𝚄 𝙳 𝙴 . ~ +# `~~' `-+' +# O R F U L L Y C L O T H E D ── U P T O Y O U +# +#─── via comfyui (stable diffusion): ───── ( B U T L E T M E K N O W , Y E A H ? ) +COMFYUI_URL=http://localhost:8188 +COMFYUI_DIR=/Users/sij/workshop/ComfyUI +COMFYUI_LAUNCH_CMD="mamba activate comfyui && python main.py" +OBSIDIAN_BANNER_SCENE=wallpaper +PHOTOPRISM_USER=NOT_IMPLEMENTED +PHOTOPRISM_PASS=NOT_IMPLEMENTED +ANONYMIZED_TELEMETRY=False +#─── notes: ────────────────────────────────────────────────────────────────────────────── +# +# COMFY_URL, as you may expect, should point to the URL you use to access ComfyUI. If you +# don't know, watch for it in the server logs once ComfyUI is fully launched. +# +# COMFYUI_DIR, with similar self-evidence, should point to the base directory of your +# ComfyUI installation (i.e. the folder that contains `models`, `inputs`, and `outputs`). +# It can handle either a +# +# PhotoPrism integration is not yet implemented, so don't bother with that just yet. +# ────────── +# +# D O N ' T M I S S O N E ─────────────────────────────────────── +#\ F I N A L S M A T T E R I N G O F Ⓜ Ⓘ Ⓢ Ⓒ Ⓔ Ⓛ Ⓛ Ⓐ Ⓝ Ⓨ \ +# \ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\ +# \ _ _ _/\\\\_ _ _ _ _ _ /\\\\ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\ +# \ _ _ \/\\\\\\_ _ _ _ /\\\\\\ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\ +# \ _ _ \/\\\//\\\_ _ /\\\//\\\ _ _/\\\ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\ +# \ _ _ \/\\\\///\\\/\\\/ \/\\\ _ _///_ _ _/\\\\\\\\\\_ _ _ _/\\\\\\\\_ _\ +# \ _ _ \/\\\ _\///\\\/ _ \/\\\ _ _/\\\ _ \/\\\////// _ _ _/\\\////// _ _\ +# \ _ _ \/\\\ _ _\/// _ _ \/\\\ _ _/\\\ _ \/\\\\\\\\\\_ _ /\\\_ _ _ _ _ _\ +# \ _ _ \/\\\ _ _ _ _ _ _ \/\\\ _ _/\\\ _ \////////\\\_ _\//\\\ _ _ _ _ _\ +# \ _ _ \/\\\ _ _ _ _ _ _ \/\\\ _ _/\\\ _ _/\\\\\\\\\\_ _ \///\\\\\\\\_ _\ +# \ _ _ \///_ _ _ _ _ _ _ \///_ _ _///_ _ \////////// _ _ _ \//////// _ _\ +# \ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\ +# ─────────────────── A N D O T H E R W H A T - H A V E - Y O U S ── +# +#─── other needful API keys, mainly: ──────────────────────────────────────────────────── +CF_API_BASE_URL=¿SECRET? # <--- Cloudflare API URL +CF_TOKEN=¿SECRET? # <--- Cloudflare Token +VISUALCROSSING_BASE_URL='https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline' +VISUALCROSSING_API_KEY=¿SECRET? # <--- VisualCrossing API key (for Weather) +ELEVENLABS_API_KEY=¿SECRET? # <--- ElevenLabs API key (for TTS) +COURTLISTENER_BASE_URL='https://www.courtlistener.com' +COURTLISTENER_API_KEY=¿SECRET? # <--- CourtListener API key (for court docket entries) +TIMING_API_URL='https://web.timingapp.com/api/v1' +TIMING_API_KEY=¿SECRET? # <--- API key for macOS/web app Timing (time tracking) +PUBLIC_KEY_FILE=sij.asc # <--- public PGP key (served at /pgp) +MAC_ID=¿SECRET? # <--- Tailscale hostname for primary macOS (alerts) +MAC_UN=¿SECRET? # <--- Primary macOS username +MAC_PW=¿SECRET? # <--- Primary macOS password +#─── notes: ────────────────────────────────────────────────────────────────────────────── +# +# +# CF_TOKEN: a Cloudflare token. This is used on the cf router for quick +# deployment of new domains in tandem with Caddy and for ddns. +# +# VISUALCROSSING_API_KEY: used for obtaining weather forecasts. It is a very data-rich +# yet affordable source of weather info, with a generous free +# plan. +# +# ELEVENLABS_API_KEY: used when on the tts router if tts tasks are outsourced to +# the state-of-the-art models at ElevenLabs. +# +# COURTLISTENER_API_KEY: used primarily on the hooks router, but likely relevant only +# to legal professionals that will be aware what it is for. +# +# TIMING_API_URL: are used on the time router for generating various tasks +# & related to timekeeping, as well as on the notes router for +# TIMING_API_KEY: generating markdown-formatted timeslips. It requires an +# active subscription to the Timing app (macOS or web), but +# it's worth noting comes included in the SetApp subscribtion +# bundle, for the same price, last I checked, as subscribing to +# Timing alone. If you have a Mac and somehow don't know this +# already, SetApp is an utterly insane value. I pay $15/mo for +# apps that I would otherwise pay ~$100/mo for if subscribing +# individually. I want to say I wasn't paid to say this, but +# with those savings I almost feel like I was. +# +# MAC_ID: These last three variables are for a specific use case where +# MAC_UN: you want certain commands run, or alerts appearing, on a +# MAD_PW: designated macaOS computer. The alerts router is designed to +# deliver OS-level notifications to the specified Mac when a +# webhook gets a hit on specified keywords within the payload. +# Setting the MAC_ID to the TS_ID of the target Mac, allows +# the script to readily know whether it itself is the target +# (this is relevant in a load-balancing context), and how to +# reach the target if not — to wit, ssh using MagicDNS. + diff --git a/sijapi/config/config.py b/sijapi/config/config.py new file mode 100644 index 0000000..6fc0b72 --- /dev/null +++ b/sijapi/config/config.py @@ -0,0 +1,98 @@ +import os +import yaml +from time import sleep +from pathlib import Path +import ipaddress + +import yaml + +class Config: + def __init__(self, yaml_file): + with open(yaml_file, 'r') as file: + self.data = yaml.safe_load(file) + + def __getattr__(self, name): + if name in self.data: + value = self.data[name] + if isinstance(value, dict): + return ConfigSection(value) + return value + raise AttributeError(f"Config has no attribute '{name}'") + +class ConfigSection: + def __init__(self, data): + self.data = data + + def __getattr__(self, name): + if name in self.data: + value = self.data[name] + if isinstance(value, dict): + return ConfigSection(value) + return value + raise AttributeError(f"ConfigSection has no attribute '{name}'") + + def __setattr__(self, name, value): + if name == 'data': + super().__setattr__(name, value) + else: + self.data[name] = value + +# Load the YAML configuration file +CFG = Config('.config.yaml') + +# Access existing attributes +print(CFG.API.PORT) # Output: localhost + +def load_config(): + yaml_file = os.path.join(os.path.dirname(__file__), ".config.yaml") + + HOME_DIR = Path.home() + BASE_DIR = Path(__file__).resolve().parent.parent + CONFIG_DIR = BASE_DIR / "config" + ROUTER_DIR = BASE_DIR / "routers" + + DATA_DIR = BASE_DIR / "data" + os.makedirs(DATA_DIR, exist_ok=True) + + ALERTS_DIR = DATA_DIR / "alerts" + os.makedirs(ALERTS_DIR, exist_ok=True) + + LOGS_DIR = BASE_DIR / "logs" + os.makedirs(LOGS_DIR, exist_ok=True) + REQUESTS_DIR = LOGS_DIR / "requests" + os.makedirs(REQUESTS_DIR, exist_ok=True) + REQUESTS_LOG_PATH = LOGS_DIR / "requests.log" + DOC_DIR = DATA_DIR / "docs" + os.makedirs(DOC_DIR, exist_ok=True) + SD_IMAGE_DIR = DATA_DIR / "sd" / "images" + os.makedirs(SD_IMAGE_DIR, exist_ok=True) + SD_WORKFLOWS_DIR = DATA_DIR / "sd" / "workflows" + + + + try: + with open(yaml_file, 'r') as file: + config_data = yaml.safe_load(file) + + vars = { + + + "API": { + + } + } + + + config = Config(config_data) + return config + except Exception as e: + print(f"Error while loading configuration: {e}") + return None + +def reload_config(): + while True: + global config + with open('config.yaml', 'r') as file: + config_data = yaml.safe_load(file) + config = Config(config_data) + sleep(300) # reload every 5 minutes \ No newline at end of file diff --git a/sijapi/config/llms.json-example b/sijapi/config/llms.json-example new file mode 100644 index 0000000..c75165d --- /dev/null +++ b/sijapi/config/llms.json-example @@ -0,0 +1,151 @@ +{ + "Alpaca": { + "models": [ + "mythomax", + "openhermes", + "deepseek" + ], + "prefix": "\n### Instruction:\n", + "stops": [ + "### Instruction" + ], + "suffix": "\n### Response:\n", + "sysPrefix": "### System\n", + "sysSuffix": "\n" + }, + "Amazon": { + "models": [ + "mistrallite" + ], + "prefix": "<|prompter|>", + "stops": [ + "<|prompter|>", + "" + ], + "suffix": "<|assistant|>", + "sysPrefix": "", + "sysSuffix": "" + }, + "ChatML": { + "models": [ + "dolphin", + "capybara", + "nous-hermes-2" + ], + "prefix": "<|im_end|>\n<|im_start|>user\n", + "stops": [ + "<|im_end|>", + "<|im_start|>" + ], + "suffix": "<|im_end|>\n<|im_start|>assistant\n", + "sysPrefix": "<|im_start|>system\n", + "sysSuffix": "<|im_end|>" + }, + "Llama2": { + "models": [ + "llama2-placeholder" + ], + "prefix": "\n\n[INST] ", + "stops": [ + "[/INST]", + "[INST]" + ], + "suffix": "[/INST]\n\n", + "sysPrefix": "", + "sysSuffix": "\n\n" + }, + "Mistral": { + "models": [ + "mistral-instruct", + "mixtral-8x7b-instruct" + ], + "prefix": "\n[INST] ", + "stops": [ + "[/INST]", + "[INST]", + "" + ], + "suffix": "[/INST]\n", + "sysPrefix": "", + "sysSuffix": "\n" + }, + "Orca": { + "models": [ + "upstage", + "neural", + "solar", + "SOLAR" + ], + "prefix": "\n### User:\n", + "stops": [ + "###", + "User:" + ], + "suffix": "\n### Assistant:\n", + "sysPrefix": "### System:\n", + "sysSuffix": "\n" + }, + "Phi2": { + "models": [ + "phi-2" + ], + "prefix": "\nSangye: ", + "stops": [ + "###", + "User Message" + ], + "suffix": "\nAssistant: ", + "sysPrefix": "Systen: ", + "sysSuffix": "\n" + }, + "Phind": { + "models": [ + "phind" + ], + "prefix": "\n### User Message\n", + "stops": [ + "###", + "User Message" + ], + "suffix": "\n### Assistant\n", + "sysPrefix": "### System Prompt\n", + "sysSuffix": "\n" + }, + "Vicuna": { + "models": [ + "xwin", + "synthia", + "tess" + ], + "prefix": "\nUSER: ", + "stops": [ + "", + "USER:", + "SYSTEM:" + ], + "suffix": "\nASSISTANT: ", + "sysPrefix": "SYSTEM: ", + "sysSuffix": "\n" + }, + "Zephyr": { + "models": [ + "zephyr" + ], + "prefix": " ", + "stops": [ + "" + ], + "suffix": "\n ", + "sysPrefix": " ", + "sysSuffix": "\n" + }, + "default": { + "prefix": "\n### Instruction:\n", + "stops": [ + "### Instruction" + ], + "suffix": "\n### Response:\n", + "sysPrefix": "### System\n", + "sysSuffix": "\n" + } +} diff --git a/sijapi/config/logging.conf b/sijapi/config/logging.conf new file mode 100644 index 0000000..9967700 --- /dev/null +++ b/sijapi/config/logging.conf @@ -0,0 +1,21 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler + +[formatters] +keys=consoleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=consoleFormatter +args=(sys.stdout,) + +[formatter_consoleFormatter] +format=%(asctime)s %(name)s %(levelname)s %(message)s \ No newline at end of file diff --git a/sijapi/config/sd-example.json b/sijapi/config/sd-example.json new file mode 100644 index 0000000..3361f1c --- /dev/null +++ b/sijapi/config/sd-example.json @@ -0,0 +1,43 @@ +{ + "scenes": [ + { + "scene": "default", + "triggers": [""], + "API_PPrompt": "(Highly-detailed) image of ", + "API_SPrompt": "; ((masterpiece)); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", + "API_NPrompt": "`oil, paint splash, oil effect, dots, paint, freckles, liquid effect, canvas frame, 3d, bad art, asian, illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, explicit, topless`", + "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", + "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this scene description to its essence, staying true to what it describes: ", + "workflows": [{"workflow": "turbo.json", "size": "1024x768"}] + }, + { + "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:0.5), 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" + } + ] + }, + { + "scene": "wallpaper", + "triggers": ["wallpaper"], + "API_PPrompt": "Stunning widescreen image of ", + "API_SPrompt": ", masterpiece, (subtle:0.7), (nuanced:0.6), best quality, ultra detailed, ultra high resolution, 8k, (documentary:0.3), cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, (eliot porter:0.6), (frans lanting:0.4), (daniel kordan:0.6), landscapephotography, ultra detailed, earth tones, moody", + "API_NPrompt": "FastNegativeV2, (easynegative:0.5), canvas frame, 3d, ((bad art)), illustrated, deformed, blurry, duplicate, Photoshop, video game, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, (Thomas Kinkade:0.5), sentimental, kitsch, kitschy, twee, commercial, holiday card, modern, futuristic, urban, comic, cartoon, FastNegativeV2, epiCNegative, easynegative, verybadimagenegative_v1.3", + "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": "landscape.json", "size": "1160x768"}] + } + ] +} diff --git a/sijapi/config/sd.json b/sijapi/config/sd.json new file mode 100644 index 0000000..8aa59de --- /dev/null +++ b/sijapi/config/sd.json @@ -0,0 +1,74 @@ +{ + "scenes": [ + { + "API_NPrompt": "`oil, paint splash, oil effect, dots, paint, freckles, liquid effect, canvas frame, 3d, bad art, asian, illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, explicit, topless`", + "API_PPrompt": "(Highly-detailed) image of ", + "API_SPrompt": "; ((masterpiece)); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", + "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_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.", + "scene": "default", + "triggers": [ + "" + ], + "workflows": [ + { + "size": "1024x768", + "workflow": "turbo.json" + } + ] + }, + { + "API_NPrompt": "FastNegativeV2, easynegative, 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_PPrompt": "Stunning widescreen image of ", + "API_SPrompt": ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscapephotography, ultra detailed, earth tones, moody", + "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_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.", + "scene": "landscape", + "triggers": [ + "landscape" + ], + "workflows": [ + { + "size": "1160x768", + "workflow": "landscape.json" + } + ] + }, + { + "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_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", + "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_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.", + "scene": "wallpaper", + "triggers": [ + "wallpaper" + ], + "workflows": [ + { + "size": "1080x512", + "workflow": "wallpaper.json" + } + ] + }, + { + "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_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.", + "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: ", + "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.", + "scene": "portrait", + "triggers": [ + "portrait", + "profile", + "headshot" + ], + "workflows": [ + { + "size": "768x1024", + "workflow": "selfie.json" + } + ] + } + ] +} \ No newline at end of file diff --git a/sijapi/data/loc_overrides.json b/sijapi/data/loc_overrides.json new file mode 100644 index 0000000..f006591 --- /dev/null +++ b/sijapi/data/loc_overrides.json @@ -0,0 +1,8 @@ +[ + { + "name": "Echo Valley Ranch", + "latitude": 42.8098216, + "longitude": -123.049396, + "radius": 1.5 + } +] \ No newline at end of file diff --git a/sijapi/data/sd/workflows/base.json b/sijapi/data/sd/workflows/base.json new file mode 100755 index 0000000..f231c8c --- /dev/null +++ b/sijapi/data/sd/workflows/base.json @@ -0,0 +1,220 @@ +{ + "4": { + "inputs": { + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": "API_PPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "API_NPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "16", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "API_", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "10": { + "inputs": { + "text": "API_SPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "14": { + "inputs": { + "conditioning_1": [ + "6", + 0 + ], + "conditioning_2": [ + "10", + 0 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + }, + "15": { + "inputs": { + "batch_size": 1, + "width": 1023, + "height": 1025, + "resampling": "nearest-exact", + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 8, + "persistence": 1.5, + "lacunarity": 2, + "exponent": 4, + "brightness": 0, + "contrast": 0, + "clamp_min": 0, + "clamp_max": 1, + "seed": 648867523029843, + "device": "cpu", + "optional_vae": [ + "4", + 2 + ], + "ppf_settings": [ + "17", + 0 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + } + }, + "16": { + "inputs": { + "seed": 863091325074880, + "steps": 10, + "cfg": 8, + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "start_at_step": 0, + "end_at_step": 10000, + "enable_denoise": "false", + "denoise": 1, + "add_noise": "enable", + "return_with_leftover_noise": "disable", + "noise_type": "brownian_fractal", + "noise_blending": "cuberp", + "noise_mode": "additive", + "scale": 1, + "alpha_exponent": 1, + "modulator": 1, + "sigma_tolerance": 0.5, + "boost_leading_sigma": "false", + "guide_use_noise": "true", + "model": [ + "4", + 0 + ], + "positive": [ + "14", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "15", + 0 + ], + "ppf_settings": [ + "17", + 0 + ], + "ch_settings": [ + "18", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" + } + }, + "17": { + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 8, + "persistence": 1.5, + "lacunarity": 2, + "exponent": 4, + "brightness": 0, + "contrast": 0 + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + } + }, + "18": { + "inputs": { + "frequency": 320, + "octaves": 12, + "persistence": 1.5, + "num_colors": 16, + "color_tolerance": 0.05, + "angle_degrees": 45, + "brightness": 0, + "contrast": 0, + "blur": 2.5 + }, + "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/default.json b/sijapi/data/sd/workflows/default.json new file mode 100755 index 0000000..9fc0705 --- /dev/null +++ b/sijapi/data/sd/workflows/default.json @@ -0,0 +1,298 @@ +{ + "10": { + "_meta": { + "title": "Power KSampler Advanced 🦚" + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "inputs": { + "add_noise": "enable", + "alpha_exponent": 1, + "boost_leading_sigma": "false", + "cfg": 4.5, + "ch_settings": [ + "12", + 0 + ], + "denoise": 1, + "enable_denoise": "false", + "end_at_step": 10000, + "guide_use_noise": "true", + "latent_image": [ + "13", + 0 + ], + "model": [ + "4", + 0 + ], + "modulator": 1, + "negative": [ + "7", + 0 + ], + "noise_blending": "hslerp", + "noise_mode": "additive", + "noise_type": "vanilla_comfy", + "positive": [ + "6", + 0 + ], + "ppf_settings": [ + "11", + 0 + ], + "return_with_leftover_noise": "disable", + "sampler_name": "dpmpp_2m_sde", + "scale": 1, + "scheduler": "karras", + "seed": 301923985151711, + "sigma_tolerance": 0.5, + "start_at_step": 0, + "steps": 20 + } + }, + "11": { + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "brightness": 0, + "contrast": 0, + "evolution": 0, + "exponent": 4, + "frame": 0, + "lacunarity": 2, + "octaves": 8, + "persistence": 1.5, + "scale": 5 + } + }, + "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", + 2 + ], + "persistence": 1.5, + "ppf_settings": [ + "11", + 0 + ], + "resampling": "nearest-exact", + "scale": 5, + "seed": 961984691493347, + "width": 1023 + } + }, + "23": { + "_meta": { + "title": "Ultimate SD Upscale" + }, + "class_type": "UltimateSDUpscale", + "inputs": { + "cfg": 7.5, + "denoise": 0.32, + "force_uniform_tiles": true, + "image": [ + "8", + 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 + ] + } + }, + "24": { + "_meta": { + "title": "Load Checkpoint" + }, + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "SD1.5/realisticVisionV60B1_v51VAE.safetensors" + } + }, + "31": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "24", + 1 + ], + "text": "" + } + }, + "32": { + "_meta": { + "title": "ConditioningZeroOut" + }, + "class_type": "ConditioningZeroOut", + "inputs": { + "conditioning": [ + "31", + 0 + ] + } + }, + "33": { + "_meta": { + "title": "Load Upscale Model" + }, + "class_type": "UpscaleModelLoader", + "inputs": { + "model_name": "4x-UltraSharp.pth" + } + }, + "34": { + "_meta": { + "title": "Save Image" + }, + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "API_", + "images": [ + "23", + 0 + ] + } + }, + "36": { + "_meta": { + "title": "Save Image" + }, + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "Pre_", + "images": [ + "8", + 0 + ] + } + }, + "4": { + "_meta": { + "title": "Load Checkpoint" + }, + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "Other/playgroundv2.safetensors" + } + }, + "6": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "API_PPrompt" + } + }, + "7": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "API_NPrompt" + } + }, + "8": { + "_meta": { + "title": "VAE Decode" + }, + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "10", + 0 + ], + "vae": [ + "4", + 2 + ] + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/landscape.json b/sijapi/data/sd/workflows/landscape.json new file mode 100755 index 0000000..8a6223a --- /dev/null +++ b/sijapi/data/sd/workflows/landscape.json @@ -0,0 +1,456 @@ +{ + "11": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "12", + 1 + ], + "text": [ + "25", + 0 + ] + } + }, + "12": { + "_meta": { + "title": "Load LoRA" + }, + "class_type": "LoraLoader", + "inputs": { + "clip": [ + "4", + 1 + ], + "lora_name": "SDXL/add-detail-xl.safetensors", + "model": [ + "4", + 0 + ], + "strength_clip": 0.3, + "strength_model": 0.33 + } + }, + "13": { + "_meta": { + "title": "Load LoRA" + }, + "class_type": "LoraLoader", + "inputs": { + "clip": [ + "12", + 1 + ], + "lora_name": "SDXL/SDXLLandskaper_v1-000003.safetensors", + "model": [ + "12", + 0 + ], + "strength_clip": 0.75, + "strength_model": 0.8 + } + }, + "14": { + "_meta": { + "title": "Power KSampler Advanced 🦚" + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "inputs": { + "add_noise": "enable", + "alpha_exponent": 1, + "boost_leading_sigma": "false", + "cfg": 8, + "ch_settings": [ + "19", + 0 + ], + "denoise": 1, + "enable_denoise": "false", + "end_at_step": 10000, + "guide_use_noise": "true", + "latent_image": [ + "20", + 0 + ], + "model": [ + "13", + 0 + ], + "modulator": 1, + "negative": [ + "61", + 0 + ], + "noise_blending": "cuberp", + "noise_mode": "additive", + "noise_type": "brownian_fractal", + "positive": [ + "63", + 0 + ], + "ppf_settings": [ + "18", + 0 + ], + "return_with_leftover_noise": "disable", + "sampler_name": "dpmpp_2m_sde", + "scale": 1, + "scheduler": "karras", + "seed": 809193506471910, + "sigma_tolerance": 0.5, + "start_at_step": 0, + "steps": 28 + } + }, + "18": { + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "brightness": 0, + "contrast": 0, + "evolution": 0.2, + "exponent": 5, + "frame": 40, + "lacunarity": 2.4, + "octaves": 8, + "persistence": 1.6, + "scale": 8 + } + }, + "19": { + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" + }, + "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", + "inputs": { + "angle_degrees": 45, + "blur": 2.5, + "brightness": 0, + "color_tolerance": 0.05, + "contrast": 0, + "frequency": 320, + "num_colors": 32, + "octaves": 24, + "persistence": 1.5 + } + }, + "20": { + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "batch_size": 1, + "brightness": 0, + "clamp_max": 1, + "clamp_min": 0, + "contrast": 0, + "device": "cpu", + "evolution": 0.2, + "exponent": 4, + "frame": 40, + "height": [ + "54", + 1 + ], + "lacunarity": 2.4, + "octaves": 8, + "optional_vae": [ + "4", + 2 + ], + "persistence": 1.6, + "ppf_settings": [ + "18", + 0 + ], + "resampling": "nearest-exact", + "scale": 8, + "seed": 189685705390202, + "width": [ + "54", + 0 + ] + } + }, + "21": { + "_meta": { + "title": "Conditioning (Combine)" + }, + "class_type": "ConditioningCombine", + "inputs": { + "conditioning_1": [ + "11", + 0 + ], + "conditioning_2": [ + "6", + 0 + ] + } + }, + "23": { + "_meta": { + "title": "String (Multiline)" + }, + "class_type": "JWStringMultiline", + "inputs": { + "text": "API_SPrompt" + } + }, + "24": { + "_meta": { + "title": "String (Multiline)" + }, + "class_type": "JWStringMultiline", + "inputs": { + "text": "API_NPrompt" + } + }, + "25": { + "_meta": { + "title": "String (Multiline)" + }, + "class_type": "JWStringMultiline", + "inputs": { + "text": "API_PPrompt" + } + }, + "28": { + "_meta": { + "title": "Tiled VAE Decode" + }, + "class_type": "VAEDecodeTiled_TiledDiffusion", + "inputs": { + "fast": false, + "samples": [ + "14", + 0 + ], + "tile_size": [ + "53", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "36": { + "_meta": { + "title": "Save Image" + }, + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "API_", + "images": [ + "52", + 0 + ] + } + }, + "4": { + "_meta": { + "title": "Load Checkpoint" + }, + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "SDXL/realismEngineSDXL_v20VAE.safetensors" + } + }, + "42": { + "_meta": { + "title": "Upscale Model Loader" + }, + "class_type": "Upscale Model Loader", + "inputs": { + "model_name": "RealESRGAN_x2plus.pth" + } + }, + "52": { + "_meta": { + "title": "Ultimate SD Upscale" + }, + "class_type": "UltimateSDUpscale", + "inputs": { + "cfg": 8, + "denoise": 0.24, + "force_uniform_tiles": true, + "image": [ + "28", + 0 + ], + "mask_blur": 8, + "mode_type": "Linear", + "model": [ + "12", + 0 + ], + "negative": [ + "7", + 0 + ], + "positive": [ + "21", + 0 + ], + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "seam_fix_denoise": 1, + "seam_fix_mask_blur": 8, + "seam_fix_mode": "None", + "seam_fix_padding": 16, + "seam_fix_width": 64, + "seed": 1041855229054013, + "steps": 16, + "tile_height": [ + "53", + 0 + ], + "tile_padding": 32, + "tile_width": [ + "53", + 0 + ], + "tiled_decode": true, + "upscale_by": 2, + "upscale_model": [ + "42", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "53": { + "_meta": { + "title": "Integer" + }, + "class_type": "JWInteger", + "inputs": { + "value": 768 + } + }, + "54": { + "_meta": { + "title": "AnyAspectRatio" + }, + "class_type": "AnyAspectRatio", + "inputs": { + "height_ratio": 3, + "rounding_value": 32, + "side_length": 1023, + "width_ratio": 4 + } + }, + "6": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "12", + 1 + ], + "text": [ + "23", + 0 + ] + } + }, + "60": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "13", + 1 + ], + "text": [ + "23", + 0 + ] + } + }, + "61": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "13", + 1 + ], + "text": [ + "24", + 0 + ] + } + }, + "62": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "13", + 1 + ], + "text": [ + "25", + 0 + ] + } + }, + "63": { + "_meta": { + "title": "Conditioning (Combine)" + }, + "class_type": "ConditioningCombine", + "inputs": { + "conditioning_1": [ + "62", + 0 + ], + "conditioning_2": [ + "60", + 0 + ] + } + }, + "7": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "12", + 1 + ], + "text": [ + "24", + 0 + ] + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/selfie.json b/sijapi/data/sd/workflows/selfie.json new file mode 100755 index 0000000..11db16d --- /dev/null +++ b/sijapi/data/sd/workflows/selfie.json @@ -0,0 +1,486 @@ +{ + "4": { + "inputs": { + "ckpt_name": "SDXL/hassansdxl_v10.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": [ + "17", + 0 + ], + "clip": [ + "15", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": [ + "18", + 0 + ], + "clip": [ + "15", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "12": { + "inputs": { + "lora_name": "SDXL/styleWegg.safetensors", + "strength_model": 0.3, + "strength_clip": 0.25, + "model": [ + "91", + 0 + ], + "clip": [ + "91", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "13": { + "inputs": { + "lora_name": "SDXL/add-detail-xl.safetensors", + "strength_model": 0.2, + "strength_clip": 0.2, + "model": [ + "12", + 0 + ], + "clip": [ + "12", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "14": { + "inputs": { + "lora_name": "SDXL/amazing_portraits_xl_v1b.safetensors", + "strength_model": 0.5, + "strength_clip": 0.45, + "model": [ + "13", + 0 + ], + "clip": [ + "13", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "15": { + "inputs": { + "lora_name": "SDXL/sd_xl_offset_example-lora_1.0.safetensors", + "strength_model": 0.2, + "strength_clip": 0.15, + "model": [ + "53", + 0 + ], + "clip": [ + "53", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "17": { + "inputs": { + "text": "API_PPrompt" + }, + "class_type": "JWStringMultiline", + "_meta": { + "title": "String (Multiline)" + } + }, + "18": { + "inputs": { + "text": "API_NPrompt" + }, + "class_type": "JWStringMultiline", + "_meta": { + "title": "String (Multiline)" + } + }, + "23": { + "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 🦚" + } + }, + "24": { + "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 🦚" + } + }, + "37": { + "inputs": { + "seed": 923916094743956 + }, + "class_type": "Seed", + "_meta": { + "title": "Seed" + } + }, + "38": { + "inputs": { + "batch_size": 1.3125, + "width": [ + "95", + 0 + ], + "height": [ + "95", + 1 + ], + "resampling": "nearest-exact", + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 10, + "octaves": 8, + "persistence": 1.5, + "lacunarity": 3, + "exponent": 5, + "brightness": 0, + "contrast": 0, + "clamp_min": 0, + "clamp_max": 1, + "seed": [ + "37", + 3 + ], + "device": "cpu", + "optional_vae": [ + "4", + 2 + ], + "ppf_settings": [ + "23", + 0 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + } + }, + "43": { + "inputs": { + "seed": [ + "37", + 3 + ], + "steps": 32, + "cfg": 8.5, + "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": [ + "15", + 0 + ], + "positive": [ + "98", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "38", + 0 + ], + "ppf_settings": [ + "23", + 0 + ], + "ch_settings": [ + "24", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" + } + }, + "44": { + "inputs": { + "samples": [ + "43", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "45": { + "inputs": { + "filename_prefix": "API_", + "images": [ + "44", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "53": { + "inputs": { + "lora_name": "SDXL/PerfectEyesXL.safetensors", + "strength_model": 0.5, + "strength_clip": 0.5, + "model": [ + "14", + 0 + ], + "clip": [ + "14", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "89": { + "inputs": { + "lora_name": "SDXL/ahxl_v1.safetensors", + "strength_model": 0.4, + "strength_clip": 0.33, + "model": [ + "92", + 0 + ], + "clip": [ + "93", + 0 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "90": { + "inputs": { + "lora_name": "SDXL/age.safetensors", + "strength_model": -0.8, + "strength_clip": -0.7000000000000001, + "model": [ + "89", + 0 + ], + "clip": [ + "89", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "91": { + "inputs": { + "lora_name": "SDXL/StokeRealV1.safetensors", + "strength_model": 0.2, + "strength_clip": 0.2, + "model": [ + "90", + 0 + ], + "clip": [ + "90", + 1 + ] + }, + "class_type": "LoraLoader", + "_meta": { + "title": "Load LoRA" + } + }, + "92": { + "inputs": { + "input": 0.36, + "middle": 0.5, + "out": 0.64, + "model1": [ + "4", + 0 + ], + "model2": [ + "94", + 0 + ] + }, + "class_type": "ModelMergeBlocks", + "_meta": { + "title": "ModelMergeBlocks" + } + }, + "93": { + "inputs": { + "ratio": 0.45, + "clip1": [ + "4", + 1 + ], + "clip2": [ + "94", + 1 + ] + }, + "class_type": "CLIPMergeSimple", + "_meta": { + "title": "CLIPMergeSimple" + } + }, + "94": { + "inputs": { + "ckpt_name": "SDXL/dreamshaperXL_alpha2Xl10.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "95": { + "inputs": { + "width_ratio": 5, + "height_ratio": 7, + "side_length": 1025, + "rounding_value": 64 + }, + "class_type": "AnyAspectRatio", + "_meta": { + "title": "AnyAspectRatio" + } + }, + "96": { + "inputs": { + "text": "API_SPrompt" + }, + "class_type": "JWStringMultiline", + "_meta": { + "title": "String (Multiline)" + } + }, + "97": { + "inputs": { + "text": [ + "96", + 0 + ], + "clip": [ + "15", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "98": { + "inputs": { + "conditioning_1": [ + "6", + 0 + ], + "conditioning_2": [ + "97", + 0 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/turbo.json b/sijapi/data/sd/workflows/turbo.json new file mode 100755 index 0000000..8369894 --- /dev/null +++ b/sijapi/data/sd/workflows/turbo.json @@ -0,0 +1,220 @@ +{ + "4": { + "inputs": { + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "6": { + "inputs": { + "text": "API_PPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "API_NPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "13", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "API_", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "11": { + "inputs": { + "batch_size": 1, + "width": 1023, + "height": 1025, + "resampling": "nearest-exact", + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 8, + "persistence": 1.5, + "lacunarity": 2, + "exponent": 4, + "brightness": 0, + "contrast": 0, + "clamp_min": 0, + "clamp_max": 1, + "seed": 704513836266662, + "device": "cpu", + "optional_vae": [ + "4", + 2 + ], + "ppf_settings": [ + "14", + 0 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + } + }, + "13": { + "inputs": { + "seed": 525862638063448, + "steps": 8, + "cfg": 1.6, + "sampler_name": "dpmpp_2m_sde", + "scheduler": "karras", + "start_at_step": 0, + "end_at_step": 10000, + "enable_denoise": "false", + "denoise": 1, + "add_noise": "enable", + "return_with_leftover_noise": "disable", + "noise_type": "brownian_fractal", + "noise_blending": "cuberp", + "noise_mode": "additive", + "scale": 1, + "alpha_exponent": 1, + "modulator": 1, + "sigma_tolerance": 0.5, + "boost_leading_sigma": "false", + "guide_use_noise": "true", + "model": [ + "4", + 0 + ], + "positive": [ + "20", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "11", + 0 + ], + "ppf_settings": [ + "14", + 0 + ], + "ch_settings": [ + "15", + 0 + ] + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "_meta": { + "title": "Power KSampler Advanced 🦚" + } + }, + "14": { + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "evolution": 0, + "frame": 0, + "scale": 5, + "octaves": 8, + "persistence": 1.5, + "lacunarity": 2, + "exponent": 4, + "brightness": 0, + "contrast": 0 + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + } + }, + "15": { + "inputs": { + "frequency": 320, + "octaves": 12, + "persistence": 1.5, + "num_colors": 16, + "color_tolerance": 0.05, + "angle_degrees": 45, + "brightness": 0, + "contrast": 0, + "blur": 2.5 + }, + "class_type": "Cross-Hatch Power Fractal Settings (PPF Noise)", + "_meta": { + "title": "Cross-Hatch Power Fractal Settings 🦚" + } + }, + "20": { + "inputs": { + "conditioning_1": [ + "6", + 0 + ], + "conditioning_2": [ + "21", + 0 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + }, + "21": { + "inputs": { + "text": "API_SPrompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/wallpaper.json b/sijapi/data/sd/workflows/wallpaper.json new file mode 100644 index 0000000..ed4732a --- /dev/null +++ b/sijapi/data/sd/workflows/wallpaper.json @@ -0,0 +1,332 @@ +{ + "11": { + "_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, + "octaves": 8, + "optional_vae": [ + "4", + 2 + ], + "persistence": 1.5, + "resampling": "nearest-exact", + "scale": 5, + "seed": 490162938389882, + "width": 1023 + } + }, + "13": { + "_meta": { + "title": "Power KSampler Advanced 🦚" + }, + "class_type": "Power KSampler Advanced (PPF Noise)", + "inputs": { + "add_noise": "enable", + "alpha_exponent": 1, + "boost_leading_sigma": "false", + "cfg": 1.6, + "ch_settings": [ + "15", + 0 + ], + "denoise": 1, + "enable_denoise": "false", + "end_at_step": 10000, + "guide_use_noise": "true", + "latent_image": [ + "11", + 0 + ], + "model": [ + "4", + 0 + ], + "modulator": 1, + "negative": [ + "7", + 0 + ], + "noise_blending": "cuberp", + "noise_mode": "additive", + "noise_type": "brownian_fractal", + "positive": [ + "20", + 0 + ], + "ppf_settings": [ + "14", + 0 + ], + "return_with_leftover_noise": "disable", + "sampler_name": "dpmpp_2m_sde", + "scale": 1, + "scheduler": "karras", + "seed": 697312143874418, + "sigma_tolerance": 0.5, + "start_at_step": 0, + "steps": 8 + } + }, + "14": { + "_meta": { + "title": "Perlin Power Fractal Settings 🦚" + }, + "class_type": "Perlin Power Fractal Settings (PPF Noise)", + "inputs": { + "X": 0, + "Y": 0, + "Z": 0, + "brightness": 0, + "contrast": 0, + "evolution": 0, + "exponent": 4, + "frame": 0, + "lacunarity": 2, + "octaves": 8, + "persistence": 1.5, + "scale": 5 + } + }, + "15": { + "_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 + } + }, + "20": { + "_meta": { + "title": "Conditioning (Combine)" + }, + "class_type": "ConditioningCombine", + "inputs": { + "conditioning_1": [ + "6", + 0 + ], + "conditioning_2": [ + "21", + 0 + ] + } + }, + "21": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "API_SPrompt" + } + }, + "22": { + "_meta": { + "title": "Ultimate SD Upscale" + }, + "class_type": "UltimateSDUpscale", + "inputs": { + "cfg": 8, + "denoise": 0.21, + "force_uniform_tiles": true, + "image": [ + "8", + 0 + ], + "mask_blur": 8, + "mode_type": "Linear", + "model": [ + "4", + 0 + ], + "negative": [ + "23", + 0 + ], + "positive": [ + "6", + 0 + ], + "sampler_name": "euler", + "scheduler": "normal", + "seam_fix_denoise": 1, + "seam_fix_mask_blur": 8, + "seam_fix_mode": "None", + "seam_fix_padding": 16, + "seam_fix_width": 64, + "seed": 470914682435746, + "steps": 20, + "tile_height": 512, + "tile_padding": 32, + "tile_width": 512, + "tiled_decode": false, + "upscale_by": 2, + "upscale_model": [ + "24", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "23": { + "_meta": { + "title": "ConditioningZeroOut" + }, + "class_type": "ConditioningZeroOut", + "inputs": { + "conditioning": [ + "7", + 0 + ] + } + }, + "24": { + "_meta": { + "title": "Load Upscale Model" + }, + "class_type": "UpscaleModelLoader", + "inputs": { + "model_name": "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth" + } + }, + "26": { + "_meta": { + "title": "Upscale Image (using Model)" + }, + "class_type": "ImageUpscaleWithModel", + "inputs": { + "image": [ + "22", + 0 + ], + "upscale_model": [ + "24", + 0 + ] + } + }, + "27": { + "_meta": { + "title": "Image Resize by Factor" + }, + "class_type": "JWImageResizeByFactor", + "inputs": { + "factor": 0.5, + "image": [ + "30", + 0 + ], + "interpolation_mode": "bicubic" + } + }, + "30": { + "_meta": { + "title": "ImageBlur" + }, + "class_type": "ImageBlur", + "inputs": { + "blur_radius": 3, + "image": [ + "26", + 0 + ], + "sigma": 1.5 + } + }, + "4": { + "_meta": { + "title": "Load Checkpoint" + }, + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "Other/dreamshaperXL_v21TurboDPMSDE.safetensors" + } + }, + "6": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "API_PPrompt" + } + }, + "7": { + "_meta": { + "title": "CLIP Text Encode (Prompt)" + }, + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "API_NPrompt" + } + }, + "8": { + "_meta": { + "title": "VAE Decode" + }, + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "13", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "9": { + "_meta": { + "title": "Save Image" + }, + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "API_", + "images": [ + "27", + 0 + ] + } + } +} \ No newline at end of file diff --git a/sijapi/data/sd/workflows/wallpaper_fast.json b/sijapi/data/sd/workflows/wallpaper_fast.json new file mode 100644 index 0000000..fdfc521 --- /dev/null +++ b/sijapi/data/sd/workflows/wallpaper_fast.json @@ -0,0 +1,281 @@ +{ + "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": [ + "27", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "11": { + "inputs": { + "batch_size": 1, + "width": 1023, + "height": 1024, + "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": 490162938389882, + "device": "cpu", + "optional_vae": [ + "4", + 2 + ] + }, + "class_type": "Perlin Power Fractal Latent (PPF Noise)", + "_meta": { + "title": "Perlin Power Fractal Noise 🦚" + } + }, + "13": { + "inputs": { + "seed": 697312143874418, + "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)" + } + }, + "23": { + "inputs": { + "conditioning": [ + "7", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "24": { + "inputs": { + "model_name": "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth" + }, + "class_type": "UpscaleModelLoader", + "_meta": { + "title": "Load Upscale Model" + } + }, + "26": { + "inputs": { + "upscale_model": [ + "24", + 0 + ], + "image": [ + "8", + 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" + } + } +} \ No newline at end of file diff --git a/sijapi/helpers/calendar/exportCal.scpt b/sijapi/helpers/calendar/exportCal.scpt new file mode 100644 index 0000000000000000000000000000000000000000..8937955ad00d5a88293115c58ba09f1eede0661a GIT binary patch literal 2748 zcmb_eSyxk66#ni71nc$2#wR=OkG{m!J>L%K7d-`<~(Z_T6`%+ZjpsY+AC>-`3g|)U6yq zafJvh*eJ4SK+(3g(yxETHi~(t1Y;DTC{7!2Q7UicjY28xlo{rt3}Y3+j+*{hIB*q# zGfLs#9m7iIH3V9*V6)I4)((GO$ z|DsZ!IlNT0#v-Y7Z*Na367Nr&b>T;ZeT7O`1IA;?Q%9bn%+>zrwsgvtXMiOyG(6Yv zTEi=?+|5oZMVUNtG%sp=!A&cNMC$Dz)beOi=`*(#PGV!mkKMrH@g$r--v{GLbX@w~TUAJAiZ33Co)gO<+$=aQ? zExD&P-PM{Vv(;j{A{b3o^wh~Mxv5Z>)ucW3Xix;hsfqz_EJvf}G{cJ-%)h1Ya#Cn0 zaJk{i4I?)iiN^+eBB{a1Kn$LIZykG<+|qn*YChB1n$WBWh7VIkuFEyKYRPpy=$eMB zdSo+O3uY>U8Ci|_Xq78+SurMdm=m=pfS@9n*W8bJXtUAAiL)?U5zI^Nr?NSis|fZS zPV-~BT*5quQ0#0^#TeUpSv@tVm5Z)i{D>Zfxdr_fT)BWrLAaT|SYEX0EX~Ek#%vOr zj|GZg2sKdrmuOc66Y&NHdto^i>WDA$ViEZUnAbX^`)FmD@d0MU4*K+4kSXLudx6J! zSI(m*h>D(AI@3QG$@C`%laWl{JB}_oY%Cz3ukf`Z7(UI6%r{u92&U>aBZwvFPz00J z&1jJ!SB5@md4-Owsul#~oFnJ5m9SS~ae?evSI*{x*2|1FM0Esp`esRfO_EFHjAHWf z@mmb}Tse(p2wA+qL#q)r{%q3wIWr$_e3`GvR&t4TJLO1}wm?EMty zC>{3v-A=xQ`lam3J``Kd=o%Z$3aw@}SYCnU*3V_kuK)SBJ+(Ol+d}?Wu%)P9~rq%OpO>_i77sTi5g?kB>4G@fGf7KKbC!t```-USRloJcq0R zG&@VKXt=Cv$V|2sa!O%Eq1v336LQ>=lX}nz4aYfBPH8^Om5NI4S|tBqC01Fyu==g! zm}8C{;~^c1{g6p8l!M#iao{J4{{>C_m17l3E9GyjmZNe6Yp_-hJM4};Lt%CP+hji0 z*;vQTOAxjZrr|}G&0N!`(Rv%}Y1CqajSVzfZ( zMV0)8Y4Rr;uI~_SV>;uA)1EYL;II=(Y8RN*l+!uta7a#GP!R5B2 zpSLba>G>Z%xm?oK^P?l4C64ULRc)7|MytE7?Ea*J<;bpFiE?FUe#H+KC9fRDf(u(b zur9S+t`8jx@4i|WIv(D0y)JYzd^2=9eA|brraOBBp>yGTq4VKG5meTPzK1v5wW%VK Oj;2_~JTHfae*F)@*P+?~ literal 0 HcmV?d00001 diff --git a/sijapi/helpers/calendar/updateCal b/sijapi/helpers/calendar/updateCal new file mode 100755 index 0000000..42ef363 --- /dev/null +++ b/sijapi/helpers/calendar/updateCal @@ -0,0 +1,2 @@ +#!/bin/bash +osascript /Users/sij/workshop/sijapi/helpers/updateCal.scpt diff --git a/sijapi/helpers/calendar/updateCal.scpt b/sijapi/helpers/calendar/updateCal.scpt new file mode 100755 index 0000000000000000000000000000000000000000..401b465718b64f360615fa78801818b43c89f998 GIT binary patch literal 32442 zcmchA2Y^&H_ix&acrO_kMJa-g(gdU=-i2(1Y=Cq zvzBotnB<5sjX0l)eEO#ZQ}`yv;u?W0Mx3#b?PiUQG~11da#t*5b=dA2fyi^|-C4jw zjR0@Nkh2@BYyY+XXjYerQdeR(w3E=(SZAbTsBueRw=fmVs+d0uUuE8mv3mAz`l9-kJ&$2efvj%k47B|(&ZI}h56;_;-U&yFH5mB(wfz0yu{^j`+H!2XLUl#&zn%Gp*xvxon1K_A}_u`*W9X0i%49kE%gBVt8t21_Ge zgj7Cam3TU8wpSLzA4B^iYubs`Eto&6th}-c$Es8Y%U=k~p9#x7@HAojXao++JpfQs z`-A;nv!)(P%4x>-)d(Dxd!V)bSV|*sSndI;Y!2<_oiwV!TgTHqL9YE1Y=6S^fFwHr zIOhS2VphOP9Zuz}1MtgZ6`Jjv#p}D!e&+~Tg;!B=0SkP55aQhaKsbF*IPHU{IcuR2 zIGh>*^p^Hp`;BHTJx-LhcL^gzc&kKwn3;Wva~3p@w0 z_8Ni15W;7_wqMyVx&4|7eMP~SRI)vu4kRKSs3fOuV839USZ9t8Ru{20ta)l+X~mrK zf>h;nJn8aj1(m7%qVj^g%ChnWse<`M6_pjJ1`^ttknECVUDzP7b08R2!RA0n3y`XW znEKB&a;70(iQIg!lD^MFio|&V@(S2|&m)m1P+ybfz!eEbNiWQ2Q!fa zcqykl>!A@iC2|03XFp|!*iQm{H0n^XprWi`enB4fb!(RrhRpZ-Lp*a2V6EAqtfxlc z1n>aJMXvO zus#|=HJ3lI@7wpd{eTL+Pr-XsvJalVte-|u&EQ;IcZtjWfo1np|8#NQoZ@t4 zK`Nhu@^tC6g4BV<>59r!*HlSaY31~aRApJJv|xTVqYY{X;|CD21}4GsVlc43>#uoW zeX-L)#CETZC{~WVQYQc@^d~AotWJT!DG<60-;teG>{}3zD#GR$=U1i}#uXBg(mBP& z+`b9Q**AzCL$jUn_0Yb~cJIVum1X(q1w`O?D0rKwJQ&YWY?wyiVCey%hTGTdtC|h> zuu#qjmZK4%L=1fH$41&$?8};sWa1de59J)qMrj02)AV3H?MtD3Y3nvM8|7c`34V^Y zFWMJ08_mSAc%5>_u(29}ljWZ5F#Ei;&$Hb$8|x+XFxJOz2c8)b><7(^-*5fVMdZP>6}ZOjfM% z;o7F05>~1axVCfgPFVu|3vJIzLZ#KO>b2FQtVYLkTmWP2gQ0!Usjj?qaY1Q*y1X=9 zQo!nFQX`P6i}u>H)~Qhivx?Js1*v&OmDAmbK}Az#g{cK)bIMbhHvlly24G#wW+&Nf zbchs~UjRTzA><*J!UU(tseR|PZ;<_P7U9fZX? z`+&Wl+jT^@2Pn9oh(}{rC7YuWIL1wj@6G1g`|Q1%&GjP;<;-LAH3BzxE{X5O76`O1 z*g|%^M&Mp7VI}q+d$+xd+k2??yD7Mfnm8WMB6fmC;J8tOcNeoI8iD8r$V%88yOu2t z5U5~Yc>%y)>WM#_RoXj4d*@Dwui26;v(|)m4QtYgg&41^=}n3LRm>CY1OT**E!PMf zKnoE$k*&}OoOmomiIoYqlK68{lAVNgLT4IcS7ckcBeZw8Rx(Y@DavPICNXx%0Kt}{ zm6O>i8i6Cwa<<&wZf~=GHu)2ut#p0E9Ehx=QInE#u&rGs2F%L*VgZOC$%@=7TDgX;fqTN*DG&Hh`ouv^tq?hBR zv)MTs0UC_~+*8@P_GWvNX6O3RopM&Q^E3k91$9L%xv^RwI zhFYo4?ahSmP1McL$?!*ku6x8g{Kl-~hja z-OtdZM&JOygWb(;u$Ne1e1iv^a&BZ`=#3umHSBJCQD84}fOiAnH+eAbU~BA!p}nw1 zz%L=dFDAgR&jNlyXfN0X@RC_>gs29*WR?f~S^$1CyG0{#fIq;VV7Ic{Gy(_s1MCrY zyFK4R(r)*FQx1UA2;97K9ec#C4(w_NcozV^#)I(yTW8M=?YT7qem((y9szz^7VvXI zdk#CW6Wha4xL|%>!K})nGU#=T?mll|CeI^q3lO-It z?7*Ilnd;QCvblxnVt{yew!yPPdzMpSMWuPgbMn2Qv$|C&C+mW(MI-kxsNH+KsIO=1 z?J9ewg%VgrSe!}086@iW;<=CAuMwz85cTzZ2qQ|I&K}f=h{2S{{$>v`jJ^+f8n0&` zfrh8qQ#D8{2DuU+u}9fs8iDBJ(yy?`8Rip@d%)MT7wjpaJ!KbItiez>SiZnswkL=7 z{?civ*}+LfVQS?dJlG$Pum#Q%pq z9X@5hv#0F}d!lAfdpc0gGYk{8XPD>%*gj<&!QS;6frIT+_Jv&@+U488w&OmkL3a1f z7wl`hEVRpNq{FkG4&Sn$*#`E!M&RE2mVIxRhM0rXg12^kpxN{Oo$uLCc1dWL)JlgH zM28cJ4(q%K|I0ahfxV~^I1&DrMRsvu7gvdJW%(S;)L!&0fGGBa(4Md}5rzPwfd`#X zas{D$3F5HGVv6t*6Nkg{rg*?!VOS8n;wnaZH@h&j3;&DGsM)KG4+S>6@!jnLVX+9& z>}99&x#k1eYj!?+U9;Cb5Y2fX_6B>?&a)V;-f$AYoAYjvwmG&^v$vcCxSY4yJDR=C zL?2Y$oFB;EW$$SO#1*7l*ox3ruvVQ|vLKx|eOr~`vbNC}8shEVY{6UG^3ayoCj4_ckG4}oJGI7fuYmB&Ck}7S zaxER&v_rv{pF|25&x@=i1`}dmNA$G9Z=G>{pM(BtF?58`@)QKte+| zI$_M^Q|vK;JqGWS<%-)Wgj_Bm_d^!B$)TO>YI5k%Y`l((xvBVOAoUyjT_bQv74muP z5B8@~D=gz=n_XY_^5{qY==|mzVI_c2Zy`0ffBiX`t|rCq^Lv{MSyj z6EyobQ&S0xn9&HTYMRL`2dHpE5HY6lnRa|&$2*R8?yNcTNXHcNX?9#_$JIy-{cn^C-^eCqf(7(R?>gtR?(JJ1VfFGQ{F_ zJhA5UCD7SN+mV{@o~daG4|u3CEI4A#=O^>x(v}6KerK!R7=u#}liY=5VPHvF7sy zc0_1L)I==qeI2N6u^k@R;nl=Cnus+L#Nu&JtP}YPJ1nrnGQ{F4ORS?pdlcKZ6Vr}7 z#btSEnk7~xhgBs77a&)H*V8=V$#ojPl-K7;&Fgz4PvaNzJ@}rQ!ok52-`ftcM{2%zrlxavL*7X9hMAhq;*EI|%^Q1iozBm) zg9AI*ldFs7O+2|yma`g{wf7YTCtMAB_Ur>=(UgYN0RXGE)IfB;%(e~j@HHSBXRO4m*M&69?t9dhz z{AK)FzMt)9`)a(4>dJaiA0p^sK7nJ4|RAr2+ z&JY|9BHn_x)Ew>zLh&YkH*dvTYu?JEcoSd45462)FOBJj(?XQfhPTxmW@VC;oA~XR z862c}JCEYc{C3+jusuDB-8DbRqj(d)*&Z6&Lu*74-WTHU?fedVNMH}CMzJ@c*o#nX zo#kVX(DraOITYuV7iSdds9}P`L&V$j4w|?3NZrF9;T?G=%{zLe?&0frXWQL^{+&G% zl+%TG)zG6(0p82k*=~XD=8#C|Yu?o(aSy-Ob`5RU8j#?&I{|wz0oy4HY?sh>ab^bx zEUY7BmJVH_Z*lQ}^a8;<0OW2Q0Ks8Ee0Y>U$Gh_$n!{m2qwu5rDa>{{TOa|K2+^H# z4&^;Hhf9QX_GA1h+cB^m9moYxLp?pnkMhTChtPJY5oCJVb|R4B(tzTkf3|&S+h;+p zs7zPRsi*=Oi+v;nhiixT;=MKR;*A;X$UHJ{-NJ zj|X`Jf62BDY+DC1R{FfJ2YEf;VB3VYO^qNQL?E{#kgHrb1oDBQJus^YOUm+#3X2Nz zt2E*8s-(aiQt*Dfzvlfs*st@C_y9go^8p_0*ZF&VkZo;SX+FqHJmn1L&~R`b5anOz z@9-gfsK(mCQT`47j%^v(mJW^15F+o3K)E+;i_o^H0S(QM^b-FLf7dn-Y;(Ntrw*+N zy;g+Yz$`}&2<-t`^ePHw&v9Ma>;2JHNx}OAy`%Ur&5!cveatuV;e3SV!##Q*^DlUg z-QT7(&++I`4yJ0Fk0e#k`6v7fyI)}U^MaJt+`BpW$NUq!Z)o?e0Ud7lC$Lil_J}Ol z%|hEOyHHG5R>Cx0P*72|aO|5&3l8T5AH_#&?i?qAZ{nM5Q@f9C!fjKkcpnOyU^2|% zN8w}mSk1=}O5Bxf%*XNZ+%{&cE*}SX^d70qP?*XqE=ZRal}^L#iY54X*dqCaB%c5` zU<&rfGG~^qge{u9T4baqFE)9okn(PEBv-Kk||UqJX?K$xE@-ggnHw%>kl?)eQBNANg;*jL*_gE3T*f!hf@S26j(~ zmh-&wS-!*n$bYeWgm#atd}p|~-9$$78NT`7`Coi?0ux{~U!LUUYzla{2u}sT$j4`E z_}S8M5Rbsq0y`C^x>N;x)yIIZ9a($#*AX6_*8lOQFIB#6DPbFs1U95#ZxV9&gfiMP z8QP?S-N{-Oc9+af!t%kj3SOx(k0&KBgxHhM;d3>g~6J^Z-pmc1F=7r z4JT>7GLvq`Pv)m+ezGTL1JRJ5n&77rIZsRS)3z0QI7lG$qM>NYPv>W7e!3@J6VcSh z0voF)-5H*A4Mh{HLaTO)x#lPN=9`HWKQqD4q~=#8`6{RsFZA2SnC+j-Efi^8ehy@)b!zAcE<@`_YZX|)das`p%idv%hpcAmuXChUCaM;VtG#Hn7oE(%q4{^GY|{Kv-+WimonMpS*HH7}+QYTo3E`nA*#Y}=auGikWt>hF#_gixBtV#4X?|g~0GWS6 z^UqEp6a0Me+#&cZ`Aw-FKSfsPb@5M z=H^clwVSZDYp>K+i?!{>^qUFmTax@1?BJqZECmpiJP1q?T-CLV>^Gv>TlsC8-|B^? zj~Gj<56y4)Leoba&DWSe%d`?eLDd%V!}7f15@`2(8Y?}cWNIMVzQm|v=e<^eA> z{ly^jb7+3v#pSK$cX=R&h+%wPg0CYW9!&BF|33_+=2znV9~Asf!UYFf)lh1F3e8Ur z12=YV#dB8Qb=qQu8M~vBrss{HX+ocV{hsI?12j`GESQ=i)?h9DjyCtNAmY zi^quL%y)tLu9}O_dM-{B$Cz(J^X;zEz-%Te{z$w=r z%u?9*^gkbKcc@MUe-}`HXg<~aLoY9ju?+f%f2{dOUS1Z9v-l_G6N8*jyu2XCe8fLB z9|q>5TIJHIVPx#pi`(kEl3`laSycvdYI%lKCb{uN>Rb&`L*t-QdSL6fm% z;v_79H)#$J0}LHvg*eH45SS0D zZ`K?t2>y9-eq8bI`48qD^S0*SJ6EVUUtESY+)tYS=&5wRxX8Q}nzw4T3~Bz8r_Dv; zQvP#-|4g*`CCPtbdtm=sUrhe`V-M;u{0(FSG>3NtelBq(Y~8;K{wtvRjbp_9jl@WZ zE5()OP4k9%otrm_CfMuA&Fd)7$pP>O|5I~#M`$s1rMLrY!oM|#cZAf-mEsn@h5w`Z zmQ4Bv{x6R-|JUPkrMSkt7Mj;;*}kWF4`m) z1Kl%%jnVucFK9Q2o6W0%c@=_ITv~{g`rlbbz7m>OYH3#RKY=;W)9A_$w>9EPEajx8 zT*7UQcvL76(?Vs^4~n?hO^bLYeIHhAyK7O$vGE0LO zLi0i`;0OV~j;JfN7Ii%ho)T}1gs7(lJQzfSr^M@`zDQ~T4~9>_EcOt4Y5@<1Pd_gj zh`qFc2ZMlqN<3?x56$znG^zy-VbD4r=Ffe2ww)77`d<Nozm! zY!ZKqgUnOrNiA@;VhB({j(I|~H;)JAiQ0x90fzy}&L;7@=pZ_30kgbM|13I*&RW2z zPx7%zd?&gj1gxcNMc1Uj*$1pT=p+wqiEWSlyoLB|jgu;Uv93ztv!4s|t5Or4$pzHv z>;e8x{3N=W$IPQz!2F+w1}NuX(Orv!J^j8DKbS{C^GMc`3X^;d+j~TJ59ANxNAqxC z9)_ejv$A=NbmyZO7eq&2;%D)j=#daG`>z#;B*h^(^hCCxb`JE30J{WFK5H$^<_*iYSw9Ss1q?MA$dJTFD-gHnj_sx z^cII{0fRkwfb;?4aB+kdhiB4(=p*`S(Z@B4@8jly&^%B}qgueh0RG9i++Flbh<=24 z|D@>84saqi5ZZh|&En;baKvOF_ZI`q{pLO`;4*-CQI5G+3^exy=H6PxYoG@`kj=y( zF<4`Vn?o&-jm42-h!)-Agjc7^z|4CZ&dSeX}++YqLrXR{Q_lhoQwtx2s>)mwSn$#V9R~_VleU zljcro?qnFkMmbrLN!h@x3CtSE3LRZ*`2g9{+#Z$}N; zVyd~t+^o^6lU3xHo3IGJF)%mPDy#4;5a8WpUy&~gw8+n-4-@8-P8_e}u;LHRR zrOTNqinV}Sf+V+x>}jqG&2_t^D6!fi6ne^D=GwqqTP=b&kO*B*OfT>y4wHRENkWjb zLzE`5u)x8?9Vp5%4txT>C5Ed>q&RP+s&1hG8_P9L(9kMG4w6I6HG#RNS|lfCMe^#< zTwRM42r(X_S0-j@QRYQ2N6r$nMY$HUJ%@5+v8WK0TENRd9L$ked5gJP%*mw3iFsna z7V|vGa^xs;RcNlNrBN;BdqRwoW5t4mfOlc7SeO(GS>3G^C&X^9=hHYjO&o8oG*@VG zycav#p z|Bcu!_2^8L$B7forREYXPV{0&IV;3UE&O2%IZ+;CE)LDbJ5`t~J^07SW6ec@p*`+? zY`&C4I9;4!&NgRhak|^;io6TOnPQa|XJ*o;inGMoTAYPt zDdYJ5cyW$6SBrDJ1Xs#=W>sibIis{&k7QkNE}3z`X>qQn?mW3rtWJp4MBVd}0!V0a zwy%7#T#0q>1zMc%D_<;^nKMI!vnxB1SE=r1vaJaYxw&)7{ zIiE5#hj(<$!9g>z$~Sw4TrDmZkg$t=vuDV&%o(9MV;7pe#5a4EJlC8anA5@Gl8RDj z=rg>2xJ2GAE)|z)fukg(4=#~6i^~PNCY%yJeLWV-S7~u&CVjQIT3n-rcSfN6<>s`| zoK}m3XaUCqb<4};RpQ!&xRy}AE-9|Vl7AS>am)TOSVZQ~a#@S3Jc?J#>&&U4Id!ML z1TC)cLVUfv$($0HQ>uj+&KGEX`e#lK&B?WZD9qU;q-Q}$1r8aB>%|RPT<>M@F8QOl zQQTxsGU(Cp3Jd}!cgeTJ&EggR1e zFw3f?>?SW|@5=AQ{o(;Fu&J4(>Rq`}tP>Avu`ZMTLOdiM)&iz_|NST85rGxYBfkB2 zO*)WjCW#Y^I4Enf0XY>|;!5Sj(M+=RE1%LsO- zR}$hCYU0(Tc(rzkd)76s>Zp3+HSxL@uervN4$XZ0-{r<%clfDLCCt3Q%&V5br=0|< zy;U3WhImtpH=Kka-BP?I-qzx+OnQGBlC*dylWr#76Yp#Bo`+{|)!57p&D>g8t_AGs zps{MKnu-q+;sYYUhe`1v41xn-gBT4>Hwt=gBrFou+U{LneKVCZb3!v`7cID2yzL}Z z?XOyx%D_}sOQ`p7I0>x?O+_u>3bTkL^mvj`xS_;H;$tnGeO{;zs)H#vv&}4SU_Iq# zHU+b&7X7KUx+WY zfQ245UDZWBB)$^jD-`=$Y}Ddw*Ex`V0xT%sm|`E(R1Rf3TSmni<*Qv4#zQTKwo)cCb1`{FD%|O|KO{C&kayExz}a z_g4MIFXC4%e({y}R!5kk&=l=L<-hvMk5K)@Zwc`mRsMTY{9b#9_|7*wR2?n;5Pxd% zhi`VM8g8bCX8JBP`=@VqxEg7u1!h`xSNO(vg$Zhb_)Gk)g|~&P32KhmBL30B+rp8a zE&dge7XSLLFhS+Qpl7u(9+C-avMCHrVJ)(y15#!DS zbbXR4Ye1CusWv``Fi-a`wyeSY|2?IN!I9<(@Iw3Wo zm`KXRt}hZmFm4Jcpz}B4AFpd>t4echV2-WUHGezUovt}1G{@8;55mxqB}1+pu@Osz zdS#K%Qwf=(X7^9Z{cCSF>9xUi>VA2EY_6p@-mB}>-Li#jsbve_ajsKqWGmTP z%T}56t@1$GMoVwJNBJAg*wBovh5K51!VJ+$oZ&Y2?poEav}Fsi*rw)Ue!bCf#>Qe7PmgOru$vwGDTI1GXe zArD3ihsZ;&tqrG!-a&QULGO)XnBMO`g8TY>?`|e>25fRsn69nvcDN*25Q;g>rBcSAdw#6 zy!R-v5w^0yS`PC1=?k^d3I>69H2s~A3pW{LuyfaI^sU-s zuH`__@r`Pe=@*!O)p`~Bhns;!J9q?8pW{s5(DZfYoJ?hOM+&Q(NsT~C$UZ>vNI68y zBRz^gsK4bXfC6unhl{i=R5y#v#`8pS?@;t_=6QCSpwg{Idw z!dFm|E-Gf#!Z(nT!kLYaHt4oTdv(4A>>VXXn?ubZT8{GSdrK@O$H=i-j>)8xu+fdz za-1i`mY9$eOb^pthPv6YNr~O~Y(6(V2x}b4(Q>?NG#0~j;W+$vKp|CE=9+G%tCqP;Kmc6M6q(j? ziUR}bI;Kl#y6jRX(lYJesuK%MXZ+tOZMo@25IUtW)KZ_64rVcykiYw`^Y@g z$w0s4c?c;dUlwSY@7rn+YiK%zro%2mSm57k7;9qMho=2b$jeP9g0CY{5dIeo?y=^v zQ)Qu$g%JE{a=Mn&oZut7gDjFWv@G&~H;=W2!SNu|PRp52@KK_zES4pj))WwYqz{y( zvP{cTC-|`zu>(!p!1%*JX|SZsgW5dS!n6rZo1F-LiIaoafw4B`z`z_>E%*l!0__Na z8LmFpw_Aq>_Iuo3km=1;2b`+YRcD;!bf7s)&en1kp(SD+V;xN^)6%ryrWIA(l7beb z6=8gr<+4Iccb5@&C7Z)iIft9(jMbHu?slC1bQZZevwvW4bz{w^3gl8ZAwc;)a+zGN<+4n=w>(j<(DFo2^sce) za%DoUBt%Y1%9FO)i6vKfes_=ck|)bkv^?4K`_Nb~lL|}PZ9;$aAC7>!gR>M4gi0h>*PybOR}C9l@bY@WPMUa#eKnRJD`LEfk(%#|cpQ(|TECV8`#H)YZ@Wtri3E*9(}9 zV3<7K>KmCBn_-fnN!Hlkw7l6@J~LJ-?@Y)$sq(c+xfXu#o{V;A4S@w>49kJFLEh*q zEsK?#`k|>`i%4mCz3(U$vAL#RV6b-eJNG2K85u>82u)&}j-IPxwEOln-E^a0=Mx{1A%u#9c3D z@J?nE*g66L)hp_C11q)4?k1>~`u{kPHniN4bZQCD0F5FS#4eUECgh7GJ})KZOGM!fzVgds*U6XV zD_XwnE5AH;RTPC$vYqc9Hyayz9YwRxhx_m=RSo#U4 zTVr=b|Avu2231Y@9qOAme6x4N)<*vX(Ldbls&(H~*-M=b^jEkE#OAC5g1{T@cY?*cWwYl7yi(QjddGe@}g++Q#4s8AJmHi8I8 zrC`4$3@l*)uao>xex&7x9@h=A4biXBFVW9D`jra(LIJGcLVkqjWBG}eu>2FQ8)6^I zPvvJ?e(G`E5PMgCF2B$cRuLK&HpJePU&^nv{4$e%RemiuYWcNi!iLz3a#KQXBHn+K zl;7Y6VLBFEhRa8Ly7OgpGdLae>j*8Cx%-iJ7-JCF=t=Zq?3L)JF#2gL_uV!-jOACp zsaIof$Zr$!TWadNr2Gzh!?wS;yQ*TiB#2LM#@>m345J@i|Ib{6x&5t}T7KqAJG%c6 zMnAAUIKgb`o{J|6Equ6G6^?ug! zr%Za2{6+q%le$EB{Q& zKY{rK7~GF!IruvYv9Zn*g&`0w3ab3e*v4pc7;WAmE?WNV2ft0R@1pO5h>j(>ZR--= z6TIyx@ogA=TMKAH!kr@jioVejZU>^r&v7RImRq#^+vEFl>|gnh{8!6=GU>l$B#oAl z2l40FZ_%bO@>jO+Sj@GAi-Hu)Z?Qk6O-MK))+&}%j5Vm5Q*65(U;gXy`YZNNv@wV_ zR^zoLi`Um-gab@9l3YaJP|tb?{l&bE7zei%kG|51yXk#A7Oxw9sYLWe5PexSz0WTw zPSXw5bW^pvt-54txmLn~j`DSs#D7-jh!qPz8+=EDLescNQGQwIQQ zXcfH^M(@-DfL6^s_O0S=)V>J?XV6-;UsCM{6;3CKMq!~>wRr#%)q!==3Qi|1;p1)N z?W4DY=*J}YlD|nNjfa2ZbhpILS z1%J_6)i$Zx0>_cS+53s%A;!=}XVCrcq-0~%&*&FZI+8SK)yh+(XZ*0}wIISpnpJYp zB1@51!|2so0OHY`M3Fa$BKzZMrw-Dpou^3u_z2ZLdL?>UtM;BElmkzZR&euR4ixVn zAF4X3&RTWy6zCrx6ulHiFV)hZR^IOu9~2)Fy%y0SG4bE;z&kr%?qAC;JY7ZWYXi*p{l1= zhkC?v;-jMVVYI$h#Cm$xj*5>}y%MSyv9@|LDqxcovR}PmG=mqUWkv z2$xo7#_?t5qMj zCWz<7&%mw?)ekTBR|B-_PcL$0=S5Gcfoc$so?_6Y10kRXrN)=zo(s41COc1+TFI=& z=|^A6%5g~{R4sneMqsp1@K~)?gOh47re$;hmu@5Q=YujDc86LOW&JN|WJ=LHC6Iku zuhVrH^y4d}n_(puY5-tAQVr4SNIw(Fi_edqR70aDg6PTG42^22XLeq^QXQp+X?0X4 zJxdK&=w8D!>0*_mMrxJg8Jrg{R7WS&(S+Nmq#Cu2;ZcorhR1kee5M+$#%ML#Gq@-| zGkQFT9tVS+p;wLZl2sTliXIE2$7*4)RwI1F#qlyVHlg4nS*yk+)i@Rh8F0mgR>ORw zv*HzMyqcgD96h8eXU8j|M}z3m>PF#pf>gz4#b-y4gpof;Rr`WH>bV>~` zn_KlOJ=;i4MwYP04?o+JzwOm2>Nr9?H>vQ`89;nEq=D8=Sr0)iQsPR{Z?t42ij0k< zUC9namZI(Mz!K^h;4vk7K&vTUsusl0i0)VE=)NGjzgDSAd#PCvKLxvA^0b$5!R=J(oisg#)~Qz? zlTaDmre>>h9^HnY%2BgX=GGv>xuOo~g1MDtm|4@hgt4Wl?`+l<{}QU4?N&d!MO8#M zM>nZTH7B|;h;FIn4XrAi3I&DERC9%zi^gMWo=|X0VJ$XaC>Y;a6Kq0L3lM3p77Dcx zk*?}^q2QcigVZ9S;GAMdVJo15`-F{Ci-lT@NLnosY6&9K)l#9BB2uZA3AGHwsH>I> zwH%SX)QLi!h)7DU5NZV??bJ%4RwB|xog~yrh#VH(Afg+HL^l(OZX#k-f;iVl*G1Qc z(e*VftHS8IUA_i7jIOQKj96dIVV6bML{~>wMOQ{wutw44Z2#yo);78{x`g$LE{-mW zE{raS&X3NER!8SX=di-)Y*rSXrBt*kI+Lx8&WKKrPK!>BPKi#APKs7WE20yl<CrTf+kvA( zemt(pJ(FL-ui?1jGAh6wlCSZ%_*eWJ{xdgGz6iwLxZ0?T=pp)vp<=X{f@?WS!~(iU zL97-RMtR~|oIbx>JSJWcuZRzDO!YU2TpfushO((l$+ogfG&M>`Q=;7Hxae4UOms{% zS+10)Mw8@fd0{kB-XZUlFUnWtCi!hNLH;4FlF@jzXEaXjtJS#3<``U_w=o<8k z%r6E85za)__*;WPbQO9e{cd3pU5Oa&mJ5mCm=5pwM*>5zqV^xy3&E(YOlyM+-pF6`uJR1nSarMrYt+6A(`^C^r99R$uno;~~K zl9{t_VN|e-M-PH1AFY#JC5Y%!fAXY+(bS!~E5azfOD?$}n&PlacMhZ6oow?6BHI2; zJK@6U*li%#k)JS#=>C0_29+l}x>b|R-(bAm2A?pRRHK_Yj3zp5xZ@27VKiZfhV&pB zU!~f2WC9MOajwoCFXWTajqz2V&Zx`y=Ldb!YT%nWPk+^N;M+ORZfrU5-JGYtX*uxy zobUEv;pS(*Kd2>2CC28o#Yeq?Q&|0+Tvk8LlDW9|c0Z)|^yvna-WTb;a?)&Xd^W^q zBYZZ-XOlf*x3o>OeR5gjj>u|?tY-M!7q2!=v;Ep5p2}$$J1Li?Ca2l{U~O?-)_mr4 z?9{j_%~~9U?_1^|){0`SDRv;m+T;`>Y>Qf2=dwNJ?o7MbUa>T5*D9dqT2|*H=JV;h zX7Fs$n-kgKT-IjBTwJn#SVPtW@$H)DSNjS=eEQe3ugd9@b0*r4uvD842--GNPXMli z2(I=7VF#k?6xNaAoe)p6&h1@J7s~0nbxt?RIe6=w?v&HR*Ki2M5B2e$6z@fC^rqNh z6g!+^jxtA3tdB3*m*V}1vi*sZH>cTvT-FWTOtXQGqxfbJ_y7V7s^W1~&XGPR&4z4U zZfGX=sI7B{<+ARs&f%Hd5vUX77?G`$az^@`G&_3ha-%Z2qpNer;@!j3Yz%KE3v*e+ zG#d-XjcWOT}#=ruEa6; z;#gcsIn&9oD|g(s<(#0P^<0#klBp}bZ5a>WRFud=U9Oh=?MjR!^tz>40e7;BSRp5Q z%%rBdR5RAdfi)dq4yp#VO3r#>qlJGPu^HL7k=pj{nRv73_HTEGk=?(ISV{J6q_%y# z6mO2$_HB^$^jvmCnw9w)5Sx{)0jW$4tsL~TL5OmccZ8_emJn6A^u^91Ul(F?vUMRf z*QGL)mD${6H4?4R#UG?rc(Cp_{fv{y2QFe6|nCGFE z^V016%o|nd3${t4A!@Pey9=}5fnM#>>>^a*D06Yk=c|+gTDXMbX?AJL^;LOe>!tDg O7>u*JTR#5y_WuKOH*fX; literal 0 HcmV?d00001 diff --git a/sijapi/helpers/calendar/updateCal2.scpt b/sijapi/helpers/calendar/updateCal2.scpt new file mode 100644 index 0000000000000000000000000000000000000000..6531a6b8a3df74048db1b1f3518db0575097dee5 GIT binary patch literal 372356 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

DHt^JZ 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

  • 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 literal 0 HcmV?d00001 diff --git a/sijapi/helpers/courtlistener/clHooks.py b/sijapi/helpers/courtlistener/clHooks.py new file mode 100644 index 0000000..683f8aa --- /dev/null +++ b/sijapi/helpers/courtlistener/clHooks.py @@ -0,0 +1,195 @@ +from fastapi import FastAPI, Request, BackgroundTasks, HTTPException, status +from fastapi.responses import JSONResponse +import httpx +import json +from pathlib import Path +import asyncio +from datetime import datetime +import os, io +from PyPDF2 import PdfReader +import aiohttp + +hook = FastAPI() + + +# /Users/sij/Library/CloudStorage/OneDrive-WELC/Documents - WELC-Docket +SYNC_FOLDER = Path(__file__).resolve().parent.parent +HOME_FOLDER = Path.home() +DOCKETS_FOLDER = HOME_FOLDER / "Dockets" +SEARCH_FOLDER = HOME_FOLDER / "Watched Cases" +SCRIPTS_FOLDER = SYNC_FOLDER / ".scripts" +REQUESTS_FOLDER = HOME_FOLDER / "sync" / "requests" +COURTLISTENER_BASE_URL = "https://www.courtlistener.com" +COURTLISTENER_DOCKETS_URL = "https://www.courtlistener.com/api/rest/v3/dockets/" +COURTLISTENER_API_KEY = "efb5fe00f3c6c88d65a32541260945befdf53a7e" + +with open(SCRIPTS_FOLDER / 'caseTable.json', 'r') as file: + CASE_TABLE = json.load(file) + +@hook.get("/health") +async def health(): + return {"status": "ok"} + +@hook.post("/cl/docket") +async def respond(request: Request, background_tasks: BackgroundTasks): + client_ip = request.client.host + logging.info(f"Received request from IP: {client_ip}") + data = await request.json() + payload = data['payload'] + results = data['payload']['results'] + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + payload_file = REQUESTS_FOLDER / f"{timestamp}-{client_ip}_docket.json" + with open(payload_file, 'w') as file: + json.dump(payload, file, indent=2) + + for result in results: + background_tasks.add_task(process_docket, result) + return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK) + +async def process_docket(result): + async with httpx.AsyncClient() as session: + await process_docket_result(result, session) + + +async def process_docket_result(result, session): + docket = str(result.get('docket')) + case_code, case_shortname = get_case_details(docket) + date_filed = result.get('date_filed', 'No Date Filed') + + try: + date_filed_formatted = datetime.strptime(date_filed, '%Y-%m-%d').strftime('%Y%m%d') + except ValueError: + date_filed_formatted = 'NoDateFiled' + + # Fetching court docket information from the API + url = f"{COURTLISTENER_DOCKETS_URL}?id={docket}" + headers = {'Authorization': f'Token {COURTLISTENER_API_KEY}'} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + if response.status == 200: + logging.info(f"Fetching CourtListener docket information for {docket}...") + data = await response.json() + court_docket = data['results'][0]['docket_number_core'] + court_docket = f"{court_docket[:2]}-cv-{court_docket[2:]}" # Formatting the docket number + case_name = data['results'][0]['case_name'] + logging.info(f"Obtained from CourtListener: docket {court_docket}, case name {case_name}.") + else: + logging.info("Failed to fetch data from CourtListener API.") + court_docket = 'NoCourtDocket' + case_name = 'NoCaseName' + + for document in result.get('recap_documents', []): + filepath_ia = document.get('filepath_ia') + filepath_local = document.get('filepath_local') + + if filepath_ia: + file_url = filepath_ia + logging.info(f"Found IA file at {file_url}.") + elif filepath_local: + file_url = f"{COURTLISTENER_BASE_URL}/{filepath_local}" + logging.info(f"Found local file at {file_url}.") + else: + logging.info(f"No file URL found in filepath_ia or filepath_local for one of the documents.") + continue + + document_number = document.get('document_number', 'NoDocumentNumber') + description = document.get('description', 'NoDescription').replace(" ", "_").replace("/", "_") + description = description[:50] # Truncate description + # case_shortname = case_name # TEMPORARY OVERRIDE + file_name = f"{case_code}_{document_number}_{date_filed_formatted}_{description}.pdf" + target_path = Path(DOCKETS_FOLDER) / case_shortname / "Docket" / file_name + target_path.parent.mkdir(parents=True, exist_ok=True) + await download_file(file_url, target_path, session) + logging.info(f"Downloaded {file_name} to {target_path}") + + +def get_case_details(docket): + case_info = CASE_TABLE.get(str(docket), {"code": "000", "shortname": "UNKNOWN"}) + case_code = case_info.get("code") + short_name = case_info.get("shortname") + return case_code, short_name + + + +async def download_file(url: str, path: Path, session: aiohttp.ClientSession = None): + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36' + } + async with aiohttp.ClientSession() as session: + logging.info(f"Attempting to download {url} to {path}.") + try: + async with session.get(url, headers=headers, allow_redirects=True) as response: + if response.status == 403: + logging.error(f"Access denied (403 Forbidden) for URL: {url}. Skipping download.") + return + response.raise_for_status() + + # Check if the response content type is a PDF + content_type = response.headers.get('Content-Type') + if content_type != 'application/pdf': + logging.error(f"Invalid content type: {content_type}. Skipping download.") + return + + # Create an in-memory buffer to store the downloaded content + buffer = io.BytesIO() + async for chunk in response.content.iter_chunked(1024): + buffer.write(chunk) + + # Reset the buffer position to the beginning + buffer.seek(0) + + # Validate the downloaded PDF content + try: + PdfReader(buffer) + except Exception as e: + logging.error(f"Invalid PDF content: {str(e)}. Skipping download.") + return + + # If the PDF is valid, write the content to the file on disk + path.parent.mkdir(parents=True, exist_ok=True) + with path.open('wb') as file: + file.write(buffer.getvalue()) + + except Exception as e: + logging.error(f"Error downloading file: {str(e)}") + +@hook.post("/cl/search") +async def respond_search(request: Request, background_tasks: BackgroundTasks): + client_ip = request.client.host + logging.info(f"Received request from IP: {client_ip}") + data = await request.json() + payload = data['payload'] + results = data['payload']['results'] + + # Save the payload data + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + payload_file = REQUESTS_FOLDER / f"{timestamp}-{client_ip}_search.json" + with open(payload_file, 'w') as file: + json.dump(payload, file, indent=2) + + for result in results: + background_tasks.add_task(process_search_result, result) + return JSONResponse(content={"message": "Received"}, status_code=status.HTTP_200_OK) + + +async def process_search_result(result): + async with httpx.AsyncClient() as session: + download_url = result.get('download_url') + court_id = result.get('court_id') + case_name_short = result.get('caseNameShort') + case_name = result.get('caseName') + logging.info(f"Received payload for case {case_name} ({court_id}) and download url {download_url}") + + court_folder = court_id + + if case_name_short: + case_folder = case_name_short + else: + case_folder = case_name + + file_name = download_url.split('/')[-1] + target_path = Path(SEARCH_FOLDER) / court_folder / case_folder / file_name + target_path.parent.mkdir(parents=True, exist_ok=True) + + await download_file(download_url, target_path, session) + logging.info(f"Downloaded {file_name} to {target_path}") \ No newline at end of file diff --git a/sijapi/helpers/courtlistener/subscribeAlerts.py b/sijapi/helpers/courtlistener/subscribeAlerts.py new file mode 100644 index 0000000..62a2017 --- /dev/null +++ b/sijapi/helpers/courtlistener/subscribeAlerts.py @@ -0,0 +1,32 @@ +import json +import requests + +# Load the caseTable.json file +with open('caseTable.json', 'r') as file: + case_table = json.load(file) + +# Set the base URL and authorization token +base_url = "https://www.courtlistener.com/api/rest/v3/docket-alerts/" +auth_token = "a90d3f2de489aa4138a32133ca8bfec9d85fecfa" + +# Iterate through each key (docket ID) in the case table +for docket_id in case_table.keys(): + # Set the data payload and headers for the request + data = {'docket': docket_id} + headers = {'Authorization': f'Token {auth_token}'} + + try: + # Send the POST request to the CourtListener API + response = requests.post(base_url, data=data, headers=headers) + + # Check the response status code + if response.status_code == 200: + print(f"Successfully created docket alert for docket ID: {docket_id}") + else: + print(f"Failed to create docket alert for docket ID: {docket_id}") + print(f"Status code: {response.status_code}") + print(f"Response content: {response.content}") + + except requests.exceptions.RequestException as e: + print(f"Error occurred while creating docket alert for docket ID: {docket_id}") + print(f"Error message: {str(e)}") diff --git a/sijapi/helpers/database/dbrestore.sh b/sijapi/helpers/database/dbrestore.sh new file mode 100755 index 0000000..273410e --- /dev/null +++ b/sijapi/helpers/database/dbrestore.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +DB_NAME="weatherlocate.db" + +# Step 1: Backup existing data +echo "Backing up existing data..." +sqlite3 $DB_NAME < ... + +# Update complication on device from which this function was installed with a number of content parameters that can be string, progress, icon, target or color. + +# Each argument type is derived from input. + +# Progress has the form: 50% or 110/220 + +# Icon must match valid SF Symbol name such as globe or terminal.fill + +# Colors must be hex colours such as #000 #ff00ff where the color is used for later content and 'foreground' switches back to default colour + +# Target is used to send different content to different complications after configuring the complications with different target identifiers which requires the pro unlock. The target parameter is never assumed unless --target is used and is effective until next --target parameter allowing updates of several complications with a single command + +# You can configure complications to only show content for a given target. + +# String is the fallback type if nothing else matches, but content type can be forced for next parameter with --progress, --icon, --color, --text or --target with +# something like: + +widget --text "50/100" + +# You can update several complications at once by using --target to send all parameters until the next --target to a particular complication. Updating several complications at once allows more total updates per day. + +# EOF +# return 0 +# fi + +# local key=d7e810e7601cd296a05776c169b4fe97a6a5ee1fd46abe38de54f415732b3f4b +# local user=WuqPwm1VpGijF4U5AnIKzqNMVWGioANTRjJoonPm +# local iv=ab5bbeb426015da7eedcee8bee3dffb7 + +# local plain=$( +# echo Secure ShellFish Widget 2.0 +# for var in "$@" +# do +# echo -ne "$var" | base64 +# done) +# local base64=$(echo "$plain" | openssl enc -aes-256-cbc -base64 -K $key -iv $iv) +# curl -sS -X POST -H "Content-Type: text/plain" --data "$base64" "https://secureshellfish.app/push/?user=$user" diff --git a/sijapi/helpers/obsidian/month_o_banners.sh b/sijapi/helpers/obsidian/month_o_banners.sh new file mode 100755 index 0000000..d9e94f1 --- /dev/null +++ b/sijapi/helpers/obsidian/month_o_banners.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Iterate from 18 to 30 +for i in $(seq -w 01 31); do + # Construct the date string + DATE="2024-07-${i}" + + # Print the date being processed (optional) + echo "Processing date: $DATE" + + # Run the curl command + curl -X POST -H "Content-Type: application/json" -d '{"mood": "joyful"}' "http://localhost:4444/note/banner?dt=$DATE" + + # Wait for the curl command to finish before starting the next iteration + wait +done + diff --git a/sijapi/helpers/scrapers/Readability.js b/sijapi/helpers/scrapers/Readability.js new file mode 100644 index 0000000..89fdbd0 --- /dev/null +++ b/sijapi/helpers/scrapers/Readability.js @@ -0,0 +1,2373 @@ +/* + * Copyright (c) 2010 Arc90 Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This code is heavily based on Arc90's readability.js (1.7.1) script + * available at: http://code.google.com/p/arc90labs-readability + */ + +/** + * Public constructor. + * @param {HTMLDocument} doc The document to parse. + * @param {Object} options The options object. + */ +function Readability(doc, options) { + // In some older versions, people passed a URI as the first argument. Cope: + if (options && options.documentElement) { + doc = options; + options = arguments[2]; + } else if (!doc || !doc.documentElement) { + throw new Error("First argument to Readability constructor should be a document object."); + } + options = options || {}; + + this._doc = doc; + this._docJSDOMParser = this._doc.firstChild.__JSDOMParser__; + this._articleTitle = null; + this._articleByline = null; + this._articleDir = null; + this._articleSiteName = null; + this._attempts = []; + + // Configurable options + this._debug = !!options.debug; + this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE; + this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES; + this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD; + this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []); + this._keepClasses = !!options.keepClasses; + this._serializer = options.serializer || function(el) { + return el.innerHTML; + }; + this._disableJSONLD = !!options.disableJSONLD; + this._allowedVideoRegex = options.allowedVideoRegex || this.REGEXPS.videos; + + // Start with all flags set + this._flags = this.FLAG_STRIP_UNLIKELYS | + this.FLAG_WEIGHT_CLASSES | + this.FLAG_CLEAN_CONDITIONALLY; + + + // Control whether log messages are sent to the console + if (this._debug) { + let logNode = function(node) { + if (node.nodeType == node.TEXT_NODE) { + return `${node.nodeName} ("${node.textContent}")`; + } + let attrPairs = Array.from(node.attributes || [], function(attr) { + return `${attr.name}="${attr.value}"`; + }).join(" "); + return `<${node.localName} ${attrPairs}>`; + }; + this.log = function () { + if (typeof console !== "undefined") { + let args = Array.from(arguments, arg => { + if (arg && arg.nodeType == this.ELEMENT_NODE) { + return logNode(arg); + } + return arg; + }); + args.unshift("Reader: (Readability)"); + console.log.apply(console, args); + } else if (typeof dump !== "undefined") { + /* global dump */ + var msg = Array.prototype.map.call(arguments, function(x) { + return (x && x.nodeName) ? logNode(x) : x; + }).join(" "); + dump("Reader: (Readability) " + msg + "\n"); + } + }; + } else { + this.log = function () {}; + } +} + +Readability.prototype = { + FLAG_STRIP_UNLIKELYS: 0x1, + FLAG_WEIGHT_CLASSES: 0x2, + FLAG_CLEAN_CONDITIONALLY: 0x4, + + // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + ELEMENT_NODE: 1, + TEXT_NODE: 3, + + // Max number of nodes supported by this parser. Default: 0 (no limit) + DEFAULT_MAX_ELEMS_TO_PARSE: 0, + + // The number of top candidates to consider when analysing how + // tight the competition is among candidates. + DEFAULT_N_TOP_CANDIDATES: 5, + + // Element tags to score by default. + DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","), + + // The default number of chars an article must have in order to return a result + DEFAULT_CHAR_THRESHOLD: 500, + + // All of the regular expressions in use within readability. + // Defined up here so we don't instantiate them repeatedly in loops. + REGEXPS: { + // NOTE: These two regular expressions are duplicated in + // Readability-readerable.js. Please keep both copies in sync. + unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, + okMaybeItsACandidate: /and|article|body|column|content|main|shadow/i, + + positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i, + negative: /-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i, + extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i, + byline: /byline|author|dateline|writtenby|p-author/i, + replaceFonts: /<(\/?)font[^>]*>/gi, + normalize: /\s{2,}/g, + videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i, + shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i, + nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, + prevLink: /(prev|earl|old|new|<|«)/i, + tokenize: /\W+/g, + whitespace: /^\s*$/, + hasContent: /\S$/, + hashUrl: /^#.+/, + srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g, + b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i, + // Commas as used in Latin, Sindhi, Chinese and various other scripts. + // see: https://en.wikipedia.org/wiki/Comma#Comma_variants + commas: /\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g, + // See: https://schema.org/Article + jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/, + // used to see if a node's content matches words commonly used for ad blocks or loading indicators + adWords: /^(ad(vertising|vertisement)?|pub(licité)?|werb(ung)?|广告|Реклама|Anuncio)$/iu, + loadingWords: /^((loading|正在加载|Загрузка|chargement|cargando)(…|\.\.\.)?)$/iu, + }, + + UNLIKELY_ROLES: [ "menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog" ], + + DIV_TO_P_ELEMS: new Set([ "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL" ]), + + ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"], + + PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ], + + DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ], + + // The commented out elements qualify as phrasing content but tend to be + // removed by readability when put into paragraphs, so we ignore them here. + PHRASING_ELEMS: [ + // "CANVAS", "IFRAME", "SVG", "VIDEO", + "ABBR", "AUDIO", "B", "BDO", "BR", "BUTTON", "CITE", "CODE", "DATA", + "DATALIST", "DFN", "EM", "EMBED", "I", "IMG", "INPUT", "KBD", "LABEL", + "MARK", "MATH", "METER", "NOSCRIPT", "OBJECT", "OUTPUT", "PROGRESS", "Q", + "RUBY", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "SUB", + "SUP", "TEXTAREA", "TIME", "VAR", "WBR", + ], + + // These are the classes that readability sets itself. + CLASSES_TO_PRESERVE: [ "page" ], + + // These are the list of HTML entities that need to be escaped. + HTML_ESCAPE_MAP: { + "lt": "<", + "gt": ">", + "amp": "&", + "quot": '"', + "apos": "'", + }, + + /** + * Run any post-process modifications to article content as necessary. + * + * @param Element + * @return void + **/ + _postProcessContent: function(articleContent) { + // Readability cannot open relative uris so we convert them to absolute uris. + this._fixRelativeUris(articleContent); + + this._simplifyNestedElements(articleContent); + + if (!this._keepClasses) { + // Remove classes. + this._cleanClasses(articleContent); + } + }, + + /** + * Iterates over a NodeList, calls `filterFn` for each node and removes node + * if function returned `true`. + * + * If function is not passed, removes all the nodes in node list. + * + * @param NodeList nodeList The nodes to operate on + * @param Function filterFn the function to use as a filter + * @return void + */ + _removeNodes: function(nodeList, filterFn) { + // Avoid ever operating on live node lists. + if (this._docJSDOMParser && nodeList._isLiveNodeList) { + throw new Error("Do not pass live node lists to _removeNodes"); + } + for (var i = nodeList.length - 1; i >= 0; i--) { + var node = nodeList[i]; + var parentNode = node.parentNode; + if (parentNode) { + if (!filterFn || filterFn.call(this, node, i, nodeList)) { + parentNode.removeChild(node); + } + } + } + }, + + /** + * Iterates over a NodeList, and calls _setNodeTag for each node. + * + * @param NodeList nodeList The nodes to operate on + * @param String newTagName the new tag name to use + * @return void + */ + _replaceNodeTags: function(nodeList, newTagName) { + // Avoid ever operating on live node lists. + if (this._docJSDOMParser && nodeList._isLiveNodeList) { + throw new Error("Do not pass live node lists to _replaceNodeTags"); + } + for (const node of nodeList) { + this._setNodeTag(node, newTagName); + } + }, + + /** + * Iterate over a NodeList, which doesn't natively fully implement the Array + * interface. + * + * For convenience, the current object context is applied to the provided + * iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return void + */ + _forEachNode: function(nodeList, fn) { + Array.prototype.forEach.call(nodeList, fn, this); + }, + + /** + * Iterate over a NodeList, and return the first node that passes + * the supplied test function + * + * For convenience, the current object context is applied to the provided + * test function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The test function. + * @return void + */ + _findNode: function(nodeList, fn) { + return Array.prototype.find.call(nodeList, fn, this); + }, + + /** + * Iterate over a NodeList, return true if any of the provided iterate + * function calls returns true, false otherwise. + * + * For convenience, the current object context is applied to the + * provided iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return Boolean + */ + _someNode: function(nodeList, fn) { + return Array.prototype.some.call(nodeList, fn, this); + }, + + /** + * Iterate over a NodeList, return true if all of the provided iterate + * function calls return true, false otherwise. + * + * For convenience, the current object context is applied to the + * provided iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return Boolean + */ + _everyNode: function(nodeList, fn) { + return Array.prototype.every.call(nodeList, fn, this); + }, + + /** + * Concat all nodelists passed as arguments. + * + * @return ...NodeList + * @return Array + */ + _concatNodeLists: function() { + var slice = Array.prototype.slice; + var args = slice.call(arguments); + var nodeLists = args.map(function(list) { + return slice.call(list); + }); + return Array.prototype.concat.apply([], nodeLists); + }, + + _getAllNodesWithTag: function(node, tagNames) { + if (node.querySelectorAll) { + return node.querySelectorAll(tagNames.join(",")); + } + return [].concat.apply([], tagNames.map(function(tag) { + var collection = node.getElementsByTagName(tag); + return Array.isArray(collection) ? collection : Array.from(collection); + })); + }, + + /** + * Removes the class="" attribute from every element in the given + * subtree, except those that match CLASSES_TO_PRESERVE and + * the classesToPreserve array from the options object. + * + * @param Element + * @return void + */ + _cleanClasses: function(node) { + var classesToPreserve = this._classesToPreserve; + var className = (node.getAttribute("class") || "") + .split(/\s+/) + .filter(function(cls) { + return classesToPreserve.indexOf(cls) != -1; + }) + .join(" "); + + if (className) { + node.setAttribute("class", className); + } else { + node.removeAttribute("class"); + } + + for (node = node.firstElementChild; node; node = node.nextElementSibling) { + this._cleanClasses(node); + } + }, + + /** + * Converts each and uri in the given element to an absolute URI, + * ignoring #ref URIs. + * + * @param Element + * @return void + */ + _fixRelativeUris: function(articleContent) { + var baseURI = this._doc.baseURI; + var documentURI = this._doc.documentURI; + function toAbsoluteURI(uri) { + // Leave hash links alone if the base URI matches the document URI: + if (baseURI == documentURI && uri.charAt(0) == "#") { + return uri; + } + + // Otherwise, resolve against base URI: + try { + return new URL(uri, baseURI).href; + } catch (ex) { + // Something went wrong, just return the original: + } + return uri; + } + + var links = this._getAllNodesWithTag(articleContent, ["a"]); + this._forEachNode(links, function(link) { + var href = link.getAttribute("href"); + if (href) { + // Remove links with javascript: URIs, since + // they won't work after scripts have been removed from the page. + if (href.indexOf("javascript:") === 0) { + // if the link only contains simple text content, it can be converted to a text node + if (link.childNodes.length === 1 && link.childNodes[0].nodeType === this.TEXT_NODE) { + var text = this._doc.createTextNode(link.textContent); + link.parentNode.replaceChild(text, link); + } else { + // if the link has multiple children, they should all be preserved + var container = this._doc.createElement("span"); + while (link.firstChild) { + container.appendChild(link.firstChild); + } + link.parentNode.replaceChild(container, link); + } + } else { + link.setAttribute("href", toAbsoluteURI(href)); + } + } + }); + + var medias = this._getAllNodesWithTag(articleContent, [ + "img", "picture", "figure", "video", "audio", "source", + ]); + + this._forEachNode(medias, function(media) { + var src = media.getAttribute("src"); + var poster = media.getAttribute("poster"); + var srcset = media.getAttribute("srcset"); + + if (src) { + media.setAttribute("src", toAbsoluteURI(src)); + } + + if (poster) { + media.setAttribute("poster", toAbsoluteURI(poster)); + } + + if (srcset) { + var newSrcset = srcset.replace(this.REGEXPS.srcsetUrl, function(_, p1, p2, p3) { + return toAbsoluteURI(p1) + (p2 || "") + p3; + }); + + media.setAttribute("srcset", newSrcset); + } + }); + }, + + _simplifyNestedElements: function(articleContent) { + var node = articleContent; + + while (node) { + if (node.parentNode && ["DIV", "SECTION"].includes(node.tagName) && !(node.id && node.id.startsWith("readability"))) { + if (this._isElementWithoutContent(node)) { + node = this._removeAndGetNext(node); + continue; + } else if (this._hasSingleTagInsideElement(node, "DIV") || this._hasSingleTagInsideElement(node, "SECTION")) { + var child = node.children[0]; + for (var i = 0; i < node.attributes.length; i++) { + child.setAttribute(node.attributes[i].name, node.attributes[i].value); + } + node.parentNode.replaceChild(child, node); + node = child; + continue; + } + } + + node = this._getNextNode(node); + } + }, + + /** + * Get the article title as an H1. + * + * @return string + **/ + _getArticleTitle: function() { + var doc = this._doc; + var curTitle = ""; + var origTitle = ""; + + try { + curTitle = origTitle = doc.title.trim(); + + // If they had an element with id "title" in their HTML + if (typeof curTitle !== "string") + curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]); + } catch (e) {/* ignore exceptions setting the title. */} + + var titleHadHierarchicalSeparators = false; + function wordCount(str) { + return str.split(/\s+/).length; + } + + // If there's a separator in the title, first remove the final part + if ((/ [\|\-\\\/>»] /).test(curTitle)) { + titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle); + curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1"); + + // If the resulting title is too short (3 words or fewer), remove + // the first part instead: + if (wordCount(curTitle) < 3) + curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1"); + } else if (curTitle.indexOf(": ") !== -1) { + // Check if we have an heading containing this exact string, so we + // could assume it's the full title. + var headings = this._concatNodeLists( + doc.getElementsByTagName("h1"), + doc.getElementsByTagName("h2") + ); + var trimmedTitle = curTitle.trim(); + var match = this._someNode(headings, function(heading) { + return heading.textContent.trim() === trimmedTitle; + }); + + // If we don't, let's extract the title out of the original title string. + if (!match) { + curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1); + + // If the title is now too short, try the first colon instead: + if (wordCount(curTitle) < 3) { + curTitle = origTitle.substring(origTitle.indexOf(":") + 1); + // But if we have too many words before the colon there's something weird + // with the titles and the H tags so let's just use the original title instead + } else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) { + curTitle = origTitle; + } + } + } else if (curTitle.length > 150 || curTitle.length < 15) { + var hOnes = doc.getElementsByTagName("h1"); + + if (hOnes.length === 1) + curTitle = this._getInnerText(hOnes[0]); + } + + curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " "); + // If we now have 4 words or fewer as our title, and either no + // 'hierarchical' separators (\, /, > or ») were found in the original + // title or we decreased the number of words by more than 1 word, use + // the original title. + var curTitleWordCount = wordCount(curTitle); + if (curTitleWordCount <= 4 && + (!titleHadHierarchicalSeparators || + curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) { + curTitle = origTitle; + } + + return curTitle; + }, + + /** + * Prepare the HTML document for readability to scrape it. + * This includes things like stripping javascript, CSS, and handling terrible markup. + * + * @return void + **/ + _prepDocument: function() { + var doc = this._doc; + + // Remove all style tags in head + this._removeNodes(this._getAllNodesWithTag(doc, ["style"])); + + if (doc.body) { + this._replaceBrs(doc.body); + } + + this._replaceNodeTags(this._getAllNodesWithTag(doc, ["font"]), "SPAN"); + }, + + /** + * Finds the next node, starting from the given node, and ignoring + * whitespace in between. If the given node is an element, the same node is + * returned. + */ + _nextNode: function (node) { + var next = node; + while (next + && (next.nodeType != this.ELEMENT_NODE) + && this.REGEXPS.whitespace.test(next.textContent)) { + next = next.nextSibling; + } + return next; + }, + + /** + * Replaces 2 or more successive
    elements with a single

    . + * Whitespace between
    elements are ignored. For example: + *

    foo
    bar


    abc
    + * will become: + *
    foo
    bar

    abc

    + */ + _replaceBrs: function (elem) { + this._forEachNode(this._getAllNodesWithTag(elem, ["br"]), function(br) { + var next = br.nextSibling; + + // Whether 2 or more
    elements have been found and replaced with a + //

    block. + var replaced = false; + + // If we find a
    chain, remove the
    s until we hit another node + // or non-whitespace. This leaves behind the first
    in the chain + // (which will be replaced with a

    later). + while ((next = this._nextNode(next)) && (next.tagName == "BR")) { + replaced = true; + var brSibling = next.nextSibling; + next.parentNode.removeChild(next); + next = brSibling; + } + + // If we removed a
    chain, replace the remaining
    with a

    . Add + // all sibling nodes as children of the

    until we hit another
    + // chain. + if (replaced) { + var p = this._doc.createElement("p"); + br.parentNode.replaceChild(p, br); + + next = p.nextSibling; + while (next) { + // If we've hit another

    , we're done adding children to this

    . + if (next.tagName == "BR") { + var nextElem = this._nextNode(next.nextSibling); + if (nextElem && nextElem.tagName == "BR") + break; + } + + if (!this._isPhrasingContent(next)) + break; + + // Otherwise, make this node a child of the new

    . + var sibling = next.nextSibling; + p.appendChild(next); + next = sibling; + } + + while (p.lastChild && this._isWhitespace(p.lastChild)) { + p.removeChild(p.lastChild); + } + + if (p.parentNode.tagName === "P") + this._setNodeTag(p.parentNode, "DIV"); + } + }); + }, + + _setNodeTag: function (node, tag) { + this.log("_setNodeTag", node, tag); + if (this._docJSDOMParser) { + node.localName = tag.toLowerCase(); + node.tagName = tag.toUpperCase(); + return node; + } + + var replacement = node.ownerDocument.createElement(tag); + while (node.firstChild) { + replacement.appendChild(node.firstChild); + } + node.parentNode.replaceChild(replacement, node); + if (node.readability) + replacement.readability = node.readability; + + for (var i = 0; i < node.attributes.length; i++) { + try { + replacement.setAttribute(node.attributes[i].name, node.attributes[i].value); + } catch (ex) { + /* it's possible for setAttribute() to throw if the attribute name + * isn't a valid XML Name. Such attributes can however be parsed from + * source in HTML docs, see https://github.com/whatwg/html/issues/4275, + * so we can hit them here and then throw. We don't care about such + * attributes so we ignore them. + */ + } + } + return replacement; + }, + + /** + * Prepare the article node for display. Clean out any inline styles, + * iframes, forms, strip extraneous

    tags, etc. + * + * @param Element + * @return void + **/ + _prepArticle: function(articleContent) { + this._cleanStyles(articleContent); + + // Check for data tables before we continue, to avoid removing items in + // those tables, which will often be isolated even though they're + // visually linked to other content-ful elements (text, images, etc.). + this._markDataTables(articleContent); + + this._fixLazyImages(articleContent); + + // Clean out junk from the article content + this._cleanConditionally(articleContent, "form"); + this._cleanConditionally(articleContent, "fieldset"); + this._clean(articleContent, "object"); + this._clean(articleContent, "embed"); + this._clean(articleContent, "footer"); + this._clean(articleContent, "link"); + this._clean(articleContent, "aside"); + + // Clean out elements with little content that have "share" in their id/class combinations from final top candidates, + // which means we don't remove the top candidates even they have "share". + + var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD; + + this._forEachNode(articleContent.children, function (topCandidate) { + this._cleanMatchedNodes(topCandidate, function (node, matchString) { + return this.REGEXPS.shareElements.test(matchString) && node.textContent.length < shareElementThreshold; + }); + }); + + this._clean(articleContent, "iframe"); + this._clean(articleContent, "input"); + this._clean(articleContent, "textarea"); + this._clean(articleContent, "select"); + this._clean(articleContent, "button"); + this._cleanHeaders(articleContent); + + // Do these last as the previous stuff may have removed junk + // that will affect these + this._cleanConditionally(articleContent, "table"); + this._cleanConditionally(articleContent, "ul"); + this._cleanConditionally(articleContent, "div"); + + // replace H1 with H2 as H1 should be only title that is displayed separately + this._replaceNodeTags(this._getAllNodesWithTag(articleContent, ["h1"]), "h2"); + + // Remove extra paragraphs + this._removeNodes(this._getAllNodesWithTag(articleContent, ["p"]), function (paragraph) { + var imgCount = paragraph.getElementsByTagName("img").length; + var embedCount = paragraph.getElementsByTagName("embed").length; + var objectCount = paragraph.getElementsByTagName("object").length; + // At this point, nasty iframes have been removed, only remain embedded video ones. + var iframeCount = paragraph.getElementsByTagName("iframe").length; + var totalCount = imgCount + embedCount + objectCount + iframeCount; + + return totalCount === 0 && !this._getInnerText(paragraph, false); + }); + + this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) { + var next = this._nextNode(br.nextSibling); + if (next && next.tagName == "P") + br.parentNode.removeChild(br); + }); + + // Remove single-cell tables + this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) { + var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table; + if (this._hasSingleTagInsideElement(tbody, "TR")) { + var row = tbody.firstElementChild; + if (this._hasSingleTagInsideElement(row, "TD")) { + var cell = row.firstElementChild; + cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV"); + table.parentNode.replaceChild(cell, table); + } + } + }); + }, + + /** + * Initialize a node with the readability object. Also checks the + * className/id for special names to add to its score. + * + * @param Element + * @return void + **/ + _initializeNode: function(node) { + node.readability = {"contentScore": 0}; + + switch (node.tagName) { + case "DIV": + node.readability.contentScore += 5; + break; + + case "PRE": + case "TD": + case "BLOCKQUOTE": + node.readability.contentScore += 3; + break; + + case "ADDRESS": + case "OL": + case "UL": + case "DL": + case "DD": + case "DT": + case "LI": + case "FORM": + node.readability.contentScore -= 3; + break; + + case "H1": + case "H2": + case "H3": + case "H4": + case "H5": + case "H6": + case "TH": + node.readability.contentScore -= 5; + break; + } + + node.readability.contentScore += this._getClassWeight(node); + }, + + _removeAndGetNext: function(node) { + var nextNode = this._getNextNode(node, true); + node.parentNode.removeChild(node); + return nextNode; + }, + + /** + * Traverse the DOM from node to node, starting at the node passed in. + * Pass true for the second parameter to indicate this node itself + * (and its kids) are going away, and we want the next node over. + * + * Calling this in a loop will traverse the DOM depth-first. + */ + _getNextNode: function(node, ignoreSelfAndKids) { + // First check for kids if those aren't being ignored + if (!ignoreSelfAndKids && node.firstElementChild) { + return node.firstElementChild; + } + // Then for siblings... + if (node.nextElementSibling) { + return node.nextElementSibling; + } + // And finally, move up the parent chain *and* find a sibling + // (because this is depth-first traversal, we will have already + // seen the parent nodes themselves). + do { + node = node.parentNode; + } while (node && !node.nextElementSibling); + return node && node.nextElementSibling; + }, + + // compares second text to first one + // 1 = same text, 0 = completely different text + // works the way that it splits both texts into words and then finds words that are unique in second text + // the result is given by the lower length of unique parts + _textSimilarity: function(textA, textB) { + var tokensA = textA.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean); + var tokensB = textB.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean); + if (!tokensA.length || !tokensB.length) { + return 0; + } + var uniqTokensB = tokensB.filter(token => !tokensA.includes(token)); + var distanceB = uniqTokensB.join(" ").length / tokensB.join(" ").length; + return 1 - distanceB; + }, + + _checkByline: function(node, matchString) { + if (this._articleByline) { + return false; + } + + if (node.getAttribute !== undefined) { + var rel = node.getAttribute("rel"); + var itemprop = node.getAttribute("itemprop"); + } + + if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) { + this._articleByline = node.textContent.trim(); + return true; + } + + return false; + }, + + _getNodeAncestors: function(node, maxDepth) { + maxDepth = maxDepth || 0; + var i = 0, ancestors = []; + while (node.parentNode) { + ancestors.push(node.parentNode); + if (maxDepth && ++i === maxDepth) + break; + node = node.parentNode; + } + return ancestors; + }, + + /*** + * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is + * most likely to be the stuff a user wants to read. Then return it wrapped up in a div. + * + * @param page a document to run upon. Needs to be a full document, complete with body. + * @return Element + **/ + _grabArticle: function (page) { + this.log("**** grabArticle ****"); + var doc = this._doc; + var isPaging = page !== null; + page = page ? page : this._doc.body; + + // We can't grab an article if we don't have a page! + if (!page) { + this.log("No body found in document. Abort."); + return null; + } + + var pageCacheHtml = page.innerHTML; + + while (true) { + this.log("Starting grabArticle loop"); + var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS); + + // First, node prepping. Trash nodes that look cruddy (like ones with the + // class name "comment", etc), and turn divs into P tags where they have been + // used inappropriately (as in, where they contain no other block level elements.) + var elementsToScore = []; + var node = this._doc.documentElement; + + let shouldRemoveTitleHeader = true; + + while (node) { + + if (node.tagName === "HTML") { + this._articleLang = node.getAttribute("lang"); + } + + var matchString = node.className + " " + node.id; + + if (!this._isProbablyVisible(node)) { + this.log("Removing hidden node - " + matchString); + node = this._removeAndGetNext(node); + continue; + } + + // User is not able to see elements applied with both "aria-modal = true" and "role = dialog" + if (node.getAttribute("aria-modal") == "true" && node.getAttribute("role") == "dialog") { + node = this._removeAndGetNext(node); + continue; + } + + // Check to see if this node is a byline, and remove it if it is. + if (this._checkByline(node, matchString)) { + node = this._removeAndGetNext(node); + continue; + } + + if (shouldRemoveTitleHeader && this._headerDuplicatesTitle(node)) { + this.log("Removing header: ", node.textContent.trim(), this._articleTitle.trim()); + shouldRemoveTitleHeader = false; + node = this._removeAndGetNext(node); + continue; + } + + // Remove unlikely candidates + if (stripUnlikelyCandidates) { + if (this.REGEXPS.unlikelyCandidates.test(matchString) && + !this.REGEXPS.okMaybeItsACandidate.test(matchString) && + !this._hasAncestorTag(node, "table") && + !this._hasAncestorTag(node, "code") && + node.tagName !== "BODY" && + node.tagName !== "A") { + this.log("Removing unlikely candidate - " + matchString); + node = this._removeAndGetNext(node); + continue; + } + + if (this.UNLIKELY_ROLES.includes(node.getAttribute("role"))) { + this.log("Removing content with role " + node.getAttribute("role") + " - " + matchString); + node = this._removeAndGetNext(node); + continue; + } + } + + // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe). + if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" || + node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" || + node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") && + this._isElementWithoutContent(node)) { + node = this._removeAndGetNext(node); + continue; + } + + if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) { + elementsToScore.push(node); + } + + // Turn all divs that don't have children block level elements into p's + if (node.tagName === "DIV") { + // Put phrasing content into paragraphs. + var p = null; + var childNode = node.firstChild; + while (childNode) { + var nextSibling = childNode.nextSibling; + if (this._isPhrasingContent(childNode)) { + if (p !== null) { + p.appendChild(childNode); + } else if (!this._isWhitespace(childNode)) { + p = doc.createElement("p"); + node.replaceChild(p, childNode); + p.appendChild(childNode); + } + } else if (p !== null) { + while (p.lastChild && this._isWhitespace(p.lastChild)) { + p.removeChild(p.lastChild); + } + p = null; + } + childNode = nextSibling; + } + + // Sites like http://mobile.slate.com encloses each paragraph with a DIV + // element. DIVs with only a P element inside and no text content can be + // safely converted into plain P elements to avoid confusing the scoring + // algorithm with DIVs with are, in practice, paragraphs. + if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) { + var newNode = node.children[0]; + node.parentNode.replaceChild(newNode, node); + node = newNode; + elementsToScore.push(node); + } else if (!this._hasChildBlockElement(node)) { + node = this._setNodeTag(node, "P"); + elementsToScore.push(node); + } + } + node = this._getNextNode(node); + } + + /** + * Loop through all paragraphs, and assign a score to them based on how content-y they look. + * Then add their score to their parent node. + * + * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. + **/ + var candidates = []; + this._forEachNode(elementsToScore, function(elementToScore) { + if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined") + return; + + // If this paragraph is less than 25 characters, don't even count it. + var innerText = this._getInnerText(elementToScore); + if (innerText.length < 25) + return; + + // Exclude nodes with no ancestor. + var ancestors = this._getNodeAncestors(elementToScore, 5); + if (ancestors.length === 0) + return; + + var contentScore = 0; + + // Add a point for the paragraph itself as a base. + contentScore += 1; + + // Add points for any commas within this paragraph. + contentScore += innerText.split(this.REGEXPS.commas).length; + + // For every 100 characters in this paragraph, add another point. Up to 3 points. + contentScore += Math.min(Math.floor(innerText.length / 100), 3); + + // Initialize and score ancestors. + this._forEachNode(ancestors, function(ancestor, level) { + if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined") + return; + + if (typeof(ancestor.readability) === "undefined") { + this._initializeNode(ancestor); + candidates.push(ancestor); + } + + // Node score divider: + // - parent: 1 (no division) + // - grandparent: 2 + // - great grandparent+: ancestor level * 3 + if (level === 0) + var scoreDivider = 1; + else if (level === 1) + scoreDivider = 2; + else + scoreDivider = level * 3; + ancestor.readability.contentScore += contentScore / scoreDivider; + }); + }); + + // After we've calculated scores, loop through all of the possible + // candidate nodes we found and find the one with the highest score. + var topCandidates = []; + for (var c = 0, cl = candidates.length; c < cl; c += 1) { + var candidate = candidates[c]; + + // Scale the final candidates score based on link density. Good content + // should have a relatively small link density (5% or less) and be mostly + // unaffected by this operation. + var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate)); + candidate.readability.contentScore = candidateScore; + + this.log("Candidate:", candidate, "with score " + candidateScore); + + for (var t = 0; t < this._nbTopCandidates; t++) { + var aTopCandidate = topCandidates[t]; + + if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) { + topCandidates.splice(t, 0, candidate); + if (topCandidates.length > this._nbTopCandidates) + topCandidates.pop(); + break; + } + } + } + + var topCandidate = topCandidates[0] || null; + var neededToCreateTopCandidate = false; + var parentOfTopCandidate; + + // If we still have no top candidate, just use the body as a last resort. + // We also have to copy the body node so it is something we can modify. + if (topCandidate === null || topCandidate.tagName === "BODY") { + // Move all of the page's children into topCandidate + topCandidate = doc.createElement("DIV"); + neededToCreateTopCandidate = true; + // Move everything (not just elements, also text nodes etc.) into the container + // so we even include text directly in the body: + while (page.firstChild) { + this.log("Moving child out:", page.firstChild); + topCandidate.appendChild(page.firstChild); + } + + page.appendChild(topCandidate); + + this._initializeNode(topCandidate); + } else if (topCandidate) { + // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array + // and whose scores are quite closed with current `topCandidate` node. + var alternativeCandidateAncestors = []; + for (var i = 1; i < topCandidates.length; i++) { + if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) { + alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i])); + } + } + var MINIMUM_TOPCANDIDATES = 3; + if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) { + parentOfTopCandidate = topCandidate.parentNode; + while (parentOfTopCandidate.tagName !== "BODY") { + var listsContainingThisAncestor = 0; + for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) { + listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate)); + } + if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) { + topCandidate = parentOfTopCandidate; + break; + } + parentOfTopCandidate = parentOfTopCandidate.parentNode; + } + } + if (!topCandidate.readability) { + this._initializeNode(topCandidate); + } + + // Because of our bonus system, parents of candidates might have scores + // themselves. They get half of the node. There won't be nodes with higher + // scores than our topCandidate, but if we see the score going *up* in the first + // few steps up the tree, that's a decent sign that there might be more content + // lurking in other places that we want to unify in. The sibling stuff + // below does some of that - but only if we've looked high enough up the DOM + // tree. + parentOfTopCandidate = topCandidate.parentNode; + var lastScore = topCandidate.readability.contentScore; + // The scores shouldn't get too low. + var scoreThreshold = lastScore / 3; + while (parentOfTopCandidate.tagName !== "BODY") { + if (!parentOfTopCandidate.readability) { + parentOfTopCandidate = parentOfTopCandidate.parentNode; + continue; + } + var parentScore = parentOfTopCandidate.readability.contentScore; + if (parentScore < scoreThreshold) + break; + if (parentScore > lastScore) { + // Alright! We found a better parent to use. + topCandidate = parentOfTopCandidate; + break; + } + lastScore = parentOfTopCandidate.readability.contentScore; + parentOfTopCandidate = parentOfTopCandidate.parentNode; + } + + // If the top candidate is the only child, use parent instead. This will help sibling + // joining logic when adjacent content is actually located in parent's sibling node. + parentOfTopCandidate = topCandidate.parentNode; + while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) { + topCandidate = parentOfTopCandidate; + parentOfTopCandidate = topCandidate.parentNode; + } + if (!topCandidate.readability) { + this._initializeNode(topCandidate); + } + } + + // Now that we have the top candidate, look through its siblings for content + // that might also be related. Things like preambles, content split by ads + // that we removed, etc. + var articleContent = doc.createElement("DIV"); + if (isPaging) + articleContent.id = "readability-content"; + + var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2); + // Keep potential top candidate's parent node to try to get text direction of it later. + parentOfTopCandidate = topCandidate.parentNode; + var siblings = parentOfTopCandidate.children; + + for (var s = 0, sl = siblings.length; s < sl; s++) { + var sibling = siblings[s]; + var append = false; + + this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : ""); + this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown"); + + if (sibling === topCandidate) { + append = true; + } else { + var contentBonus = 0; + + // Give a bonus if sibling nodes and top candidates have the example same classname + if (sibling.className === topCandidate.className && topCandidate.className !== "") + contentBonus += topCandidate.readability.contentScore * 0.2; + + if (sibling.readability && + ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) { + append = true; + } else if (sibling.nodeName === "P") { + var linkDensity = this._getLinkDensity(sibling); + var nodeContent = this._getInnerText(sibling); + var nodeLength = nodeContent.length; + + if (nodeLength > 80 && linkDensity < 0.25) { + append = true; + } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && + nodeContent.search(/\.( |$)/) !== -1) { + append = true; + } + } + } + + if (append) { + this.log("Appending node:", sibling); + + if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) { + // We have a node that isn't a common block level element, like a form or td tag. + // Turn it into a div so it doesn't get filtered out later by accident. + this.log("Altering sibling:", sibling, "to div."); + + sibling = this._setNodeTag(sibling, "DIV"); + } + + articleContent.appendChild(sibling); + // Fetch children again to make it compatible + // with DOM parsers without live collection support. + siblings = parentOfTopCandidate.children; + // siblings is a reference to the children array, and + // sibling is removed from the array when we call appendChild(). + // As a result, we must revisit this index since the nodes + // have been shifted. + s -= 1; + sl -= 1; + } + } + + if (this._debug) + this.log("Article content pre-prep: " + articleContent.innerHTML); + // So we have all of the content that we need. Now we clean it up for presentation. + this._prepArticle(articleContent); + if (this._debug) + this.log("Article content post-prep: " + articleContent.innerHTML); + + if (neededToCreateTopCandidate) { + // We already created a fake div thing, and there wouldn't have been any siblings left + // for the previous loop, so there's no point trying to create a new div, and then + // move all the children over. Just assign IDs and class names here. No need to append + // because that already happened anyway. + topCandidate.id = "readability-page-1"; + topCandidate.className = "page"; + } else { + var div = doc.createElement("DIV"); + div.id = "readability-page-1"; + div.className = "page"; + while (articleContent.firstChild) { + div.appendChild(articleContent.firstChild); + } + articleContent.appendChild(div); + } + + if (this._debug) + this.log("Article content after paging: " + articleContent.innerHTML); + + var parseSuccessful = true; + + // Now that we've gone through the full algorithm, check to see if + // we got any meaningful content. If we didn't, we may need to re-run + // grabArticle with different flags set. This gives us a higher likelihood of + // finding the content, and the sieve approach gives us a higher likelihood of + // finding the -right- content. + var textLength = this._getInnerText(articleContent, true).length; + if (textLength < this._charThreshold) { + parseSuccessful = false; + page.innerHTML = pageCacheHtml; + + if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) { + this._removeFlag(this.FLAG_STRIP_UNLIKELYS); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) { + this._removeFlag(this.FLAG_WEIGHT_CLASSES); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) { + this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else { + this._attempts.push({articleContent: articleContent, textLength: textLength}); + // No luck after removing flags, just return the longest text we found during the different loops + this._attempts.sort(function (a, b) { + return b.textLength - a.textLength; + }); + + // But first check if we actually have something + if (!this._attempts[0].textLength) { + return null; + } + + articleContent = this._attempts[0].articleContent; + parseSuccessful = true; + } + } + + if (parseSuccessful) { + // Find out text direction from ancestors of final top candidate. + var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate)); + this._someNode(ancestors, function(ancestor) { + if (!ancestor.tagName) + return false; + var articleDir = ancestor.getAttribute("dir"); + if (articleDir) { + this._articleDir = articleDir; + return true; + } + return false; + }); + return articleContent; + } + } + }, + + /** + * Check whether the input string could be a byline. + * This verifies that the input is a string, and that the length + * is less than 100 chars. + * + * @param possibleByline {string} - a string to check whether its a byline. + * @return Boolean - whether the input string is a byline. + */ + _isValidByline: function(byline) { + if (typeof byline == "string" || byline instanceof String) { + byline = byline.trim(); + return (byline.length > 0) && (byline.length < 100); + } + return false; + }, + + /** + * Converts some of the common HTML entities in string to their corresponding characters. + * + * @param str {string} - a string to unescape. + * @return string without HTML entity. + */ + _unescapeHtmlEntities: function(str) { + if (!str) { + return str; + } + + var htmlEscapeMap = this.HTML_ESCAPE_MAP; + return str.replace(/&(quot|amp|apos|lt|gt);/g, function(_, tag) { + return htmlEscapeMap[tag]; + }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(_, hex, numStr) { + var num = parseInt(hex || numStr, hex ? 16 : 10); + return String.fromCharCode(num); + }); + }, + + /** + * Try to extract metadata from JSON-LD object. + * For now, only Schema.org objects of type Article or its subtypes are supported. + * @return Object with any metadata that could be extracted (possibly none) + */ + _getJSONLD: function (doc) { + var scripts = this._getAllNodesWithTag(doc, ["script"]); + + var metadata; + + this._forEachNode(scripts, function(jsonLdElement) { + if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") { + try { + // Strip CDATA markers if present + var content = jsonLdElement.textContent.replace(/^\s*\s*$/g, ""); + var parsed = JSON.parse(content); + if ( + !parsed["@context"] || + !parsed["@context"].match(/^https?\:\/\/schema\.org\/?$/) + ) { + return; + } + + if (!parsed["@type"] && Array.isArray(parsed["@graph"])) { + parsed = parsed["@graph"].find(function(it) { + return (it["@type"] || "").match( + this.REGEXPS.jsonLdArticleTypes + ); + }); + } + + if ( + !parsed || + !parsed["@type"] || + !parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes) + ) { + return; + } + + metadata = {}; + + if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) { + // we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz + // put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either + // "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default. + + var title = this._getArticleTitle(); + var nameMatches = this._textSimilarity(parsed.name, title) > 0.75; + var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75; + + if (headlineMatches && !nameMatches) { + metadata.title = parsed.headline; + } else { + metadata.title = parsed.name; + } + } else if (typeof parsed.name === "string") { + metadata.title = parsed.name.trim(); + } else if (typeof parsed.headline === "string") { + metadata.title = parsed.headline.trim(); + } + if (parsed.author) { + if (typeof parsed.author.name === "string") { + metadata.byline = parsed.author.name.trim(); + } else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") { + metadata.byline = parsed.author + .filter(function(author) { + return author && typeof author.name === "string"; + }) + .map(function(author) { + return author.name.trim(); + }) + .join(", "); + } + } + if (typeof parsed.description === "string") { + metadata.excerpt = parsed.description.trim(); + } + if ( + parsed.publisher && + typeof parsed.publisher.name === "string" + ) { + metadata.siteName = parsed.publisher.name.trim(); + } + if (typeof parsed.datePublished === "string") { + metadata.datePublished = parsed.datePublished.trim(); + } + return; + } catch (err) { + this.log(err.message); + } + } + }); + return metadata ? metadata : {}; + }, + + /** + * Attempts to get excerpt and byline metadata for the article. + * + * @param {Object} jsonld — object containing any metadata that + * could be extracted from JSON-LD object. + * + * @return Object with optional "excerpt" and "byline" properties + */ + _getArticleMetadata: function(jsonld) { + var metadata = {}; + var values = {}; + var metaElements = this._doc.getElementsByTagName("meta"); + + // property is a space-separated list of values + var propertyPattern = /\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi; + + // name is a single value + var namePattern = /^\s*(?:(dc|dcterm|og|twitter|parsely|weibo:(article|webpage))\s*[-\.:]\s*)?(author|creator|pub-date|description|title|site_name)\s*$/i; + + // Find description tags. + this._forEachNode(metaElements, function(element) { + var elementName = element.getAttribute("name"); + var elementProperty = element.getAttribute("property"); + var content = element.getAttribute("content"); + if (!content) { + return; + } + var matches = null; + var name = null; + + if (elementProperty) { + matches = elementProperty.match(propertyPattern); + if (matches) { + // Convert to lowercase, and remove any whitespace + // so we can match below. + name = matches[0].toLowerCase().replace(/\s/g, ""); + // multiple authors + values[name] = content.trim(); + } + } + if (!matches && elementName && namePattern.test(elementName)) { + name = elementName; + if (content) { + // Convert to lowercase, remove any whitespace, and convert dots + // to colons so we can match below. + name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":"); + values[name] = content.trim(); + } + } + }); + + // get title + metadata.title = jsonld.title || + values["dc:title"] || + values["dcterm:title"] || + values["og:title"] || + values["weibo:article:title"] || + values["weibo:webpage:title"] || + values["title"] || + values["twitter:title"] || + values["parsely-title"]; + + if (!metadata.title) { + metadata.title = this._getArticleTitle(); + } + + // get author + metadata.byline = jsonld.byline || + values["dc:creator"] || + values["dcterm:creator"] || + values["author"] || + values["parsely-author"]; + + // get description + metadata.excerpt = jsonld.excerpt || + values["dc:description"] || + values["dcterm:description"] || + values["og:description"] || + values["weibo:article:description"] || + values["weibo:webpage:description"] || + values["description"] || + values["twitter:description"]; + + // get site name + metadata.siteName = jsonld.siteName || + values["og:site_name"]; + + // get article published time + metadata.publishedTime = jsonld.datePublished || + values["article:published_time"] || + values["parsely-pub-date"] || + null; + + // in many sites the meta value is escaped with HTML entities, + // so here we need to unescape it + metadata.title = this._unescapeHtmlEntities(metadata.title); + metadata.byline = this._unescapeHtmlEntities(metadata.byline); + metadata.excerpt = this._unescapeHtmlEntities(metadata.excerpt); + metadata.siteName = this._unescapeHtmlEntities(metadata.siteName); + metadata.publishedTime = this._unescapeHtmlEntities(metadata.publishedTime); + + return metadata; + }, + + /** + * Check if node is image, or if node contains exactly only one image + * whether as a direct child or as its descendants. + * + * @param Element + **/ + _isSingleImage: function(node) { + if (node.tagName === "IMG") { + return true; + } + + if (node.children.length !== 1 || node.textContent.trim() !== "") { + return false; + } + + return this._isSingleImage(node.children[0]); + }, + + /** + * Find all