"""Application config.""" from __future__ import annotations import os from pathlib import Path from dotenv import load_dotenv ROOT_DIR = Path(__file__).resolve().parent.parent load_dotenv(ROOT_DIR / ".env") APP_DIR = Path(__file__).resolve().parent APP_TITLE = os.environ.get("APP_TITLE", "ZuZu Writer") HOST = os.environ.get("HOST", "0.0.0.0") PORT = int(os.environ.get("PORT", "7860") or "7860") SPACY_MODEL = os.environ.get("SPACY_MODEL", "en_core_web_sm") MAX_CHARS = max( 100_000, int(os.environ.get("MAX_CHARS", "1000000") or "1000000"), ) # Optional Supabase authentication and UI quotas. SUPABASE_URL = (os.environ.get("SUPABASE_URL") or "").rstrip("/") SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY") or "" SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY") or "" SUPABASE_JWT_SECRET = os.environ.get("SUPABASE_JWT_SECRET") or "" AUTH_ENABLED = bool(SUPABASE_URL and SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY) GUEST_DAILY_REWRITES = max( 1, min(int(os.environ.get("GUEST_DAILY_REWRITES", "1000") or "1000"), 1000) ) GUEST_MAX_WORDS = max( 20, min(int(os.environ.get("GUEST_MAX_WORDS", "100") or "100"), 5000) ) GUEST_DAILY_WORD_CAP = max( GUEST_MAX_WORDS, min( int( os.environ.get("GUEST_DAILY_WORD_CAP", str(GUEST_MAX_WORDS)) or str(GUEST_MAX_WORDS) ), 5000, ), ) SESSION_IDLE_MINUTES = int( os.environ.get("SESSION_IDLE_MINUTES", "30") or "30" ) # Self-hosted LanguageTool (ZuZu Grammar) # Example compose service: http://languagetool:8010 | local: http://127.0.0.1:8010 LANGUAGE_TOOL_URL = (os.environ.get("LANGUAGE_TOOL_URL") or "").rstrip("/") LANGUAGE_TOOL_LANGUAGE = os.environ.get("LANGUAGE_TOOL_LANGUAGE") or "en-US" LANGUAGE_TOOL_TIMEOUT = float(os.environ.get("LANGUAGE_TOOL_TIMEOUT", "90") or "90") LANGUAGE_TOOL_CHUNK_CHARS = max( 500, min(int(os.environ.get("LANGUAGE_TOOL_CHUNK_CHARS", "1800") or "1800"), 8000), ) # Standalone grammar endpoint cap. Rewrite grammar runs sentence-by-sentence. GRAMMAR_MAX_CHARS = max( 1000, min(int(os.environ.get("GRAMMAR_MAX_CHARS", "50000") or "50000"), MAX_CHARS), ) _lt_flag = (os.environ.get("LANGUAGE_TOOL_ENABLED") or "true").strip().lower() LANGUAGE_TOOL_ENABLED = _lt_flag not in {"0", "false", "no", "off"} and bool(LANGUAGE_TOOL_URL) # Optional lightweight semantic safety model. It is never used for generation. MINILM_MODEL = ( os.environ.get("MINILM_MODEL") or "sentence-transformers/all-MiniLM-L6-v2" ).strip() # Grammar cleanup is applied only after a candidate rewrite. Skipped sentences stay exact. _gfo = (os.environ.get("GRAMMAR_FIX_OUTPUT") or "true").strip().lower() GRAMMAR_FIX_OUTPUT = _gfo not in {"0", "false", "no", "off"} ENGINE_BATCH_PARAS = max( 1, min(int(os.environ.get("ENGINE_BATCH_PARAS", "20") or "20"), 100) ) ENGINE_MIN_CONFIDENCE = max( 0.0, min(float(os.environ.get("ENGINE_MIN_CONFIDENCE", "0.55") or "0.55"), 1.0) ) ENGINE_SAFETY_MIN = max( 0.0, min(float(os.environ.get("ENGINE_SAFETY_MIN", "0.80") or "0.80"), 1.0) ) # MiniLM safety gate (optional). When off, phrase/lexical use classical # collocation brakes instead of embedding similarity. _ems = (os.environ.get("ENGINE_USE_MINILM_SAFETY") or "true").strip().lower() ENGINE_USE_MINILM_SAFETY = _ems in {"1", "true", "yes", "on"} # When MiniLM is off: aggressive classical wording (option 2) maximizes # surface change per pass while keeping hard VO/sense blockers. Set false # to restore tight anti-drift mode (options 1/3 later). _eca = (os.environ.get("ENGINE_CLASSICAL_AGGRESSIVE") or "true").strip().lower() ENGINE_CLASSICAL_AGGRESSIVE = _eca in {"1", "true", "yes", "on"} # Rotate between equally-ranked structural options so repeating a request # returns a different valid rewrite instead of the identical one. _esv = (os.environ.get("ENGINE_STRUCTURAL_VARIATION") or "true").strip().lower() ENGINE_STRUCTURAL_VARIATION = _esv in {"1", "true", "yes", "on"} # Context-aware vocabulary refinement after structural/paraphrase rewrites. # Synonym count is chosen dynamically from sentence length (and polish). _elr = (os.environ.get("ENGINE_LEXICAL_REFINEMENT") or "true").strip().lower() ENGINE_LEXICAL_REFINEMENT = _elr in {"1", "true", "yes", "on"} # 0 = fully dynamic (recommended). Positive values only hard-cap that budget. ENGINE_LEXICAL_MAX_CHANGES = max( 0, min(int(os.environ.get("ENGINE_LEXICAL_MAX_CHANGES", "0") or "0"), 15), ) ENGINE_LEXICAL_MIN_WSD = max( 0.0, min(float(os.environ.get("ENGINE_LEXICAL_MIN_WSD", "0.14") or "0.14"), 1.0), ) ENGINE_LEXICAL_MIN_ZIPF = max( 0.0, min(float(os.environ.get("ENGINE_LEXICAL_MIN_ZIPF", "4.0") or "4.0"), 8.0), ) ENGINE_LEXICAL_MAX_FREQUENCY_GAP = max( 0.0, min( float( os.environ.get("ENGINE_LEXICAL_MAX_FREQUENCY_GAP", "0.60") or "0.60" ), 8.0, ), ) # Prefer everyday synonyms: allow a larger jump toward more-common words, # but only a tiny step toward rarer/advanced wording. ENGINE_LEXICAL_MAX_SIMPLER_GAP = max( 0.0, min( float( os.environ.get("ENGINE_LEXICAL_MAX_SIMPLER_GAP", "2.5") or "2.5" ), 8.0, ), ) ENGINE_LEXICAL_MAX_HARDER_GAP = max( 0.0, min( float( os.environ.get("ENGINE_LEXICAL_MAX_HARDER_GAP", "0.15") or "0.15" ), 2.0, ), ) _els = (os.environ.get("ENGINE_LEXICAL_PREFER_SIMPLER") or "true").strip().lower() ENGINE_LEXICAL_PREFER_SIMPLER = _els in {"1", "true", "yes", "on"} ENGINE_WORDNET_LEXICON = ( os.environ.get("ENGINE_WORDNET_LEXICON") or "oewn:2025" ).strip() # Require every rewriteable sentence to change wording when a safe option exists. _erw = (os.environ.get("ENGINE_REQUIRE_WORDING_CHANGE") or "true").strip().lower() ENGINE_REQUIRE_WORDING_CHANGE = _erw in {"1", "true", "yes", "on"} # Fixed cleft fallback is off by default; prefer local paraphrase instead. _efr = (os.environ.get("ENGINE_FORCE_REWRITE") or "false").strip().lower() ENGINE_FORCE_REWRITE = _efr in {"1", "true", "yes", "on"} # Local CPU T5 paraphraser (primary rewrite path when the model is available). _ep = (os.environ.get("ENGINE_PARAPHRASE") or "true").strip().lower() ENGINE_PARAPHRASE = _ep in {"1", "true", "yes", "on"} _epp = (os.environ.get("ENGINE_PARAPHRASE_PRIMARY") or "true").strip().lower() ENGINE_PARAPHRASE_PRIMARY = _epp in {"1", "true", "yes", "on"} ENGINE_PARAPHRASE_MODEL = ( os.environ.get("ENGINE_PARAPHRASE_MODEL") or "Vamsi/T5_Paraphrase_Paws" ).strip() ENGINE_PARAPHRASE_MIN_SIM = max( 0.0, min( float(os.environ.get("ENGINE_PARAPHRASE_MIN_SIM", "0.72") or "0.72"), 1.0, ), ) ENGINE_PARAPHRASE_NUM_RETURN = max( 1, min(int(os.environ.get("ENGINE_PARAPHRASE_NUM_RETURN", "6") or "6"), 8), ) ENGINE_PARAPHRASE_MAX_NEW_TOKENS = max( 16, min( int(os.environ.get("ENGINE_PARAPHRASE_MAX_NEW_TOKENS", "72") or "72"), 128, ), ) # Reject paraphrases whose surface form is still nearly identical to the source. ENGINE_PARAPHRASE_MAX_SURFACE = max( 0.5, min( float(os.environ.get("ENGINE_PARAPHRASE_MAX_SURFACE", "0.70") or "0.70"), 0.99, ), ) # When primary paraphrase is on, require at least this much surface distance. ENGINE_PARAPHRASE_MIN_DIVERGENCE = max( 0.0, min( float( os.environ.get("ENGINE_PARAPHRASE_MIN_DIVERGENCE", "0.30") or "0.30" ), 0.6, ), ) # Phrase-level rewrite: verb–object / modifier–noun spans via WordNet + optional T5. _ephr = (os.environ.get("ENGINE_PHRASE_REWRITE") or "true").strip().lower() ENGINE_PHRASE_REWRITE = _ephr in {"1", "true", "yes", "on"} # 0 = dynamic (1 normally, 2 with polish). Positive values hard-cap changes. ENGINE_PHRASE_MAX_CHANGES = max( 0, min(int(os.environ.get("ENGINE_PHRASE_MAX_CHANGES", "0") or "0"), 4), ) ENGINE_PHRASE_MIN_SIM = max( 0.0, min(float(os.environ.get("ENGINE_PHRASE_MIN_SIM", "0.74") or "0.74"), 1.0), ) # T5 span paraphrase is optional; WordNet phrase swaps are the default wording path. _ephr_t5 = (os.environ.get("ENGINE_PHRASE_USE_T5") or "false").strip().lower() ENGINE_PHRASE_USE_T5 = _ephr_t5 in {"1", "true", "yes", "on"} # Keep rewritten length near the source (trims only when clearly bloated). _epl = (os.environ.get("ENGINE_PRESERVE_LENGTH") or "true").strip().lower() ENGINE_PRESERVE_LENGTH = _epl in {"1", "true", "yes", "on"} # Split long clauses during structural fallback for more surface change. _esl = (os.environ.get("ENGINE_SPLIT_LONG") or "true").strip().lower() ENGINE_SPLIT_LONG = _esl in {"1", "true", "yes", "on"} ENGINE_SPLIT_MIN_WORDS = max( 10, min(int(os.environ.get("ENGINE_SPLIT_MIN_WORDS", "16") or "16"), 40), )