Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions .modalignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
__pycache__/
**/__pycache__/
*.pyc
.pytest_cache/
.venv/
.git/
data/
*.db
.gradio/
traces/
node_modules/
assets/verify/
.git
.venv
node_modules
data
assets/verify
__pycache__
3 changes: 2 additions & 1 deletion ai/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ def generate_entity(
legendary_context=legendary_block,
)

for attempt, temp in enumerate([0.8, 0.5]):
for attempt, temp in enumerate([0.75, 0.5]):
raw = ""
t0 = time.perf_counter()
try:
raw = generate(
prompts.ENTITY_GENERATION_SYSTEM,
user_prompt,
temperature=temp,
max_new_tokens=700,
)
data = extract_json(raw)
if data.get("suggested_location") in LOCATION_ALIASES:
Expand Down
82 changes: 61 additions & 21 deletions ai/modal_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,32 @@ def generate(
if USE_MOCK or not MODAL_URL:
return _mock_generate(system_prompt, user_prompt)

try:
response = requests.post(
MODAL_URL,
json={
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"max_new_tokens": max_new_tokens,
"temperature": temperature,
},
timeout=120,
)
response.raise_for_status()
data = response.json()
return data.get("text", data.get("result", ""))
except Exception as e:
if USE_MOCK:
return _mock_generate(system_prompt, user_prompt)
raise RuntimeError(f"Modal inference failed: {e}") from e
payload = {
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"max_new_tokens": max_new_tokens,
"temperature": temperature,
}
last_error: Exception | None = None
for attempt in range(2):
try:
response = requests.post(
MODAL_URL,
json=payload,
timeout=(15, 90),
)
response.raise_for_status()
data = response.json()
return data.get("text", data.get("result", ""))
except Exception as e:
last_error = e
if attempt == 0:
import time
time.sleep(2)
continue
if USE_MOCK:
return _mock_generate(system_prompt, user_prompt)
raise RuntimeError(f"Modal inference failed: {last_error}") from last_error


def extract_json(text: str) -> dict:
Expand All @@ -51,12 +59,44 @@ def extract_json(text: str) -> dict:
try:
return json.loads(text)
except json.JSONDecodeError:
match = re.search(r"\{[\s\S]*\}", text)
if match:
return json.loads(match.group())
candidate = _first_balanced_json_object(text)
if candidate:
return json.loads(candidate)
raise ValueError(f"Could not parse JSON from response: {text[:200]}...")


def _first_balanced_json_object(text: str) -> str | None:
"""Return the first complete top-level JSON object embedded in text."""
start = text.find("{")
if start == -1:
return None

depth = 0
in_string = False
escape = False

for i, ch in enumerate(text[start:], start=start):
if in_string:
if escape:
escape = False
elif ch == "\\":
escape = True
elif ch == '"':
in_string = False
continue

if ch == '"':
in_string = True
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return text[start : i + 1]

return None


def _mock_generate(system_prompt: str, user_prompt: str) -> str:
"""Deterministic mock for local development without Modal."""
if "summoned something into the Realm" in user_prompt:
Expand Down
4 changes: 2 additions & 2 deletions ai/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"name": "Poetic true name. 2–4 words. Original — not the user's sentence rephrased.",
"display_name": "What most people call them. Can match or differ from true name.",
"type": "exactly one of: character | creature | object | place",
"appearance": "3–5 sentences. Highly visual. Include at least one impossible physical detail. No clichés.",
"backstory": "2–3 sentences. Where they came from, what shaped them. Something specific happened to them. Not a destiny — a specific event.",
"appearance": "2–3 sentences. Highly visual. One impossible detail.",
"backstory": "2 sentences. A specific event — not destiny.",
"personality_traits": ["trait1", "trait2", "trait3", "trait4"],
"primary_goal": "What they are actively pursuing right now. Concrete and specific. Not abstract.",
"secondary_goal": "What they secretly want but will not admit, even to themselves.",
Expand Down
118 changes: 110 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,8 @@ def build_app() -> gr.Blocks:
js="""
() => {
setTimeout(() => {
const card = document.querySelector('.soul-card-image');
const target = card || document.querySelector('.tome-summon-stage .entity-card');
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
const stage = document.querySelector('.tome-summon-stage');
if (stage) stage.scrollTo({ top: 0, behavior: 'smooth' });
}, 350);
}
""",
Expand All @@ -1073,9 +1072,8 @@ def build_app() -> gr.Blocks:
js="""
() => {
setTimeout(() => {
const card = document.querySelector('.soul-card-image');
const target = card || document.querySelector('.tome-summon-stage .entity-card');
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
const stage = document.querySelector('.tome-summon-stage');
if (stage) stage.scrollTo({ top: 0, behavior: 'smooth' });
}, 350);
}
""",
Expand Down Expand Up @@ -1277,6 +1275,14 @@ def _book_day_select_change(day_val, entry_type, search):
window._aetherInitBound = true;

const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const MOBILE_QUERY = window.matchMedia('(max-width: 920px)');
const isMobileSpread = () => MOBILE_QUERY.matches;
const syncMobileClass = () => {
const mobile = isMobileSpread();
document.documentElement.classList.toggle('tome-mobile', mobile);
document.body.classList.toggle('tome-mobile', mobile);
};
syncMobileClass();

const sections = [
{ key: 'realm', label: 'The Realm', kind: 'tab', panel: 0 },
Expand All @@ -1301,23 +1307,109 @@ def _book_day_select_change(day_val, entry_type, search):
};

const setActivePanel = (panelIndex) => {
const mobile = isMobileSpread();
const panels = Array.from(document.querySelectorAll('.realm-tabs [role="tabpanel"]'));
panels.forEach((panel, i) => {
const active = i === panelIndex;
panel.style.setProperty('display', active ? 'flex' : 'none', 'important');
panel.style.setProperty('flex-direction', 'column', 'important');
panel.style.setProperty('height', active ? '565px' : '0', 'important');
panel.style.setProperty('max-height', active ? '565px' : '0', 'important');
if (mobile) {
panel.style.setProperty('height', active ? 'auto' : '0', 'important');
panel.style.setProperty('max-height', active ? 'none' : '0', 'important');
panel.style.setProperty('flex', active ? '1 1 auto' : '0 0 0', 'important');
panel.style.setProperty('min-height', active ? '0' : '0', 'important');
} else {
panel.style.setProperty('height', active ? '565px' : '0', 'important');
panel.style.setProperty('max-height', active ? '565px' : '0', 'important');
panel.style.setProperty('flex', '', 'important');
}
panel.style.setProperty('overflow', 'hidden', 'important');
panel.setAttribute('aria-hidden', active ? 'false' : 'true');
});
wirePageExpand();
wireMobileSpreads();
const pad = document.getElementById('aether-walk-pad');
if (pad && panelIndex !== 1) {
pad.classList.remove('visible', 'explore-only');
}
};

const resetActiveSpreadLeaf = () => {
const panels = document.querySelectorAll('.realm-tabs [role="tabpanel"]');
const activePanel = panels[currentSpread];
if (!activePanel) return;
const row = activePanel.querySelector('.tome-spread-row');
if (!row) return;
row.classList.remove('tome-leaf-right');
row.classList.add('tome-leaf-left');
};

const wireMobileSpreads = () => {
const mobile = isMobileSpread();
syncMobileClass();

document.querySelectorAll('.tome-spread-shell').forEach((shell) => {
const row = shell.querySelector('.tome-spread-row');
if (!row) return;

if (!mobile) {
row.classList.remove('tome-leaf-left', 'tome-leaf-right');
shell.querySelector('.tome-leaf-bar')?.remove();
return;
}

if (!row.classList.contains('tome-leaf-left') && !row.classList.contains('tome-leaf-right')) {
row.classList.add('tome-leaf-left');
}

let bar = shell.querySelector('.tome-leaf-bar');
if (!bar) {
bar = document.createElement('div');
bar.className = 'tome-leaf-bar';
bar.innerHTML = `
<button type="button" class="tome-leaf-btn" data-leaf="left" aria-label="Show left leaf">‹ Leaf</button>
<span class="tome-leaf-dots" aria-hidden="true"><i></i><i></i></span>
<button type="button" class="tome-leaf-btn" data-leaf="right" aria-label="Show right leaf">Leaf ›</button>
`;
shell.appendChild(bar);

const syncLeafBar = () => {
const onLeft = row.classList.contains('tome-leaf-left');
bar.querySelectorAll('.tome-leaf-btn').forEach((btn) => {
const active = btn.dataset.leaf === (onLeft ? 'left' : 'right');
btn.classList.toggle('is-active', active);
});
bar.querySelectorAll('.tome-leaf-dots i').forEach((dot, idx) => {
dot.classList.toggle('active', onLeft ? idx === 0 : idx === 1);
});
};

bar.querySelectorAll('.tome-leaf-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const leaf = btn.dataset.leaf;
row.classList.remove('tome-leaf-left', 'tome-leaf-right');
row.classList.add(leaf === 'right' ? 'tome-leaf-right' : 'tome-leaf-left');
syncLeafBar();
});
});
bar._syncLeafBar = syncLeafBar;
}

bar._syncLeafBar?.();
});
};

if (!window._aetherMobileBound) {
window._aetherMobileBound = true;
MOBILE_QUERY.addEventListener('change', () => {
wireMobileSpreads();
setActivePanel(currentSpread >= 0 ? sections[currentSpread].panel : 0);
});
window.addEventListener('resize', () => {
wireMobileSpreads();
}, { passive: true });
}

const ensureAmbientMelody = () => {
if (window._aetherAmbientStarted) {
window._aetherAmbientCtx?.resume?.();
Expand Down Expand Up @@ -1535,6 +1627,8 @@ def _book_day_select_change(day_val, entry_type, search):
applyTurnFx(direction);

setActivePanel(spread.panel);
resetActiveSpreadLeaf();
wireMobileSpreads();

currentSpread = clamped;
window._aetherCurrentSpread = spread.key;
Expand Down Expand Up @@ -1887,6 +1981,7 @@ def _book_day_select_change(day_val, entry_type, search):

wireOpening();
wireTomeNav();
wireMobileSpreads();
wirePageExpand();

const gateOnLoad = document.getElementById('realm-opening');
Expand Down Expand Up @@ -2053,6 +2148,13 @@ def _book_day_select_change(day_val, entry_type, search):
el.classList.toggle('is-selected', el.getAttribute('data-location-id') === id);
});
fireHiddenInput('explore_place_pick', id);
if (window.matchMedia('(max-width: 920px)').matches) {
requestAnimationFrame(() => {
const stage = document.querySelector('.tome-explore-stage .diorama-frame')
|| document.querySelector('.tome-explore-stage');
stage?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
}
});
});
document.querySelectorAll('.day-tab').forEach((btn) => {
Expand Down
Loading
Loading