Spaces:
Sleeping
Sleeping
feat: switch to FastAPI + Docker SDK with visual UI (index.html + Chart.js)
Browse files- Dockerfile +33 -0
- README.md +2 -3
- app.py +154 -270
- requirements.txt +6 -8
- static/app.js +373 -0
- static/index.html +186 -0
- static/styles.css +614 -0
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
+
PIP_NO_CACHE_DIR=1 \
|
| 5 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
| 6 |
+
AETHER_LOG_LEVEL=INFO \
|
| 7 |
+
PORT=7860 \
|
| 8 |
+
HF_HOME=/tmp/hf_cache
|
| 9 |
+
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
+
ca-certificates \
|
| 12 |
+
curl \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
COPY requirements.txt .
|
| 18 |
+
RUN pip install --upgrade pip && pip install -r requirements.txt
|
| 19 |
+
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
RUN useradd -m -u 1000 user \
|
| 23 |
+
&& mkdir -p /tmp/hf_cache \
|
| 24 |
+
&& chown -R user:user /app /tmp/hf_cache
|
| 25 |
+
|
| 26 |
+
USER user
|
| 27 |
+
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
| 31 |
+
CMD curl -fsS http://127.0.0.1:7860/health || exit 1
|
| 32 |
+
|
| 33 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -3,9 +3,8 @@ title: AETHER-Ad Genesis
|
|
| 3 |
emoji: π
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
short_description: Emergent Ad-Idea Engine (Blending x Greimas x Pixar)
|
|
|
|
| 3 |
emoji: π
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
|
|
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
short_description: Emergent Ad-Idea Engine (Blending x Greimas x Pixar)
|
app.py
CHANGED
|
@@ -1,26 +1,33 @@
|
|
| 1 |
-
"""AETHER-Ad Genesis β
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import json
|
| 6 |
import logging
|
| 7 |
import os
|
| 8 |
import sys
|
| 9 |
-
import traceback
|
| 10 |
from pathlib import Path
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
ROOT = Path(__file__).resolve().parent
|
| 15 |
sys.path.insert(0, str(ROOT))
|
| 16 |
|
| 17 |
-
from aether_ad.core.
|
| 18 |
-
from aether_ad.core.
|
| 19 |
-
from aether_ad.core.
|
| 20 |
-
from aether_ad.
|
| 21 |
-
from aether_ad.llm.
|
| 22 |
-
from aether_ad.
|
|
|
|
| 23 |
|
|
|
|
| 24 |
logging.basicConfig(
|
| 25 |
level=os.getenv("AETHER_LOG_LEVEL", "INFO"),
|
| 26 |
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
@@ -29,284 +36,161 @@ log = logging.getLogger("aether_ad.app")
|
|
| 29 |
|
| 30 |
PRODUCTS_DIR = ROOT / "aether_ad" / "data" / "products"
|
| 31 |
TENSIONS_PATH = ROOT / "aether_ad" / "data" / "tensions" / "default_tensions.json"
|
| 32 |
-
|
| 33 |
-
TENSIONS = CONTEXT.tension_archetypes
|
| 34 |
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
continue
|
| 44 |
-
return items
|
| 45 |
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
return (
|
| 55 |
-
f"**{t.name}** β _{t.description}_ \n"
|
| 56 |
-
f"`S1`: {sq.S1} Β· `S2`: {sq.S2} \n"
|
| 57 |
-
f"`~S1`: {sq.not_S1} Β· `~S2`: {sq.not_S2} \n"
|
| 58 |
-
f"**payoff**: {t.emotional_payoff}"
|
| 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 |
-
f"surprise={sc.surprise:.2f} Β· risk={sc.risk:.2f} Β· "
|
| 85 |
-
f"gating_pass={sc.gating_passed}/5\n\n---\n"
|
| 86 |
-
)
|
| 87 |
-
return "\n".join(lines)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def _scores_table(scored: list[ScoredSeed]) -> list[list]:
|
| 91 |
-
rows = []
|
| 92 |
-
for i, s in enumerate(scored, 1):
|
| 93 |
-
rows.append(
|
| 94 |
-
[
|
| 95 |
-
i,
|
| 96 |
-
s.seed.seed_id,
|
| 97 |
-
", ".join(s.seed.rules_applied),
|
| 98 |
-
round(s.score.novelty, 3),
|
| 99 |
-
round(s.score.utility, 3),
|
| 100 |
-
round(s.score.affect, 3),
|
| 101 |
-
round(s.score.humor_or_pathos, 3),
|
| 102 |
-
round(s.score.surprise, 3),
|
| 103 |
-
round(s.score.risk, 3),
|
| 104 |
-
round(s.score.final, 3),
|
| 105 |
-
]
|
| 106 |
-
)
|
| 107 |
-
return rows
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def run_pipeline(
|
| 111 |
-
product_path: str,
|
| 112 |
-
tension_id: str,
|
| 113 |
-
persona_text: str,
|
| 114 |
-
duration: int,
|
| 115 |
-
n_seeds: int,
|
| 116 |
-
top_k: int,
|
| 117 |
-
backend_name: str,
|
| 118 |
-
rng_seed: int,
|
| 119 |
-
llm_model: str,
|
| 120 |
-
progress: gr.Progress = gr.Progress(track_tqdm=False),
|
| 121 |
-
):
|
| 122 |
-
try:
|
| 123 |
-
if not product_path:
|
| 124 |
-
return "μ νμ μ ννμΈμ.", [], "{}", ""
|
| 125 |
-
|
| 126 |
-
progress(0.05, desc="Loading genome...")
|
| 127 |
-
genome: ProductGenome = load_genome(product_path)
|
| 128 |
-
|
| 129 |
-
backend_name = (backend_name or "fireworks").lower()
|
| 130 |
-
llm = None
|
| 131 |
-
if backend_name != "none":
|
| 132 |
-
progress(0.10, desc=f"Connecting {backend_name} backend...")
|
| 133 |
-
try:
|
| 134 |
-
if backend_name == "fireworks":
|
| 135 |
-
if not os.getenv("FIREWORKS_API_KEY"):
|
| 136 |
-
return (
|
| 137 |
-
"β FIREWORKS_API_KEYκ° Space secretμ μ€μ λμ§ μμμ΅λλ€. "
|
| 138 |
-
"Settings β Variables and secretsμμ FIREWORKS_API_KEYλ₯Ό μΆκ°νκ±°λ, "
|
| 139 |
-
"Backendλ₯Ό 'none'μΌλ‘ λ°κΏ off-LLM ν
νλ¦Ώ λͺ¨λλ‘ μ€ννμΈμ.",
|
| 140 |
-
[],
|
| 141 |
-
"{}",
|
| 142 |
-
"",
|
| 143 |
-
)
|
| 144 |
-
llm = FireworksKimiBackend(model=llm_model or None)
|
| 145 |
-
elif backend_name == "hf":
|
| 146 |
-
if not os.getenv("HF_TOKEN"):
|
| 147 |
-
return (
|
| 148 |
-
"β HF_TOKENμ΄ Space secretμ μ€μ λμ§ μμμ΅λλ€. "
|
| 149 |
-
"Backendλ₯Ό 'none'μΌλ‘ λ°κΏ off-LLM ν
νλ¦Ώ λͺ¨λλ‘ μ€ννμΈμ.",
|
| 150 |
-
[],
|
| 151 |
-
"{}",
|
| 152 |
-
"",
|
| 153 |
-
)
|
| 154 |
-
llm = HFInferenceBackend(model=llm_model or None)
|
| 155 |
-
else:
|
| 156 |
-
return f"β Unknown backend '{backend_name}'", [], "{}", ""
|
| 157 |
-
except Exception as e:
|
| 158 |
-
log.exception("backend init failed")
|
| 159 |
-
return f"LLM μ΄κΈ°ν μ€ν¨: {e}", [], "{}", ""
|
| 160 |
-
|
| 161 |
-
engine = GenesisEngine(
|
| 162 |
-
llm=llm,
|
| 163 |
-
context=CONTEXT,
|
| 164 |
-
seed_count_per_run=int(n_seeds),
|
| 165 |
-
keep_top_k=int(top_k),
|
| 166 |
-
rng_seed=int(rng_seed) if rng_seed else None,
|
| 167 |
-
)
|
| 168 |
|
| 169 |
-
progress(0.25, desc="Stage 1-3: corpus β encoding β incubation...")
|
| 170 |
-
progress(0.55, desc="Stage 4: emergence (Pixar spine)...")
|
| 171 |
-
progress(0.80, desc="Stage 5: filtering (wow filter)...")
|
| 172 |
-
scored = engine.generate(
|
| 173 |
-
product=genome,
|
| 174 |
-
tension_id=tension_id,
|
| 175 |
-
persona_text=persona_text or "μΌλ° μλΉμ",
|
| 176 |
-
duration=15 if int(duration) == 15 else 30,
|
| 177 |
-
)
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
summary = (
|
| 183 |
-
f"β
{len(scored)}κ° μλ μμ± μλ£ (μμ {top_k}). "
|
| 184 |
-
f"νκ· final={sum(s.score.final for s in scored) / max(1, len(scored)):.3f}, "
|
| 185 |
-
f"μ΅κ³ final={max((s.score.final for s in scored), default=0.0):.3f}."
|
| 186 |
-
)
|
| 187 |
-
return summary, rows, raw, md
|
| 188 |
-
except Exception as e:
|
| 189 |
-
log.exception("pipeline failed")
|
| 190 |
-
return f"β μ€ν μ€ν¨: {e}\n\n{traceback.format_exc()}", [], "{}", ""
|
| 191 |
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
|
| 197 |
-
theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="purple")
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
-
with gr.Row():
|
| 207 |
-
with gr.Column(scale=1):
|
| 208 |
-
gr.Markdown("### 1οΈβ£ Product Genome")
|
| 209 |
-
product_dd = gr.Dropdown(
|
| 210 |
-
choices=products,
|
| 211 |
-
label="Product",
|
| 212 |
-
value=products[0][1] if products else None,
|
| 213 |
-
)
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
| 228 |
)
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
with gr.Row():
|
| 236 |
-
backend_dd = gr.Dropdown(
|
| 237 |
-
choices=[
|
| 238 |
-
("Fireworks Β· Kimi-K2P6 (default)", "fireworks"),
|
| 239 |
-
("HF Inference Β· Qwen2.5-72B", "hf"),
|
| 240 |
-
("none (template fallback)", "none"),
|
| 241 |
-
],
|
| 242 |
-
value="fireworks",
|
| 243 |
-
label="LLM backend",
|
| 244 |
-
)
|
| 245 |
-
rng_seed_in = gr.Number(value=42, precision=0, label="rng_seed")
|
| 246 |
-
llm_model = gr.Textbox(
|
| 247 |
-
label="Model id (leave blank to use backend default)",
|
| 248 |
-
value=os.getenv(
|
| 249 |
-
"FIREWORKS_MODEL", "accounts/fireworks/models/kimi-k2p6"
|
| 250 |
-
),
|
| 251 |
)
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
wrap=True,
|
| 267 |
-
)
|
| 268 |
-
with gr.TabItem("Storyboards"):
|
| 269 |
-
storyboards_md = gr.Markdown()
|
| 270 |
-
with gr.TabItem("Raw JSON"):
|
| 271 |
-
raw_json = gr.Code(language="json")
|
| 272 |
-
|
| 273 |
-
tension_dd.change(fn=_format_tension_detail, inputs=tension_dd, outputs=tension_md)
|
| 274 |
-
|
| 275 |
-
run_btn.click(
|
| 276 |
-
fn=run_pipeline,
|
| 277 |
-
inputs=[
|
| 278 |
-
product_dd, tension_dd, persona, duration, n_seeds, top_k,
|
| 279 |
-
backend_dd, rng_seed_in, llm_model,
|
| 280 |
-
],
|
| 281 |
-
outputs=[summary, scores_table, raw_json, storyboards_md],
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
with gr.Accordion("π Theoretical Pillars", open=False):
|
| 285 |
-
gr.Markdown(
|
| 286 |
-
"""
|
| 287 |
-
- **Fauconnier & Turner (2002)** β *The Way We Think*, Conceptual Blending.
|
| 288 |
-
- **Greimas (1966)** β *SΓ©mantique structurale*, Semiotic Square (S1 / S2 / ~S1 / ~S2).
|
| 289 |
-
- **Eberle (1971)** β SCAMPER (Substitute Β· Combine Β· Adapt Β· Modify Β· Put-to Β· Eliminate Β· Reverse).
|
| 290 |
-
- **Stuhlfaut & Yoo (2011)** β Advertising Creativity 4-D Model.
|
| 291 |
-
- **Boden (2007)** β Novelty Β· Utility Β· Surprise.
|
| 292 |
-
- **Coats / McKee (2011)** β Pixar Story Spine (6 beats).
|
| 293 |
-
- **Floch (1990)** β Four consumption-value axes (practical Β· utopian Β· critical Β· playful).
|
| 294 |
-
"""
|
| 295 |
-
)
|
| 296 |
-
|
| 297 |
-
with gr.Accordion("π§ͺ 15 Collision Rules", open=False):
|
| 298 |
-
from aether_ad.core.collision import ALL_RULES
|
| 299 |
-
rows = [[r.rule_id, r.name, r.pattern, r.theoretical_anchor] for r in ALL_RULES]
|
| 300 |
-
gr.Dataframe(
|
| 301 |
-
value=rows,
|
| 302 |
-
headers=["rule_id", "name", "pattern", "theoretical_anchor"],
|
| 303 |
-
interactive=False,
|
| 304 |
-
wrap=True,
|
| 305 |
-
)
|
| 306 |
-
|
| 307 |
-
return demo
|
| 308 |
-
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AETHER-Ad Genesis β FastAPI backend for the visual HF Space (Docker SDK)."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import logging
|
| 6 |
import os
|
| 7 |
import sys
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
+
from typing import Any, Literal
|
| 10 |
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from fastapi import FastAPI, HTTPException
|
| 13 |
+
from fastapi.concurrency import run_in_threadpool
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from fastapi.responses import FileResponse
|
| 16 |
+
from fastapi.staticfiles import StaticFiles
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
|
| 19 |
ROOT = Path(__file__).resolve().parent
|
| 20 |
sys.path.insert(0, str(ROOT))
|
| 21 |
|
| 22 |
+
from aether_ad.core.collision import ALL_RULES # noqa: E402
|
| 23 |
+
from aether_ad.core.context import load_context # noqa: E402
|
| 24 |
+
from aether_ad.core.engine import GenesisEngine # noqa: E402
|
| 25 |
+
from aether_ad.core.genome import load_genome # noqa: E402
|
| 26 |
+
from aether_ad.llm.fireworks_kimi import FireworksKimiBackend # noqa: E402
|
| 27 |
+
from aether_ad.llm.hf_inference import HFInferenceBackend # noqa: E402
|
| 28 |
+
from aether_ad.version import __version__ # noqa: E402
|
| 29 |
|
| 30 |
+
load_dotenv()
|
| 31 |
logging.basicConfig(
|
| 32 |
level=os.getenv("AETHER_LOG_LEVEL", "INFO"),
|
| 33 |
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
|
|
| 36 |
|
| 37 |
PRODUCTS_DIR = ROOT / "aether_ad" / "data" / "products"
|
| 38 |
TENSIONS_PATH = ROOT / "aether_ad" / "data" / "tensions" / "default_tensions.json"
|
| 39 |
+
STATIC_DIR = ROOT / "static"
|
|
|
|
| 40 |
|
| 41 |
+
CONTEXT = load_context(TENSIONS_PATH)
|
| 42 |
|
| 43 |
+
app = FastAPI(title="AETHER-Ad Genesis", version=__version__)
|
| 44 |
+
app.add_middleware(
|
| 45 |
+
CORSMiddleware,
|
| 46 |
+
allow_origins=["*"],
|
| 47 |
+
allow_methods=["*"],
|
| 48 |
+
allow_headers=["*"],
|
| 49 |
+
)
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
+
@app.get("/", include_in_schema=False)
|
| 53 |
+
async def index() -> FileResponse:
|
| 54 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 55 |
|
| 56 |
|
| 57 |
+
@app.get("/health")
|
| 58 |
+
async def health() -> dict[str, Any]:
|
| 59 |
+
return {"status": "ok", "version": __version__}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
+
@app.get("/api/meta")
|
| 63 |
+
async def meta() -> dict[str, Any]:
|
| 64 |
+
return {
|
| 65 |
+
"version": __version__,
|
| 66 |
+
"default_backend": os.getenv("AETHER_DEFAULT_BACKEND", "fireworks"),
|
| 67 |
+
"has_fireworks_key": bool(os.getenv("FIREWORKS_API_KEY")),
|
| 68 |
+
"has_hf_token": bool(os.getenv("HF_TOKEN")),
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@app.get("/api/products")
|
| 73 |
+
async def products() -> list[dict[str, Any]]:
|
| 74 |
+
items: list[dict[str, Any]] = []
|
| 75 |
+
for p in sorted(PRODUCTS_DIR.glob("*.json")):
|
| 76 |
+
try:
|
| 77 |
+
g = load_genome(p)
|
| 78 |
+
items.append(
|
| 79 |
+
{
|
| 80 |
+
"product_id": g.product_id,
|
| 81 |
+
"brand": g.brand,
|
| 82 |
+
"category": g.category,
|
| 83 |
+
"price_tier": g.price_tier,
|
| 84 |
+
"atom_count": len(g.functional_atoms),
|
| 85 |
+
"atoms": [{"id": a.id, "name": a.name} for a in g.functional_atoms],
|
| 86 |
+
}
|
| 87 |
)
|
| 88 |
+
except Exception as e:
|
| 89 |
+
log.warning("skip product %s: %s", p, e)
|
| 90 |
+
return items
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
@app.get("/api/tensions")
|
| 94 |
+
async def tensions() -> list[dict[str, Any]]:
|
| 95 |
+
return [t.model_dump() for t in CONTEXT.tension_archetypes]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
+
@app.get("/api/rules")
|
| 99 |
+
async def rules() -> list[dict[str, Any]]:
|
| 100 |
+
return [r.model_dump() for r in ALL_RULES]
|
| 101 |
|
|
|
|
| 102 |
|
| 103 |
+
class GenerateRequest(BaseModel):
|
| 104 |
+
product_id: str
|
| 105 |
+
tension_id: str
|
| 106 |
+
persona: str = "μΌλ° μλΉμ"
|
| 107 |
+
duration: Literal[15, 30] = 15
|
| 108 |
+
n_seeds: int = Field(15, ge=1, le=50)
|
| 109 |
+
top_k: int = Field(5, ge=1, le=20)
|
| 110 |
+
backend: Literal["fireworks", "hf", "none"] = "fireworks"
|
| 111 |
+
model: str | None = None
|
| 112 |
+
rng_seed: int | None = 42
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
@app.post("/api/generate")
|
| 116 |
+
async def generate(req: GenerateRequest) -> dict[str, Any]:
|
| 117 |
+
product_path: Path | None = None
|
| 118 |
+
for p in PRODUCTS_DIR.glob("*.json"):
|
| 119 |
+
try:
|
| 120 |
+
if load_genome(p).product_id == req.product_id:
|
| 121 |
+
product_path = p
|
| 122 |
+
break
|
| 123 |
+
except Exception:
|
| 124 |
+
continue
|
| 125 |
+
if product_path is None:
|
| 126 |
+
raise HTTPException(status_code=404, detail=f"Product '{req.product_id}' not found")
|
| 127 |
+
genome = load_genome(product_path)
|
| 128 |
|
| 129 |
+
llm = None
|
| 130 |
+
try:
|
| 131 |
+
if req.backend == "fireworks":
|
| 132 |
+
if not os.getenv("FIREWORKS_API_KEY"):
|
| 133 |
+
raise HTTPException(
|
| 134 |
+
status_code=400,
|
| 135 |
+
detail="FIREWORKS_API_KEY is not set in Space secrets.",
|
| 136 |
)
|
| 137 |
+
llm = FireworksKimiBackend(model=req.model or None)
|
| 138 |
+
elif req.backend == "hf":
|
| 139 |
+
if not os.getenv("HF_TOKEN"):
|
| 140 |
+
raise HTTPException(
|
| 141 |
+
status_code=400,
|
| 142 |
+
detail="HF_TOKEN is not set in Space secrets.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
)
|
| 144 |
+
llm = HFInferenceBackend(model=req.model or None)
|
| 145 |
+
except HTTPException:
|
| 146 |
+
raise
|
| 147 |
+
except Exception as e:
|
| 148 |
+
log.exception("backend init failed")
|
| 149 |
+
raise HTTPException(status_code=500, detail=f"Backend init failed: {e}") from e
|
| 150 |
+
|
| 151 |
+
engine = GenesisEngine(
|
| 152 |
+
llm=llm,
|
| 153 |
+
context=CONTEXT,
|
| 154 |
+
seed_count_per_run=req.n_seeds,
|
| 155 |
+
keep_top_k=req.top_k,
|
| 156 |
+
rng_seed=req.rng_seed,
|
| 157 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
try:
|
| 160 |
+
scored = await run_in_threadpool(
|
| 161 |
+
engine.generate,
|
| 162 |
+
product=genome,
|
| 163 |
+
tension_id=req.tension_id,
|
| 164 |
+
persona_text=req.persona or "μΌλ° μλΉμ",
|
| 165 |
+
duration=req.duration,
|
| 166 |
+
)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
log.exception("pipeline failed")
|
| 169 |
+
raise HTTPException(status_code=500, detail=f"Pipeline failed: {e}") from e
|
| 170 |
+
|
| 171 |
+
seeds = [s.model_dump() for s in scored]
|
| 172 |
+
avg_final = sum(s["score"]["final"] for s in seeds) / max(1, len(seeds))
|
| 173 |
+
top_final = max((s["score"]["final"] for s in seeds), default=0.0)
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
"summary": {
|
| 177 |
+
"count": len(seeds),
|
| 178 |
+
"avg_final": avg_final,
|
| 179 |
+
"top_final": top_final,
|
| 180 |
+
"product_id": req.product_id,
|
| 181 |
+
"tension_id": req.tension_id,
|
| 182 |
+
"backend": req.backend,
|
| 183 |
+
"model": req.model,
|
| 184 |
+
"duration": req.duration,
|
| 185 |
+
},
|
| 186 |
+
"seeds": seeds,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
if STATIC_DIR.exists():
|
| 191 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
if __name__ == "__main__": # pragma: no cover
|
| 195 |
+
import uvicorn
|
| 196 |
+
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
|
requirements.txt
CHANGED
|
@@ -1,15 +1,13 @@
|
|
| 1 |
pydantic>=2.5.0
|
| 2 |
jsonschema>=4.20.0
|
| 3 |
-
openai>=1.30.0
|
| 4 |
httpx>=0.27.0
|
| 5 |
python-dotenv>=1.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
typer>=0.12.0
|
| 7 |
rich>=13.7.0
|
| 8 |
pyyaml>=6.0.1
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
sentence-transformers>=2.7.0
|
| 12 |
-
structlog>=24.1.0
|
| 13 |
-
huggingface_hub>=0.24.0
|
| 14 |
-
gradio>=5.0.0
|
| 15 |
-
audioop-lts>=0.2.1; python_version>="3.13"
|
|
|
|
| 1 |
pydantic>=2.5.0
|
| 2 |
jsonschema>=4.20.0
|
|
|
|
| 3 |
httpx>=0.27.0
|
| 4 |
python-dotenv>=1.0.0
|
| 5 |
+
numpy>=1.26.0
|
| 6 |
+
huggingface_hub>=0.24.0
|
| 7 |
+
structlog>=24.1.0
|
| 8 |
+
openai>=1.30.0
|
| 9 |
typer>=0.12.0
|
| 10 |
rich>=13.7.0
|
| 11 |
pyyaml>=6.0.1
|
| 12 |
+
fastapi>=0.110.0
|
| 13 |
+
uvicorn[standard]>=0.27.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/app.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
|
| 3 |
+
const $ = (sel, el = document) => el.querySelector(sel);
|
| 4 |
+
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
|
| 5 |
+
|
| 6 |
+
const API = {
|
| 7 |
+
meta: () => fetch('/api/meta').then(r => r.json()),
|
| 8 |
+
products: () => fetch('/api/products').then(r => r.json()),
|
| 9 |
+
tensions: () => fetch('/api/tensions').then(r => r.json()),
|
| 10 |
+
rules: () => fetch('/api/rules').then(r => r.json()),
|
| 11 |
+
generate: (body) => fetch('/api/generate', {
|
| 12 |
+
method: 'POST',
|
| 13 |
+
headers: { 'Content-Type': 'application/json' },
|
| 14 |
+
body: JSON.stringify(body),
|
| 15 |
+
}).then(async r => {
|
| 16 |
+
if (!r.ok) {
|
| 17 |
+
const j = await r.json().catch(() => ({ detail: r.statusText }));
|
| 18 |
+
throw new Error(j.detail || `HTTP ${r.status}`);
|
| 19 |
+
}
|
| 20 |
+
return r.json();
|
| 21 |
+
}),
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
let TENSIONS = [];
|
| 25 |
+
let PRODUCTS = [];
|
| 26 |
+
let CHARTS = [];
|
| 27 |
+
let PIPELINE_TIMER = null;
|
| 28 |
+
|
| 29 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 30 |
+
|
| 31 |
+
async function init() {
|
| 32 |
+
wireInputs();
|
| 33 |
+
await Promise.all([loadMeta(), loadProducts(), loadTensions(), loadRules()]);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function loadMeta() {
|
| 37 |
+
try {
|
| 38 |
+
const m = await API.meta();
|
| 39 |
+
$('#version').textContent = m.version;
|
| 40 |
+
const sel = $('#backendSel');
|
| 41 |
+
sel.querySelectorAll('option').forEach(o => {
|
| 42 |
+
if (o.value === 'fireworks' && !m.has_fireworks_key) {
|
| 43 |
+
o.disabled = true; o.textContent += ' (no key)';
|
| 44 |
+
}
|
| 45 |
+
if (o.value === 'hf' && !m.has_hf_token) {
|
| 46 |
+
o.disabled = true; o.textContent += ' (no token)';
|
| 47 |
+
}
|
| 48 |
+
});
|
| 49 |
+
sel.value = m.default_backend || 'fireworks';
|
| 50 |
+
syncBackendPlaceholder();
|
| 51 |
+
} catch (e) { console.error('meta failed', e); }
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async function loadProducts() {
|
| 55 |
+
PRODUCTS = await API.products();
|
| 56 |
+
const sel = $('#productSel'); sel.innerHTML = '';
|
| 57 |
+
PRODUCTS.forEach(p => {
|
| 58 |
+
const o = document.createElement('option');
|
| 59 |
+
o.value = p.product_id;
|
| 60 |
+
o.textContent = `${p.brand} Β· ${p.category}`;
|
| 61 |
+
sel.appendChild(o);
|
| 62 |
+
});
|
| 63 |
+
updateProductMeta();
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function updateProductMeta() {
|
| 67 |
+
const pid = $('#productSel').value;
|
| 68 |
+
const p = PRODUCTS.find(x => x.product_id === pid);
|
| 69 |
+
if (!p) return;
|
| 70 |
+
const atomList = (p.atoms || []).map(a => a.name).join(' Β· ');
|
| 71 |
+
$('#productMeta').innerHTML = `
|
| 72 |
+
<strong>${p.product_id}</strong> Β· tier: <strong>${p.price_tier || 'β'}</strong> Β· ${p.atom_count} functional atoms
|
| 73 |
+
<br/><span style="color: var(--text-subtle);">${atomList}</span>
|
| 74 |
+
`;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async function loadTensions() {
|
| 78 |
+
TENSIONS = await API.tensions();
|
| 79 |
+
const sel = $('#tensionSel'); sel.innerHTML = '';
|
| 80 |
+
TENSIONS.forEach(t => {
|
| 81 |
+
const o = document.createElement('option');
|
| 82 |
+
o.value = t.id; o.textContent = `${t.id} Β· ${t.name}`;
|
| 83 |
+
sel.appendChild(o);
|
| 84 |
+
});
|
| 85 |
+
updateGreimas();
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function updateGreimas() {
|
| 89 |
+
const id = $('#tensionSel').value;
|
| 90 |
+
const t = TENSIONS.find(x => x.id === id);
|
| 91 |
+
if (!t) return;
|
| 92 |
+
const sq = t.greimas_square;
|
| 93 |
+
$('[data-pos="s1"]').textContent = sq.S1;
|
| 94 |
+
$('[data-pos="s2"]').textContent = sq.S2;
|
| 95 |
+
$('[data-pos="ns1"]').textContent = sq.not_S1;
|
| 96 |
+
$('[data-pos="ns2"]').textContent = sq.not_S2;
|
| 97 |
+
$('#tensionPayoff').innerHTML = `${escapeHtml(t.description || '')}<br/><strong>payoff:</strong> ${escapeHtml(t.emotional_payoff)}`;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
async function loadRules() {
|
| 101 |
+
const rules = await API.rules();
|
| 102 |
+
const rows = [
|
| 103 |
+
`<div class="rule-row header">
|
| 104 |
+
<div>rule_id</div><div>name</div><div>pattern</div><div>theoretical_anchor</div>
|
| 105 |
+
</div>`,
|
| 106 |
+
];
|
| 107 |
+
rules.forEach(r => {
|
| 108 |
+
rows.push(`
|
| 109 |
+
<div class="rule-row">
|
| 110 |
+
<div class="rule-id">${r.rule_id}</div>
|
| 111 |
+
<div><strong>${escapeHtml(r.name)}</strong></div>
|
| 112 |
+
<div>${escapeHtml(r.pattern)}</div>
|
| 113 |
+
<div class="rule-anchor">${escapeHtml(r.theoretical_anchor)}</div>
|
| 114 |
+
</div>`);
|
| 115 |
+
});
|
| 116 |
+
$('#rulesTable').innerHTML = rows.join('');
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function wireInputs() {
|
| 120 |
+
$('#productSel').addEventListener('change', updateProductMeta);
|
| 121 |
+
$('#tensionSel').addEventListener('change', updateGreimas);
|
| 122 |
+
$('#nSeeds').addEventListener('input', e => $('#nSeedsVal').textContent = e.target.value);
|
| 123 |
+
$('#topK').addEventListener('input', e => $('#topKVal').textContent = e.target.value);
|
| 124 |
+
$('#backendSel').addEventListener('change', syncBackendPlaceholder);
|
| 125 |
+
$('#genBtn').addEventListener('click', runGenerate);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function syncBackendPlaceholder() {
|
| 129 |
+
const v = $('#backendSel').value;
|
| 130 |
+
const placeholders = {
|
| 131 |
+
fireworks: 'accounts/fireworks/models/kimi-k2p6',
|
| 132 |
+
hf: 'Qwen/Qwen2.5-72B-Instruct',
|
| 133 |
+
none: '(not used)',
|
| 134 |
+
};
|
| 135 |
+
$('#modelIn').placeholder = placeholders[v] || '';
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
async function runGenerate() {
|
| 139 |
+
const body = {
|
| 140 |
+
product_id: $('#productSel').value,
|
| 141 |
+
tension_id: $('#tensionSel').value,
|
| 142 |
+
persona: $('#personaIn').value.trim() || 'μΌλ° μλΉμ',
|
| 143 |
+
duration: parseInt($$('input[name="duration"]').find(r => r.checked).value, 10),
|
| 144 |
+
n_seeds: parseInt($('#nSeeds').value, 10),
|
| 145 |
+
top_k: parseInt($('#topK').value, 10),
|
| 146 |
+
backend: $('#backendSel').value,
|
| 147 |
+
model: $('#modelIn').value.trim() || null,
|
| 148 |
+
rng_seed: parseInt($('#rngSeed').value || '42', 10),
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
setRunning(true);
|
| 152 |
+
$('#errBox').hidden = true;
|
| 153 |
+
$('#emptyState').style.display = 'none';
|
| 154 |
+
animatePipeline();
|
| 155 |
+
|
| 156 |
+
try {
|
| 157 |
+
const r = await API.generate(body);
|
| 158 |
+
completePipeline();
|
| 159 |
+
renderResults(r);
|
| 160 |
+
} catch (err) {
|
| 161 |
+
resetPipeline();
|
| 162 |
+
const msg = err && err.message ? err.message : String(err);
|
| 163 |
+
$('#errBox').hidden = false;
|
| 164 |
+
$('#errBox').textContent = `β ${msg}`;
|
| 165 |
+
if (!$('#summaryBar').hidden) { /* keep previous results visible */ }
|
| 166 |
+
else $('#emptyState').style.display = '';
|
| 167 |
+
console.error(err);
|
| 168 |
+
} finally {
|
| 169 |
+
setRunning(false);
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function setRunning(running) {
|
| 174 |
+
$('#genBtn').disabled = running;
|
| 175 |
+
$('#genBtn .btn-spinner').hidden = !running;
|
| 176 |
+
$('#genBtn .btn-label').textContent = running ? 'μμ± μ€β¦' : 'β‘ Generate';
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function animatePipeline() {
|
| 180 |
+
resetPipeline();
|
| 181 |
+
const stages = ['corpus', 'encoding', 'incubation', 'emergence', 'filtering'];
|
| 182 |
+
let i = 0;
|
| 183 |
+
const tick = () => {
|
| 184 |
+
if (i > 0) {
|
| 185 |
+
const prev = document.querySelector(`.stage[data-stage="${stages[i-1]}"]`);
|
| 186 |
+
prev.classList.remove('active'); prev.classList.add('done');
|
| 187 |
+
}
|
| 188 |
+
if (i < stages.length) {
|
| 189 |
+
document.querySelector(`.stage[data-stage="${stages[i]}"]`).classList.add('active');
|
| 190 |
+
i++;
|
| 191 |
+
PIPELINE_TIMER = setTimeout(tick, 1200);
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
tick();
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
function completePipeline() {
|
| 198 |
+
if (PIPELINE_TIMER) { clearTimeout(PIPELINE_TIMER); PIPELINE_TIMER = null; }
|
| 199 |
+
$$('.stage').forEach(s => { s.classList.remove('active'); s.classList.add('done'); });
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function resetPipeline() {
|
| 203 |
+
if (PIPELINE_TIMER) { clearTimeout(PIPELINE_TIMER); PIPELINE_TIMER = null; }
|
| 204 |
+
$$('.stage').forEach(s => s.classList.remove('active', 'done'));
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
function renderResults(data) {
|
| 208 |
+
$('#summaryBar').hidden = false;
|
| 209 |
+
$('#sumCount').textContent = data.summary.count;
|
| 210 |
+
$('#sumAvg').textContent = (data.summary.avg_final || 0).toFixed(3);
|
| 211 |
+
$('#sumTop').textContent = (data.summary.top_final || 0).toFixed(3);
|
| 212 |
+
$('#sumBackend').textContent = data.summary.backend + (data.summary.model ? ' Β· ' + data.summary.model.split('/').pop() : '');
|
| 213 |
+
|
| 214 |
+
CHARTS.forEach(c => { try { c.destroy(); } catch(_){} });
|
| 215 |
+
CHARTS = [];
|
| 216 |
+
const box = $('#seedsBox'); box.innerHTML = '';
|
| 217 |
+
|
| 218 |
+
data.seeds.forEach((s, idx) => {
|
| 219 |
+
const card = renderSeedCard(s, idx + 1);
|
| 220 |
+
box.appendChild(card);
|
| 221 |
+
});
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function renderSeedCard(s, rank) {
|
| 225 |
+
const seed = s.seed;
|
| 226 |
+
const score = s.score;
|
| 227 |
+
const final = score.final;
|
| 228 |
+
const color = final >= 0.5 ? '#10b981' : final >= 0.3 ? '#f59e0b' : '#ef4444';
|
| 229 |
+
const R = 34, CIRC = 2 * Math.PI * R;
|
| 230 |
+
const offset = CIRC * (1 - Math.max(0, Math.min(1, final)));
|
| 231 |
+
|
| 232 |
+
const card = document.createElement('div');
|
| 233 |
+
card.className = 'seed-card';
|
| 234 |
+
card.innerHTML = `
|
| 235 |
+
<div class="seed-head">
|
| 236 |
+
<div class="seed-rank-block">
|
| 237 |
+
<span class="seed-rank-label">Rank</span>
|
| 238 |
+
<div class="seed-rank">#${rank}</div>
|
| 239 |
+
</div>
|
| 240 |
+
<div class="seed-info">
|
| 241 |
+
<div class="seed-id">${escapeHtml(seed.seed_id)}</div>
|
| 242 |
+
<div class="seed-rules">
|
| 243 |
+
${seed.rules_applied.map(r => `<span class="rule-pill">${escapeHtml(r)}</span>`).join('')}
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="final-gauge" title="final score">
|
| 247 |
+
<svg viewBox="0 0 80 80">
|
| 248 |
+
<circle class="final-gauge-track" cx="40" cy="40" r="${R}"/>
|
| 249 |
+
<circle class="final-gauge-fill" cx="40" cy="40" r="${R}"
|
| 250 |
+
stroke="${color}"
|
| 251 |
+
stroke-dasharray="${CIRC}"
|
| 252 |
+
stroke-dashoffset="${offset}"/>
|
| 253 |
+
</svg>
|
| 254 |
+
<div class="final-gauge-text">
|
| 255 |
+
<div>
|
| 256 |
+
<div class="final-gauge-value">${final.toFixed(2)}</div>
|
| 257 |
+
<div class="final-gauge-label">final</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<div class="seed-concept">${escapeHtml(seed.concept)}</div>
|
| 264 |
+
|
| 265 |
+
<div class="seed-section-title">Pixar Story Spine Β· ${seed.duration}s Β· ${seed.beats.length} beats</div>
|
| 266 |
+
<div class="timeline">
|
| 267 |
+
<div class="timeline-bar">${renderBeatBar(seed.beats, seed.duration)}</div>
|
| 268 |
+
<div class="timeline-scale">${renderTicks(seed.duration)}</div>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div class="seed-section-title">Beats</div>
|
| 272 |
+
<div class="beats-detail">
|
| 273 |
+
${seed.beats.map(b => `
|
| 274 |
+
<div class="beat-row">
|
| 275 |
+
<div class="beat-row-time">${b.time_range[0]}β${b.time_range[1]}s</div>
|
| 276 |
+
<div class="beat-row-name">${escapeHtml(b.beat)}</div>
|
| 277 |
+
<div class="beat-row-content">${escapeHtml(b.content || 'β')}</div>
|
| 278 |
+
</div>
|
| 279 |
+
`).join('')}
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
<div class="scores-section">
|
| 283 |
+
<div class="radar-wrap"><canvas></canvas></div>
|
| 284 |
+
<div class="scores-table">
|
| 285 |
+
${scoreItem('Novelty', score.novelty)}
|
| 286 |
+
${scoreItem('Utility', score.utility)}
|
| 287 |
+
${scoreItem('Affect', score.affect)}
|
| 288 |
+
${scoreItem('Humor/Pathos', score.humor_or_pathos)}
|
| 289 |
+
${scoreItem('Surprise', score.surprise)}
|
| 290 |
+
${scoreItem('Risk', score.risk, { risk: true })}
|
| 291 |
+
${scoreItem('Gating', (score.gating_passed || 0) + '/5', { raw: true })}
|
| 292 |
+
${scoreItem('Final', score.final, { bold: true })}
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
`;
|
| 296 |
+
|
| 297 |
+
const canvas = $('canvas', card);
|
| 298 |
+
const chart = new Chart(canvas.getContext('2d'), {
|
| 299 |
+
type: 'radar',
|
| 300 |
+
data: {
|
| 301 |
+
labels: ['Novelty', 'Utility', 'Affect', 'Humor/Pathos', 'Surprise'],
|
| 302 |
+
datasets: [{
|
| 303 |
+
data: [
|
| 304 |
+
score.novelty, score.utility, score.affect,
|
| 305 |
+
score.humor_or_pathos, score.surprise,
|
| 306 |
+
],
|
| 307 |
+
backgroundColor: 'rgba(79, 70, 229, 0.18)',
|
| 308 |
+
borderColor: '#4f46e5',
|
| 309 |
+
borderWidth: 2,
|
| 310 |
+
pointBackgroundColor: '#4f46e5',
|
| 311 |
+
pointBorderColor: '#ffffff',
|
| 312 |
+
pointRadius: 3,
|
| 313 |
+
}],
|
| 314 |
+
},
|
| 315 |
+
options: {
|
| 316 |
+
responsive: true, maintainAspectRatio: false,
|
| 317 |
+
plugins: { legend: { display: false } },
|
| 318 |
+
scales: {
|
| 319 |
+
r: {
|
| 320 |
+
suggestedMin: 0, suggestedMax: 1,
|
| 321 |
+
ticks: { display: false, stepSize: 0.25 },
|
| 322 |
+
grid: { color: '#e6e9f0' },
|
| 323 |
+
angleLines: { color: '#e6e9f0' },
|
| 324 |
+
pointLabels:{ font: { size: 10, weight: '600' }, color: '#475569' },
|
| 325 |
+
},
|
| 326 |
+
},
|
| 327 |
+
},
|
| 328 |
+
});
|
| 329 |
+
CHARTS.push(chart);
|
| 330 |
+
return card;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
function renderBeatBar(beats, duration) {
|
| 334 |
+
return beats.map((b, i) => {
|
| 335 |
+
const pct = ((b.time_range[1] - b.time_range[0]) / duration) * 100;
|
| 336 |
+
const short = (b.beat || '').split('_')[0] || b.beat;
|
| 337 |
+
return `
|
| 338 |
+
<div class="beat-seg beat-color-${Math.min(i, 5)}" style="width: ${pct}%;"
|
| 339 |
+
title="${escapeHtml(b.beat)} Β· ${b.time_range[0]}-${b.time_range[1]}s">
|
| 340 |
+
<div class="beat-seg-name">${escapeHtml(short)}</div>
|
| 341 |
+
<div class="beat-seg-time">${b.time_range[0]}-${b.time_range[1]}s</div>
|
| 342 |
+
</div>
|
| 343 |
+
`;
|
| 344 |
+
}).join('');
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function renderTicks(duration) {
|
| 348 |
+
const ticks = duration === 15 ? [0, 3, 6, 9, 12, 15] : [0, 5, 10, 15, 20, 25, 30];
|
| 349 |
+
return ticks.map(t => {
|
| 350 |
+
const left = (t / duration) * 100;
|
| 351 |
+
return `<span class="timeline-tick" style="left: ${left}%;">${t}s</span>`;
|
| 352 |
+
}).join('');
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
function scoreItem(label, val, opts = {}) {
|
| 356 |
+
const v = opts.raw ? val : (typeof val === 'number' ? val.toFixed(3) : val);
|
| 357 |
+
const style = [];
|
| 358 |
+
if (opts.risk && Number(val) > 0) style.push('color: var(--danger);');
|
| 359 |
+
if (opts.bold) style.push('color: var(--primary); font-size: 15px;');
|
| 360 |
+
return `
|
| 361 |
+
<div class="score-item">
|
| 362 |
+
<span class="score-item-label">${label}</span>
|
| 363 |
+
<span class="score-item-value" style="${style.join(' ')}">${v}</span>
|
| 364 |
+
</div>
|
| 365 |
+
`;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function escapeHtml(s) {
|
| 369 |
+
if (s == null) return '';
|
| 370 |
+
return String(s).replace(/[&<>"']/g, ch => ({
|
| 371 |
+
'&':'&','<':'<','>':'>','"':'"',"'":''',
|
| 372 |
+
}[ch]));
|
| 373 |
+
}
|
static/index.html
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>AETHER-Ad Genesis Β· Emergent Ad-Idea Engine</title>
|
| 7 |
+
<meta name="description" content="Fauconnier-Turner Γ Greimas Γ SCAMPER Γ Pixar Story Spine Γ Stuhlfaut 4D Γ Boden Surprise β emergent ad-idea generation engine." />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
| 11 |
+
<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" rel="stylesheet" />
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
|
| 13 |
+
<link rel="stylesheet" href="/static/styles.css" />
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<header class="hero">
|
| 17 |
+
<div class="container hero-inner">
|
| 18 |
+
<div class="hero-brand">
|
| 19 |
+
<div class="hero-logo">π</div>
|
| 20 |
+
<div>
|
| 21 |
+
<h1 class="hero-title">AETHER-Ad <span class="grad">Genesis</span></h1>
|
| 22 |
+
<p class="hero-subtitle">μ°½λ°μ κ΄κ³ μμ΄λμ΄ μμ± μμ§ Β· v<span id="version">0.2.0</span> Β· VIDRAFT Β· μ§λμ AI</p>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="hero-pillars">
|
| 26 |
+
<span class="pill">Fauconnier-Turner Β· Blending</span>
|
| 27 |
+
<span class="pill">Greimas Β· Semiotic Square</span>
|
| 28 |
+
<span class="pill">Eberle Β· SCAMPER</span>
|
| 29 |
+
<span class="pill">Coats Β· Pixar Spine</span>
|
| 30 |
+
<span class="pill">Stuhlfaut Β· 4-D Creativity</span>
|
| 31 |
+
<span class="pill">Boden Β· Surprise</span>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
</header>
|
| 35 |
+
|
| 36 |
+
<main class="container main">
|
| 37 |
+
<aside class="sidebar">
|
| 38 |
+
<div class="card">
|
| 39 |
+
<div class="card-header"><h2><span class="step">1</span>Product Genome</h2></div>
|
| 40 |
+
<div class="field">
|
| 41 |
+
<label for="productSel">Product</label>
|
| 42 |
+
<select id="productSel"></select>
|
| 43 |
+
<div id="productMeta" class="meta"></div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div class="card">
|
| 48 |
+
<div class="card-header"><h2><span class="step">2</span>Tension <small>Greimas Square</small></h2></div>
|
| 49 |
+
<div class="field">
|
| 50 |
+
<label for="tensionSel">Tension archetype</label>
|
| 51 |
+
<select id="tensionSel"></select>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="greimas">
|
| 54 |
+
<div class="g-cell g-s1"><span class="g-label">S1 positive</span><span class="g-value" data-pos="s1">β</span></div>
|
| 55 |
+
<div class="g-cell g-s2"><span class="g-label">S2 contrary</span><span class="g-value" data-pos="s2">β</span></div>
|
| 56 |
+
<div class="g-cell g-ns1"><span class="g-label">~S1 contradiction</span><span class="g-value" data-pos="ns1">β</span></div>
|
| 57 |
+
<div class="g-cell g-ns2"><span class="g-label">~S2 contradiction</span><span class="g-value" data-pos="ns2">β</span></div>
|
| 58 |
+
</div>
|
| 59 |
+
<div id="tensionPayoff" class="meta"></div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="card">
|
| 63 |
+
<div class="card-header"><h2><span class="step">3</span>Persona</h2></div>
|
| 64 |
+
<div class="field">
|
| 65 |
+
<label for="personaIn">Target persona (free-text)</label>
|
| 66 |
+
<textarea id="personaIn" rows="2" placeholder="μ: 10λ λ¨νμ, μ’μ ">10λ λ¨νμ, μ’μ </textarea>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="card">
|
| 71 |
+
<div class="card-header"><h2><span class="step">4</span>Pipeline</h2></div>
|
| 72 |
+
<div class="field">
|
| 73 |
+
<label>Spot duration</label>
|
| 74 |
+
<div class="radio-group">
|
| 75 |
+
<label class="radio-chip"><input type="radio" name="duration" value="15" checked /><span>15s Β· 5 beats</span></label>
|
| 76 |
+
<label class="radio-chip"><input type="radio" name="duration" value="30" /><span>30s Β· 6 beats</span></label>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="field">
|
| 80 |
+
<label>n_seeds Β· <output id="nSeedsVal">15</output></label>
|
| 81 |
+
<input type="range" id="nSeeds" min="5" max="40" value="15" />
|
| 82 |
+
</div>
|
| 83 |
+
<div class="field">
|
| 84 |
+
<label>top_k Β· <output id="topKVal">5</output></label>
|
| 85 |
+
<input type="range" id="topK" min="1" max="10" value="5" />
|
| 86 |
+
</div>
|
| 87 |
+
<div class="field">
|
| 88 |
+
<label for="backendSel">LLM backend</label>
|
| 89 |
+
<select id="backendSel">
|
| 90 |
+
<option value="fireworks" selected>Fireworks Β· Kimi-K2P6</option>
|
| 91 |
+
<option value="hf">HF Inference Β· Qwen2.5-72B</option>
|
| 92 |
+
<option value="none">Template (off-LLM)</option>
|
| 93 |
+
</select>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="field">
|
| 96 |
+
<label for="modelIn">Model id <small>(optional override)</small></label>
|
| 97 |
+
<input type="text" id="modelIn" placeholder="accounts/fireworks/models/kimi-k2p6" />
|
| 98 |
+
</div>
|
| 99 |
+
<div class="field">
|
| 100 |
+
<label for="rngSeed">rng_seed</label>
|
| 101 |
+
<input type="number" id="rngSeed" value="42" />
|
| 102 |
+
</div>
|
| 103 |
+
<button id="genBtn" class="btn-primary">
|
| 104 |
+
<span class="btn-spinner" hidden></span>
|
| 105 |
+
<span class="btn-label">β‘ Generate</span>
|
| 106 |
+
</button>
|
| 107 |
+
<div id="errBox" class="err" hidden></div>
|
| 108 |
+
</div>
|
| 109 |
+
</aside>
|
| 110 |
+
|
| 111 |
+
<section class="results">
|
| 112 |
+
<div class="pipeline card-flat">
|
| 113 |
+
<h3 class="pipeline-title">5-Stage Pipeline Β· Young(1940) Adapted</h3>
|
| 114 |
+
<div class="pipeline-stages">
|
| 115 |
+
<div class="stage" data-stage="corpus">
|
| 116 |
+
<div class="stage-dot"></div>
|
| 117 |
+
<div class="stage-label">Corpus</div>
|
| 118 |
+
</div>
|
| 119 |
+
<div class="stage" data-stage="encoding">
|
| 120 |
+
<div class="stage-dot"></div>
|
| 121 |
+
<div class="stage-label">Encoding</div>
|
| 122 |
+
</div>
|
| 123 |
+
<div class="stage" data-stage="incubation">
|
| 124 |
+
<div class="stage-dot"></div>
|
| 125 |
+
<div class="stage-label">Incubation</div>
|
| 126 |
+
</div>
|
| 127 |
+
<div class="stage" data-stage="emergence">
|
| 128 |
+
<div class="stage-dot"></div>
|
| 129 |
+
<div class="stage-label">Emergence</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="stage" data-stage="filtering">
|
| 132 |
+
<div class="stage-dot"></div>
|
| 133 |
+
<div class="stage-label">Filtering</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div id="emptyState" class="empty">
|
| 139 |
+
<div class="empty-icon">β¨</div>
|
| 140 |
+
<h3>μ°½λ°μ κ΄κ³ μμ΄λμ΄λ₯Ό μμ±ν©λλ€</h3>
|
| 141 |
+
<p>μ’μΈ‘μμ μ ν Β· κΈ΄μ₯ Β· νλ₯΄μλλ₯Ό μ€μ νκ³ <strong>Generate</strong>λ₯Ό λλ₯΄μΈμ.<br/>
|
| 142 |
+
Fireworks Kimi-K2P6μ΄ 6κ° μ΄λ‘ νλ μμ κ²½μ ν΄ Top-K μλλ₯Ό λ°νν©λλ€.</p>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div id="summaryBar" class="summary" hidden>
|
| 146 |
+
<div class="summary-metric"><div class="summary-label">Seeds</div><div class="summary-value" id="sumCount">β</div></div>
|
| 147 |
+
<div class="summary-metric"><div class="summary-label">Avg final</div><div class="summary-value" id="sumAvg">β</div></div>
|
| 148 |
+
<div class="summary-metric"><div class="summary-label">Top final</div><div class="summary-value" id="sumTop">β</div></div>
|
| 149 |
+
<div class="summary-metric"><div class="summary-label">Backend</div><div class="summary-value summary-value-sm" id="sumBackend">β</div></div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div id="seedsBox" class="seeds"></div>
|
| 153 |
+
</section>
|
| 154 |
+
</main>
|
| 155 |
+
|
| 156 |
+
<footer class="footer container">
|
| 157 |
+
<details class="foot-details">
|
| 158 |
+
<summary>π Theoretical Foundation</summary>
|
| 159 |
+
<div class="foot-body">
|
| 160 |
+
<ul>
|
| 161 |
+
<li><strong>Fauconnier & Turner (2002)</strong> β <em>The Way We Think</em>, Conceptual Blending.</li>
|
| 162 |
+
<li><strong>Greimas (1966)</strong> β <em>SΓ©mantique structurale</em>, Semiotic Square (S1 / S2 / ~S1 / ~S2).</li>
|
| 163 |
+
<li><strong>Eberle (1971)</strong> β SCAMPER (Substitute Β· Combine Β· Adapt Β· Modify Β· Put-to Β· Eliminate Β· Reverse).</li>
|
| 164 |
+
<li><strong>Stuhlfaut & Yoo (2011)</strong> β Advertising Creativity 4-D (divergence Β· relevance Β· artistry Β· elaboration).</li>
|
| 165 |
+
<li><strong>Boden (2007)</strong> β Novelty Β· Utility Β· Surprise.</li>
|
| 166 |
+
<li><strong>Coats / McKee (2011)</strong> β Pixar Story Spine (6 beats).</li>
|
| 167 |
+
<li><strong>Floch (1990)</strong> β Four consumption-value axes (practical Β· utopian Β· critical Β· playful).</li>
|
| 168 |
+
</ul>
|
| 169 |
+
</div>
|
| 170 |
+
</details>
|
| 171 |
+
|
| 172 |
+
<details class="foot-details">
|
| 173 |
+
<summary>π§ͺ 15 Collision Rules</summary>
|
| 174 |
+
<div id="rulesTable" class="rules-table"></div>
|
| 175 |
+
</details>
|
| 176 |
+
|
| 177 |
+
<div class="foot-meta">
|
| 178 |
+
Apache-2.0 Β· VIDRAFT Β· μ§λμ AI Β·
|
| 179 |
+
<a href="https://huggingface.co/spaces/SeaWolf-AI/aether-ad-genesis" target="_blank" rel="noopener">HF Space</a> Β·
|
| 180 |
+
Docker SDK Β· FastAPI
|
| 181 |
+
</div>
|
| 182 |
+
</footer>
|
| 183 |
+
|
| 184 |
+
<script src="/static/app.js"></script>
|
| 185 |
+
</body>
|
| 186 |
+
</html>
|
static/styles.css
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #ffffff;
|
| 3 |
+
--surface: #fafbfc;
|
| 4 |
+
--surface-2: #f4f6fa;
|
| 5 |
+
--border: #e6e9f0;
|
| 6 |
+
--border-strong: #cfd4df;
|
| 7 |
+
--text: #0f172a;
|
| 8 |
+
--text-muted: #64748b;
|
| 9 |
+
--text-subtle: #94a3b8;
|
| 10 |
+
--primary: #4f46e5;
|
| 11 |
+
--primary-hover: #4338ca;
|
| 12 |
+
--primary-light: #eef2ff;
|
| 13 |
+
--accent: #a855f7;
|
| 14 |
+
--accent-light: #f3e8ff;
|
| 15 |
+
--success: #10b981;
|
| 16 |
+
--warn: #f59e0b;
|
| 17 |
+
--danger: #ef4444;
|
| 18 |
+
--radius: 14px;
|
| 19 |
+
--radius-sm: 8px;
|
| 20 |
+
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
|
| 21 |
+
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06);
|
| 22 |
+
--shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.10);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 26 |
+
html { scroll-behavior: smooth; }
|
| 27 |
+
body {
|
| 28 |
+
font-family: "Pretendard Variable", Pretendard, "Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
| 29 |
+
background: var(--bg);
|
| 30 |
+
color: var(--text);
|
| 31 |
+
line-height: 1.6;
|
| 32 |
+
-webkit-font-smoothing: antialiased;
|
| 33 |
+
-moz-osx-font-smoothing: grayscale;
|
| 34 |
+
}
|
| 35 |
+
a { color: var(--primary); }
|
| 36 |
+
|
| 37 |
+
.container { max-width: 1440px; margin: 0 auto; padding: 0 28px; }
|
| 38 |
+
|
| 39 |
+
/* ββ HERO ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 40 |
+
.hero {
|
| 41 |
+
padding: 48px 0 40px;
|
| 42 |
+
background: linear-gradient(180deg, #fafbff 0%, #ffffff 100%);
|
| 43 |
+
border-bottom: 1px solid var(--border);
|
| 44 |
+
position: relative;
|
| 45 |
+
overflow: hidden;
|
| 46 |
+
}
|
| 47 |
+
.hero::before {
|
| 48 |
+
content: ""; position: absolute; top: -240px; right: -180px;
|
| 49 |
+
width: 620px; height: 620px;
|
| 50 |
+
background: radial-gradient(circle, rgba(168, 85, 247, 0.14), transparent 70%);
|
| 51 |
+
pointer-events: none;
|
| 52 |
+
}
|
| 53 |
+
.hero::after {
|
| 54 |
+
content: ""; position: absolute; bottom: -260px; left: -120px;
|
| 55 |
+
width: 540px; height: 540px;
|
| 56 |
+
background: radial-gradient(circle, rgba(79, 70, 229, 0.12), transparent 70%);
|
| 57 |
+
pointer-events: none;
|
| 58 |
+
}
|
| 59 |
+
.hero-inner { position: relative; display: flex; flex-direction: column; gap: 22px; }
|
| 60 |
+
.hero-brand { display: flex; align-items: center; gap: 18px; }
|
| 61 |
+
.hero-logo {
|
| 62 |
+
font-size: 44px;
|
| 63 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 64 |
+
width: 76px; height: 76px;
|
| 65 |
+
display: grid; place-items: center;
|
| 66 |
+
border-radius: 20px;
|
| 67 |
+
color: white;
|
| 68 |
+
box-shadow: 0 10px 28px rgba(79, 70, 229, 0.35);
|
| 69 |
+
}
|
| 70 |
+
.hero-title {
|
| 71 |
+
font-size: 38px; font-weight: 800;
|
| 72 |
+
letter-spacing: -0.03em; line-height: 1.08;
|
| 73 |
+
}
|
| 74 |
+
.hero-title .grad {
|
| 75 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 76 |
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
| 77 |
+
}
|
| 78 |
+
.hero-subtitle { font-size: 15px; color: var(--text-muted); margin-top: 6px; }
|
| 79 |
+
.hero-pillars { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 80 |
+
.pill {
|
| 81 |
+
padding: 7px 14px;
|
| 82 |
+
border-radius: 999px;
|
| 83 |
+
background: white;
|
| 84 |
+
color: var(--primary);
|
| 85 |
+
font-size: 12px; font-weight: 600;
|
| 86 |
+
border: 1px solid rgba(79, 70, 229, 0.22);
|
| 87 |
+
box-shadow: var(--shadow-sm);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* ββ MAIN LAYOUT βββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 91 |
+
.main {
|
| 92 |
+
display: grid;
|
| 93 |
+
grid-template-columns: 390px 1fr;
|
| 94 |
+
gap: 32px;
|
| 95 |
+
padding-top: 32px;
|
| 96 |
+
padding-bottom: 56px;
|
| 97 |
+
}
|
| 98 |
+
@media (max-width: 1024px) {
|
| 99 |
+
.main { grid-template-columns: 1fr; }
|
| 100 |
+
.sidebar { position: static !important; max-height: none !important; }
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.sidebar {
|
| 104 |
+
display: flex; flex-direction: column; gap: 16px;
|
| 105 |
+
position: sticky; top: 20px; align-self: start;
|
| 106 |
+
max-height: calc(100vh - 40px);
|
| 107 |
+
overflow-y: auto;
|
| 108 |
+
padding-right: 6px;
|
| 109 |
+
}
|
| 110 |
+
.sidebar::-webkit-scrollbar { width: 6px; }
|
| 111 |
+
.sidebar::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
|
| 112 |
+
|
| 113 |
+
.card, .card-flat {
|
| 114 |
+
background: white;
|
| 115 |
+
border: 1px solid var(--border);
|
| 116 |
+
border-radius: var(--radius);
|
| 117 |
+
padding: 22px;
|
| 118 |
+
box-shadow: var(--shadow-sm);
|
| 119 |
+
}
|
| 120 |
+
.card-flat { padding: 20px 22px; }
|
| 121 |
+
|
| 122 |
+
.card-header { margin-bottom: 16px; }
|
| 123 |
+
.card-header h2 {
|
| 124 |
+
font-size: 12px; font-weight: 700;
|
| 125 |
+
display: flex; align-items: center; gap: 10px;
|
| 126 |
+
text-transform: uppercase; letter-spacing: 0.08em;
|
| 127 |
+
color: var(--text);
|
| 128 |
+
}
|
| 129 |
+
.card-header h2 small {
|
| 130 |
+
font-size: 11px; color: var(--text-muted);
|
| 131 |
+
font-weight: 500; text-transform: none; letter-spacing: 0;
|
| 132 |
+
margin-left: auto;
|
| 133 |
+
}
|
| 134 |
+
.step {
|
| 135 |
+
width: 24px; height: 24px;
|
| 136 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 137 |
+
color: white; border-radius: 7px;
|
| 138 |
+
display: grid; place-items: center;
|
| 139 |
+
font-size: 11px; font-weight: 700;
|
| 140 |
+
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.28);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.field { margin-bottom: 14px; }
|
| 144 |
+
.field:last-child { margin-bottom: 0; }
|
| 145 |
+
.field label {
|
| 146 |
+
display: block; font-size: 12px; font-weight: 600;
|
| 147 |
+
color: var(--text-muted); margin-bottom: 7px;
|
| 148 |
+
}
|
| 149 |
+
.field label small { font-weight: 400; color: var(--text-subtle); }
|
| 150 |
+
.field output {
|
| 151 |
+
font-weight: 700; color: var(--primary);
|
| 152 |
+
font-variant-numeric: tabular-nums;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
select, input[type="text"], input[type="number"], textarea {
|
| 156 |
+
width: 100%; padding: 10px 12px;
|
| 157 |
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
| 158 |
+
background: white; font: inherit; font-size: 14px; color: var(--text);
|
| 159 |
+
transition: border-color 0.15s, box-shadow 0.15s;
|
| 160 |
+
font-family: inherit;
|
| 161 |
+
}
|
| 162 |
+
textarea { resize: vertical; min-height: 48px; }
|
| 163 |
+
select:focus, input:focus, textarea:focus {
|
| 164 |
+
outline: none; border-color: var(--primary);
|
| 165 |
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.14);
|
| 166 |
+
}
|
| 167 |
+
input[type="range"] { padding: 0; width: 100%; accent-color: var(--primary); }
|
| 168 |
+
|
| 169 |
+
.radio-group { display: flex; gap: 8px; }
|
| 170 |
+
.radio-chip { flex: 1; display: block; position: relative; cursor: pointer; }
|
| 171 |
+
.radio-chip input { position: absolute; opacity: 0; pointer-events: none; }
|
| 172 |
+
.radio-chip span {
|
| 173 |
+
display: block; padding: 10px 12px; text-align: center;
|
| 174 |
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
| 175 |
+
font-size: 13px; font-weight: 600;
|
| 176 |
+
transition: all 0.15s; background: white;
|
| 177 |
+
}
|
| 178 |
+
.radio-chip input:checked + span {
|
| 179 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 180 |
+
color: white; border-color: transparent;
|
| 181 |
+
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.28);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.meta {
|
| 185 |
+
font-size: 12px; color: var(--text-muted);
|
| 186 |
+
margin-top: 10px; line-height: 1.55;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* ββ GREIMAS SQUARE ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 190 |
+
.greimas {
|
| 191 |
+
display: grid; grid-template-columns: 1fr 1fr;
|
| 192 |
+
gap: 8px; margin: 12px 0 2px;
|
| 193 |
+
}
|
| 194 |
+
.g-cell {
|
| 195 |
+
padding: 11px 12px; border-radius: 10px;
|
| 196 |
+
border: 1px solid var(--border); background: var(--surface);
|
| 197 |
+
min-height: 70px;
|
| 198 |
+
transition: transform 0.15s, box-shadow 0.15s;
|
| 199 |
+
}
|
| 200 |
+
.g-cell.g-s1 {
|
| 201 |
+
border-color: rgba(79, 70, 229, 0.28);
|
| 202 |
+
background: linear-gradient(135deg, var(--primary-light), #ffffff);
|
| 203 |
+
}
|
| 204 |
+
.g-cell.g-s2 {
|
| 205 |
+
border-color: rgba(168, 85, 247, 0.28);
|
| 206 |
+
background: linear-gradient(135deg, var(--accent-light), #ffffff);
|
| 207 |
+
}
|
| 208 |
+
.g-cell.g-ns1 { background: linear-gradient(135deg, #f8fafc, #ffffff); }
|
| 209 |
+
.g-cell.g-ns2 { background: linear-gradient(135deg, #f8fafc, #ffffff); }
|
| 210 |
+
.g-label {
|
| 211 |
+
display: block; font-size: 10px; font-weight: 700;
|
| 212 |
+
color: var(--text-muted); margin-bottom: 5px;
|
| 213 |
+
font-family: "JetBrains Mono", monospace;
|
| 214 |
+
text-transform: uppercase; letter-spacing: 0.05em;
|
| 215 |
+
}
|
| 216 |
+
.g-value {
|
| 217 |
+
display: block; font-size: 12.5px; font-weight: 500;
|
| 218 |
+
color: var(--text); line-height: 1.35;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/* ββ BUTTONS βββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 222 |
+
.btn-primary {
|
| 223 |
+
width: 100%; padding: 13px 16px; margin-top: 8px;
|
| 224 |
+
border: none; border-radius: var(--radius-sm);
|
| 225 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 226 |
+
color: white; font-weight: 700; font-size: 14.5px;
|
| 227 |
+
cursor: pointer;
|
| 228 |
+
transition: transform 0.1s, box-shadow 0.15s;
|
| 229 |
+
display: flex; align-items: center; justify-content: center; gap: 10px;
|
| 230 |
+
box-shadow: 0 6px 18px rgba(79, 70, 229, 0.28);
|
| 231 |
+
font-family: inherit;
|
| 232 |
+
}
|
| 233 |
+
.btn-primary:hover:not(:disabled) {
|
| 234 |
+
transform: translateY(-1px);
|
| 235 |
+
box-shadow: 0 8px 24px rgba(79, 70, 229, 0.38);
|
| 236 |
+
}
|
| 237 |
+
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
| 238 |
+
.btn-spinner {
|
| 239 |
+
width: 16px; height: 16px;
|
| 240 |
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
| 241 |
+
border-top-color: white; border-radius: 50%;
|
| 242 |
+
animation: spin 0.7s linear infinite;
|
| 243 |
+
}
|
| 244 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 245 |
+
|
| 246 |
+
.err {
|
| 247 |
+
margin-top: 12px; padding: 10px 12px;
|
| 248 |
+
background: #fef2f2; border: 1px solid #fecaca;
|
| 249 |
+
border-radius: var(--radius-sm);
|
| 250 |
+
color: var(--danger); font-size: 13px; line-height: 1.5;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* ββ RESULTS βββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 254 |
+
.results { display: flex; flex-direction: column; gap: 16px; }
|
| 255 |
+
|
| 256 |
+
/* Pipeline */
|
| 257 |
+
.pipeline-title {
|
| 258 |
+
font-size: 11px; font-weight: 700;
|
| 259 |
+
color: var(--text-muted);
|
| 260 |
+
text-transform: uppercase; letter-spacing: 0.08em;
|
| 261 |
+
margin-bottom: 14px;
|
| 262 |
+
}
|
| 263 |
+
.pipeline-stages {
|
| 264 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 265 |
+
position: relative;
|
| 266 |
+
}
|
| 267 |
+
.pipeline-stages::before {
|
| 268 |
+
content: ""; position: absolute;
|
| 269 |
+
top: 14px; left: 4%; right: 4%;
|
| 270 |
+
height: 2px; background: var(--border);
|
| 271 |
+
z-index: 0;
|
| 272 |
+
}
|
| 273 |
+
.stage {
|
| 274 |
+
flex: 1; display: flex; flex-direction: column; align-items: center;
|
| 275 |
+
position: relative; z-index: 1; gap: 8px;
|
| 276 |
+
}
|
| 277 |
+
.stage-dot {
|
| 278 |
+
width: 30px; height: 30px;
|
| 279 |
+
background: white; border: 2px solid var(--border);
|
| 280 |
+
border-radius: 50%; position: relative;
|
| 281 |
+
transition: all 0.3s;
|
| 282 |
+
}
|
| 283 |
+
.stage-label {
|
| 284 |
+
font-size: 11px; font-weight: 700; color: var(--text-muted);
|
| 285 |
+
text-transform: uppercase; letter-spacing: 0.04em;
|
| 286 |
+
}
|
| 287 |
+
.stage.active .stage-dot {
|
| 288 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 289 |
+
border-color: transparent;
|
| 290 |
+
box-shadow: 0 0 0 6px rgba(79, 70, 229, 0.18);
|
| 291 |
+
animation: pulse 1.2s ease-in-out infinite;
|
| 292 |
+
}
|
| 293 |
+
.stage.done .stage-dot {
|
| 294 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 295 |
+
border-color: transparent;
|
| 296 |
+
}
|
| 297 |
+
.stage.done .stage-dot::after {
|
| 298 |
+
content: "β"; position: absolute; inset: 0;
|
| 299 |
+
display: grid; place-items: center;
|
| 300 |
+
color: white; font-size: 14px; font-weight: 700;
|
| 301 |
+
}
|
| 302 |
+
.stage.active .stage-label, .stage.done .stage-label { color: var(--primary); }
|
| 303 |
+
@keyframes pulse {
|
| 304 |
+
0%, 100% { box-shadow: 0 0 0 6px rgba(79, 70, 229, 0.18); }
|
| 305 |
+
50% { box-shadow: 0 0 0 11px rgba(79, 70, 229, 0.08); }
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* Empty state */
|
| 309 |
+
.empty {
|
| 310 |
+
text-align: center; padding: 72px 28px;
|
| 311 |
+
background: white;
|
| 312 |
+
border: 2px dashed var(--border);
|
| 313 |
+
border-radius: var(--radius);
|
| 314 |
+
}
|
| 315 |
+
.empty-icon { font-size: 60px; margin-bottom: 14px; }
|
| 316 |
+
.empty h3 { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
|
| 317 |
+
.empty p { color: var(--text-muted); font-size: 14px; line-height: 1.7; }
|
| 318 |
+
|
| 319 |
+
/* Summary bar */
|
| 320 |
+
.summary {
|
| 321 |
+
display: grid; grid-template-columns: repeat(4, 1fr);
|
| 322 |
+
gap: 14px;
|
| 323 |
+
background: linear-gradient(135deg, var(--primary-light), var(--accent-light));
|
| 324 |
+
border: 1px solid rgba(79, 70, 229, 0.18);
|
| 325 |
+
border-radius: var(--radius);
|
| 326 |
+
padding: 20px 22px;
|
| 327 |
+
}
|
| 328 |
+
.summary-metric { display: flex; flex-direction: column; }
|
| 329 |
+
.summary-label {
|
| 330 |
+
font-size: 11px; font-weight: 700;
|
| 331 |
+
color: var(--text-muted);
|
| 332 |
+
text-transform: uppercase; letter-spacing: 0.06em;
|
| 333 |
+
margin-bottom: 6px;
|
| 334 |
+
}
|
| 335 |
+
.summary-value {
|
| 336 |
+
font-size: 24px; font-weight: 800;
|
| 337 |
+
color: var(--text); letter-spacing: -0.02em;
|
| 338 |
+
font-variant-numeric: tabular-nums;
|
| 339 |
+
}
|
| 340 |
+
.summary-value-sm { font-size: 15px; font-weight: 700; }
|
| 341 |
+
|
| 342 |
+
/* ββ SEED CARD βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 343 |
+
.seeds { display: flex; flex-direction: column; gap: 18px; }
|
| 344 |
+
|
| 345 |
+
.seed-card {
|
| 346 |
+
background: white;
|
| 347 |
+
border: 1px solid var(--border);
|
| 348 |
+
border-radius: var(--radius);
|
| 349 |
+
padding: 22px;
|
| 350 |
+
box-shadow: var(--shadow-sm);
|
| 351 |
+
transition: box-shadow 0.2s, transform 0.2s;
|
| 352 |
+
overflow: hidden;
|
| 353 |
+
position: relative;
|
| 354 |
+
}
|
| 355 |
+
.seed-card::before {
|
| 356 |
+
content: ""; position: absolute;
|
| 357 |
+
top: 0; left: 0; right: 0; height: 3px;
|
| 358 |
+
background: linear-gradient(90deg, var(--primary), var(--accent));
|
| 359 |
+
opacity: 0; transition: opacity 0.2s;
|
| 360 |
+
}
|
| 361 |
+
.seed-card:hover {
|
| 362 |
+
box-shadow: var(--shadow-md);
|
| 363 |
+
}
|
| 364 |
+
.seed-card:hover::before { opacity: 1; }
|
| 365 |
+
|
| 366 |
+
.seed-head {
|
| 367 |
+
display: flex; align-items: flex-start; gap: 18px;
|
| 368 |
+
margin-bottom: 16px;
|
| 369 |
+
}
|
| 370 |
+
.seed-rank-block { min-width: 54px; }
|
| 371 |
+
.seed-rank-label {
|
| 372 |
+
font-size: 10px; color: var(--text-muted);
|
| 373 |
+
text-transform: uppercase; font-weight: 700;
|
| 374 |
+
letter-spacing: 0.06em; display: block; margin-bottom: 2px;
|
| 375 |
+
}
|
| 376 |
+
.seed-rank {
|
| 377 |
+
font-size: 32px; font-weight: 900;
|
| 378 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 379 |
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
| 380 |
+
font-variant-numeric: tabular-nums;
|
| 381 |
+
letter-spacing: -0.04em; line-height: 1;
|
| 382 |
+
}
|
| 383 |
+
.seed-info { flex: 1; min-width: 0; }
|
| 384 |
+
.seed-id {
|
| 385 |
+
font-family: "JetBrains Mono", monospace;
|
| 386 |
+
font-size: 11.5px; color: var(--text-muted);
|
| 387 |
+
background: var(--surface-2); padding: 4px 9px;
|
| 388 |
+
border-radius: 5px; display: inline-block;
|
| 389 |
+
margin-bottom: 9px;
|
| 390 |
+
}
|
| 391 |
+
.seed-rules { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 392 |
+
.rule-pill {
|
| 393 |
+
font-family: "JetBrains Mono", monospace;
|
| 394 |
+
font-size: 10.5px; font-weight: 600;
|
| 395 |
+
padding: 4px 9px; border-radius: 5px;
|
| 396 |
+
background: var(--accent-light); color: #7e22ce;
|
| 397 |
+
border: 1px solid rgba(168, 85, 247, 0.28);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.final-gauge {
|
| 401 |
+
width: 86px; height: 86px;
|
| 402 |
+
position: relative; flex-shrink: 0;
|
| 403 |
+
}
|
| 404 |
+
.final-gauge svg { transform: rotate(-90deg); width: 100%; height: 100%; }
|
| 405 |
+
.final-gauge-track { fill: none; stroke: var(--surface-2); stroke-width: 8; }
|
| 406 |
+
.final-gauge-fill {
|
| 407 |
+
fill: none; stroke-width: 8; stroke-linecap: round;
|
| 408 |
+
transition: stroke-dashoffset 0.8s ease-out;
|
| 409 |
+
}
|
| 410 |
+
.final-gauge-text {
|
| 411 |
+
position: absolute; inset: 0;
|
| 412 |
+
display: grid; place-items: center; text-align: center;
|
| 413 |
+
}
|
| 414 |
+
.final-gauge-value {
|
| 415 |
+
font-size: 19px; font-weight: 800;
|
| 416 |
+
color: var(--text); line-height: 1;
|
| 417 |
+
font-variant-numeric: tabular-nums;
|
| 418 |
+
}
|
| 419 |
+
.final-gauge-label {
|
| 420 |
+
font-size: 9px; font-weight: 700;
|
| 421 |
+
color: var(--text-muted);
|
| 422 |
+
text-transform: uppercase; letter-spacing: 0.08em;
|
| 423 |
+
margin-top: 3px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.seed-concept {
|
| 427 |
+
font-size: 14.5px; line-height: 1.75;
|
| 428 |
+
color: var(--text);
|
| 429 |
+
padding: 14px 16px;
|
| 430 |
+
background: linear-gradient(180deg, var(--surface) 0%, #ffffff 100%);
|
| 431 |
+
border-left: 3px solid var(--primary);
|
| 432 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
| 433 |
+
margin-bottom: 18px;
|
| 434 |
+
word-break: keep-all;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.seed-section-title {
|
| 438 |
+
font-size: 10.5px; font-weight: 700;
|
| 439 |
+
color: var(--text-muted);
|
| 440 |
+
text-transform: uppercase; letter-spacing: 0.08em;
|
| 441 |
+
margin: 18px 0 10px;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
/* ββ TIMELINE ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 445 |
+
.timeline {
|
| 446 |
+
position: relative;
|
| 447 |
+
background: var(--surface);
|
| 448 |
+
border-radius: var(--radius-sm);
|
| 449 |
+
padding: 12px;
|
| 450 |
+
}
|
| 451 |
+
.timeline-bar {
|
| 452 |
+
height: 42px; display: flex;
|
| 453 |
+
border-radius: 8px; overflow: hidden;
|
| 454 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
|
| 455 |
+
}
|
| 456 |
+
.beat-seg {
|
| 457 |
+
position: relative;
|
| 458 |
+
display: flex; flex-direction: column; justify-content: center;
|
| 459 |
+
padding: 0 10px; overflow: hidden; white-space: nowrap;
|
| 460 |
+
transition: filter 0.15s;
|
| 461 |
+
cursor: default;
|
| 462 |
+
}
|
| 463 |
+
.beat-seg:hover { filter: brightness(0.94); }
|
| 464 |
+
.beat-seg-name {
|
| 465 |
+
font-size: 10px; font-weight: 700;
|
| 466 |
+
text-transform: uppercase; letter-spacing: 0.04em;
|
| 467 |
+
color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.18);
|
| 468 |
+
overflow: hidden; text-overflow: ellipsis;
|
| 469 |
+
}
|
| 470 |
+
.beat-seg-time {
|
| 471 |
+
font-size: 9.5px;
|
| 472 |
+
color: rgba(255, 255, 255, 0.88);
|
| 473 |
+
font-family: "JetBrains Mono", monospace;
|
| 474 |
+
}
|
| 475 |
+
.timeline-scale {
|
| 476 |
+
margin-top: 8px; position: relative; height: 20px;
|
| 477 |
+
}
|
| 478 |
+
.timeline-tick {
|
| 479 |
+
position: absolute; top: 0;
|
| 480 |
+
transform: translateX(-50%);
|
| 481 |
+
font-size: 10px; color: var(--text-muted);
|
| 482 |
+
font-family: "JetBrains Mono", monospace;
|
| 483 |
+
font-weight: 600;
|
| 484 |
+
}
|
| 485 |
+
.timeline-tick::before {
|
| 486 |
+
content: ""; display: block;
|
| 487 |
+
width: 1px; height: 5px; background: var(--border-strong);
|
| 488 |
+
margin: 0 auto 3px;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.beats-detail {
|
| 492 |
+
margin-top: 14px;
|
| 493 |
+
display: flex; flex-direction: column; gap: 2px;
|
| 494 |
+
}
|
| 495 |
+
.beat-row {
|
| 496 |
+
display: grid; grid-template-columns: 72px 130px 1fr;
|
| 497 |
+
gap: 12px; padding: 8px 0;
|
| 498 |
+
border-bottom: 1px dashed var(--border);
|
| 499 |
+
align-items: start; font-size: 12.5px;
|
| 500 |
+
}
|
| 501 |
+
.beat-row:last-child { border-bottom: 0; }
|
| 502 |
+
.beat-row-time {
|
| 503 |
+
font-family: "JetBrains Mono", monospace;
|
| 504 |
+
color: var(--text-muted); font-weight: 600;
|
| 505 |
+
font-size: 11.5px;
|
| 506 |
+
}
|
| 507 |
+
.beat-row-name {
|
| 508 |
+
font-weight: 700; color: var(--primary);
|
| 509 |
+
font-size: 11px;
|
| 510 |
+
text-transform: uppercase; letter-spacing: 0.03em;
|
| 511 |
+
}
|
| 512 |
+
.beat-row-content { color: var(--text); line-height: 1.6; }
|
| 513 |
+
|
| 514 |
+
/* ββ SCORES ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 515 |
+
.scores-section {
|
| 516 |
+
display: grid; grid-template-columns: 220px 1fr;
|
| 517 |
+
gap: 22px; margin-top: 16px; padding-top: 18px;
|
| 518 |
+
border-top: 1px solid var(--border);
|
| 519 |
+
}
|
| 520 |
+
.radar-wrap { position: relative; height: 220px; }
|
| 521 |
+
.scores-table {
|
| 522 |
+
display: grid; grid-template-columns: 1fr 1fr;
|
| 523 |
+
gap: 8px; align-content: start;
|
| 524 |
+
}
|
| 525 |
+
.score-item {
|
| 526 |
+
padding: 9px 12px; background: var(--surface); border-radius: 7px;
|
| 527 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 528 |
+
}
|
| 529 |
+
.score-item-label {
|
| 530 |
+
font-size: 10.5px; font-weight: 700;
|
| 531 |
+
color: var(--text-muted);
|
| 532 |
+
text-transform: uppercase; letter-spacing: 0.04em;
|
| 533 |
+
}
|
| 534 |
+
.score-item-value {
|
| 535 |
+
font-family: "JetBrains Mono", monospace;
|
| 536 |
+
font-size: 13.5px; font-weight: 700;
|
| 537 |
+
color: var(--text);
|
| 538 |
+
font-variant-numeric: tabular-nums;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
/* Beat color palette (6 beats, indigo β rose) */
|
| 542 |
+
.beat-color-0 { background: #6366f1; }
|
| 543 |
+
.beat-color-1 { background: #8b5cf6; }
|
| 544 |
+
.beat-color-2 { background: #a855f7; }
|
| 545 |
+
.beat-color-3 { background: #d946ef; }
|
| 546 |
+
.beat-color-4 { background: #ec4899; }
|
| 547 |
+
.beat-color-5 { background: #f43f5e; }
|
| 548 |
+
|
| 549 |
+
/* ββ FOOTER ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 550 |
+
.footer {
|
| 551 |
+
border-top: 1px solid var(--border);
|
| 552 |
+
padding-top: 28px; padding-bottom: 44px;
|
| 553 |
+
display: flex; flex-direction: column; gap: 14px;
|
| 554 |
+
}
|
| 555 |
+
.foot-details {
|
| 556 |
+
border: 1px solid var(--border);
|
| 557 |
+
border-radius: var(--radius-sm);
|
| 558 |
+
padding: 14px 18px;
|
| 559 |
+
background: var(--surface);
|
| 560 |
+
}
|
| 561 |
+
.foot-details summary {
|
| 562 |
+
cursor: pointer; font-weight: 700;
|
| 563 |
+
color: var(--text); font-size: 14px;
|
| 564 |
+
}
|
| 565 |
+
.foot-details[open] summary { margin-bottom: 14px; }
|
| 566 |
+
.foot-body ul {
|
| 567 |
+
list-style: none;
|
| 568 |
+
display: flex; flex-direction: column; gap: 10px;
|
| 569 |
+
font-size: 13px; color: var(--text-muted);
|
| 570 |
+
padding: 4px 0;
|
| 571 |
+
}
|
| 572 |
+
.foot-body li strong { color: var(--text); font-weight: 700; }
|
| 573 |
+
|
| 574 |
+
.rules-table { display: grid; gap: 2px; font-size: 12px; }
|
| 575 |
+
.rule-row {
|
| 576 |
+
display: grid;
|
| 577 |
+
grid-template-columns: 230px 1.2fr 1.6fr 200px;
|
| 578 |
+
padding: 9px 8px;
|
| 579 |
+
border-bottom: 1px dashed var(--border);
|
| 580 |
+
gap: 12px; align-items: start;
|
| 581 |
+
}
|
| 582 |
+
.rule-row:last-child { border-bottom: 0; }
|
| 583 |
+
.rule-row.header {
|
| 584 |
+
font-weight: 700; color: var(--text-muted);
|
| 585 |
+
text-transform: uppercase; font-size: 10px;
|
| 586 |
+
letter-spacing: 0.06em;
|
| 587 |
+
border-bottom: 1px solid var(--border-strong);
|
| 588 |
+
}
|
| 589 |
+
.rule-id {
|
| 590 |
+
font-family: "JetBrains Mono", monospace;
|
| 591 |
+
font-size: 11px; font-weight: 700;
|
| 592 |
+
color: var(--primary);
|
| 593 |
+
}
|
| 594 |
+
.rule-anchor {
|
| 595 |
+
font-size: 10.5px; color: var(--text-subtle);
|
| 596 |
+
font-family: "JetBrains Mono", monospace;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.foot-meta {
|
| 600 |
+
font-size: 12px; color: var(--text-subtle);
|
| 601 |
+
text-align: center; padding: 14px 0;
|
| 602 |
+
}
|
| 603 |
+
.foot-meta a { color: var(--primary); text-decoration: none; font-weight: 600; }
|
| 604 |
+
|
| 605 |
+
/* ββ RESPONSIVE ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 606 |
+
@media (max-width: 820px) {
|
| 607 |
+
.summary { grid-template-columns: repeat(2, 1fr); }
|
| 608 |
+
.scores-section { grid-template-columns: 1fr; }
|
| 609 |
+
.rule-row { grid-template-columns: 1fr; gap: 4px; }
|
| 610 |
+
.hero-title { font-size: 30px; }
|
| 611 |
+
.beats-detail .beat-row { grid-template-columns: 70px 1fr; }
|
| 612 |
+
.beat-row-name { grid-column: 2; }
|
| 613 |
+
.beat-row-content { grid-column: 1 / 3; padding-top: 2px; }
|
| 614 |
+
}
|