"""Central configuration: env, story styles, difficulty levels, file filters, palette. StoryCode reads a non-coder's project and explains it as a story. The values here are deliberately plain-language: labels a non-technical person sees should never contain jargon unless we explain it. """ from __future__ import annotations import os from dataclasses import dataclass from dotenv import load_dotenv load_dotenv() # --- Runtime / services ----------------------------------------------------- # The model runs on Modal (vLLM, OpenAI-compatible API). The Space is CPU-only. MODAL_ENDPOINT_URL = os.getenv("MODAL_ENDPOINT_URL", "").rstrip("/") MODAL_API_KEY = os.getenv("MODAL_API_KEY", "") MODEL_ID = os.getenv("MODEL_ID", "openbmb/MiniCPM4.1-8B") DB_PATH = os.getenv("STORYCODE_DB_PATH", "data/storycode.db") USE_ZEROGPU_FALLBACK = os.getenv("USE_ZEROGPU_FALLBACK", "0") == "1" # Generation limits (keep the Space responsive; map-reduce stays well under 32k ctx). LLM_TIMEOUT_S = int(os.getenv("LLM_TIMEOUT_S", "90")) MAX_FILES = int(os.getenv("STORYCODE_MAX_FILES", "60")) # analysed per project MAX_FILE_BYTES = int(os.getenv("STORYCODE_MAX_FILE_BYTES", str(200_000))) MAX_TOTAL_BYTES = int(os.getenv("STORYCODE_MAX_TOTAL_BYTES", str(5_000_000))) # --- Languages we understand (stated honestly in the UI) -------------------- # Extension -> language key. Anything else is listed but not deeply parsed. LANG_BY_EXT: dict[str, str] = { ".py": "python", ".js": "javascript", ".jsx": "javascript", ".ts": "typescript", ".tsx": "typescript", ".mjs": "javascript", ".cjs": "javascript", ".html": "html", ".htm": "html", ".css": "css", ".scss": "css", ".json": "json", ".yaml": "yaml", ".yml": "yaml", ".md": "markdown", } # Directories and files we never analyse (noise / not the user's own code). IGNORE_DIRS: frozenset[str] = frozenset({ "node_modules", ".git", ".svn", "__pycache__", ".venv", "venv", "env", "dist", "build", ".next", ".nuxt", "out", "target", ".idea", ".vscode", "coverage", ".pytest_cache", ".mypy_cache", "site-packages", ".cache", "vendor", "bower_components", ".gradio", }) IGNORE_FILES: frozenset[str] = frozenset({ "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", ".ds_store", "thumbs.db", }) # Binary-ish extensions to skip outright. BINARY_EXTS: frozenset[str] = frozenset({ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".svg", ".pdf", ".zip", ".gz", ".tar", ".mp3", ".mp4", ".wav", ".woff", ".woff2", ".ttf", ".otf", ".pyc", ".so", ".dll", ".exe", ".bin", ".db", ".sqlite", ".lock", }) # Manifest files the dependency explainer reads. DEP_MANIFESTS: frozenset[str] = frozenset({ "requirements.txt", "package.json", "pyproject.toml", "pipfile", }) # --- Roles ------------------------------------------------------------------- # A file's "role" drives the architecture-map grouping and the safe-to-edit # verdict. These are plain-English buckets, not technical layers. @dataclass(frozen=True) class Role: key: str label: str color: str # used in the diagram + chips ROLES: tuple[Role, ...] = ( Role("entry", "Front door", "#4b5bd6"), # what runs first / users hit Role("frontend", "What you see", "#3a7ca5"), # UI / pages / styles Role("backend", "The engine", "#6b4fb0"), # logic / services / API calls Role("data", "Memory", "#3f9b6b"), # db / storage / models / embeddings Role("config", "Settings", "#8a7a66"), # config / env / manifests Role("test", "Safety checks", "#9aa0a6"), # tests Role("util", "Helpers", "#b0793a"), # shared utilities Role("other", "Other", "#9aa0a6"), ) ROLE_KEYS: tuple[str, ...] = tuple(r.key for r in ROLES) ROLE_LABEL = {r.key: r.label for r in ROLES} ROLE_COLOR = {r.key: r.color for r in ROLES} # Filename / path hints used by analyzer.graph.classify_role (first match wins). ENTRY_NAMES: frozenset[str] = frozenset({ "app.py", "main.py", "manage.py", "server.py", "run.py", "wsgi.py", "asgi.py", "index.js", "index.ts", "index.jsx", "index.tsx", "main.js", "main.ts", "index.html", "app.js", "app.ts", "train.py", "play.py", "demo.py", "infer.py", "evaluate.py", "test.py", }) CONFIG_HINTS: tuple[str, ...] = ( "config", "settings", ".env", "requirements", "package.json", "pyproject", "dockerfile", "makefile", ".yaml", ".yml", ".toml", ".ini", ) DATA_HINTS: tuple[str, ...] = ( "db", "database", "model", "schema", "store", "embed", "vector", "index", "ingest", "migration", "seed", ) TEST_HINTS: tuple[str, ...] = ("test", "spec", "__tests__") UTIL_HINTS: tuple[str, ...] = ("util", "helper", "common", "lib", "shared", "tools") # --- Story styles ------------------------------------------------------------ # Simple Walkthrough is the DEFAULT (per the brief: creative styles can feel # patronising; they're opt-in). Each carries a short voice instruction handed to # the model, but the model only ever narrates facts we extracted. @dataclass(frozen=True) class Style: key: str label: str emoji: str voice: str # instruction injected into the narration prompt STYLES: tuple[Style, ...] = ( Style("plain", "Simple Walkthrough", "📋", "Explain plainly and warmly, step by step, no metaphors, no jargon. " "Like a patient friend who read the whole project for them."), Style("kids", "Kids Book", "🧸", "Tell it like a children's picture book. Each file is a friendly " "character with a job. Keep sentences short and gentle."), Style("thriller", "Thriller", "🕵️", "Tell it like a suspense thriller — the request arrives, tension " "builds as it moves through the code — but every claim must stay true."), Style("news", "News Report", "📰", "Report it like a breaking-news anchor: who did what, in what order, " "with punchy headlines. Accurate, never sensational about facts."), Style("recipe", "Recipe", "🍳", "Explain it like a cooking recipe: ingredients (the pieces) and steps " "(the flow), in order."), ) STYLE_KEYS: tuple[str, ...] = tuple(s.key for s in STYLES) STYLE_BY_KEY = {s.key: s for s in STYLES} DEFAULT_STYLE = "plain" @dataclass(frozen=True) class Difficulty: key: str label: str voice: str DIFFICULTIES: tuple[Difficulty, ...] = ( Difficulty("kid", "Explain like I'm 5", "Use no technical terms at all. Everyday words a child knows."), Difficulty("teen", "Teenager", "Light technical terms are fine if you explain each one in the " "same sentence."), Difficulty("adult", "Adult / curious beginner", "Full clarity. Technical terms are okay but define them the first " "time they appear."), ) DIFFICULTY_KEYS: tuple[str, ...] = tuple(d.key for d in DIFFICULTIES) DIFFICULTY_BY_KEY = {d.key: d for d in DIFFICULTIES} DEFAULT_DIFFICULTY = "teen" # --- Where did your code come from? (onboarding) ---------------------------- SOURCE_HINTS: tuple[str, ...] = ( "Claude / Claude Code", "Cursor", "ChatGPT", "Bolt / v0 / Lovable", "A ZIP someone sent me", "Not sure", ) # --- Visual palette (mirrors ui/styles.css; kept here for diagram + chips) --- PALETTE = { "paper": "#f7f4ef", "ink": "#1d2430", "accent": "#4b5bd6", "safe": "#3f9b6b", "careful": "#d8932f", "danger": "#c2503f", "muted": "#6b7280", "line": "#e7e3da", } # Safe-to-edit verdicts (computed in analyzer.graph, never by the model). SAFE = "safe" CAREFUL = "careful" DANGER = "danger" SAFETY_COLOR = {SAFE: PALETTE["safe"], CAREFUL: PALETTE["careful"], DANGER: PALETTE["danger"]} SAFETY_EMOJI = {SAFE: "🟢", CAREFUL: "🟠", DANGER: "🔴"} SAFETY_LABEL = {SAFE: "Safe to change", CAREFUL: "Change carefully", DANGER: "Don't touch"}