File size: 7,915 Bytes
71d239c 7603aa2 71d239c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """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"}
|