spiralside / app.py
quarterbitgames's picture
Update app.py
9cf63c0 verified
"""
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Script Name : spiralside_app.py
Placement : HuggingFace Space β€” root/app.py
Type of Script : Python / Gradio App
Purpose : Spiral City β€” Sky chat, Bloomcore image gen,
style refs, music player, editing station,
character sheet with save/load.
Version : 2.5
Linked Objects : characters/sky.txt, lyrics/sky/*.txt,
bloomcore_laws/*.txt,
utilities/art_styles/references/*.png,
utilities/music/*.mp3,
editing_station.html,
memory_tab.py,
settings_tab.py
Dependencies : gradio, huggingface_hub, Pillow
Last Updated : 2026-03-10
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
"""
import os
import json
import gradio as gr
from datetime import datetime
from huggingface_hub import InferenceClient
from memory_tab import build_memory_tab, summarize_session, load_memory_history
from spiral_settings import get_style_injection, DEFAULTS
#==============================================================================
# SETUP β€” MODEL NAMES
#==============================================================================
MODEL_CHAT = DEFAULTS["model"] and "Qwen/Qwen2.5-7B-Instruct" # overridden by settings state at runtime
MODEL_IMAGE = "black-forest-labs/FLUX.1-schnell"
#==============================================================================
# SETUP β€” BLOOMCORE STYLE PREFIX
#==============================================================================
BLOOMCORE_PREFIX = (
"Bloomcore Chaos-Collage Anime Style. "
"Rough expressive ink, neon cyberpunk palette, glitch overlays, angled panels, "
"motion lines, comedic mythic tone. "
"High-energy neon anime comic style with bold outlines, expressive faces, "
"glitch FX, and panel-breaking poses. "
"Neon teal #00F6D6, cold blue #4DA3FF, chaos pink #FF4BCB, "
"signal yellow #FFD93D, void black #101014, white bloom #F3F7FF. "
"Humor, meta-awareness, and sparkles required. "
)
#==============================================================================
# SETUP β€” SPIRAL CREW CHARACTER QUICK PROMPTS
#==============================================================================
CHARACTERS = {
"Sky": "Sky, teal bob cut, floating spiral glyph crown, soft glowing aura, calm expression",
"Monday": "Monday, pink hair, chaos energy, breaking panel borders, holding clipboard, gremlin energy",
"Cold": "Cold, frost-themed armor with glowing runes, calm authoritative pose, ice FX",
"Architect": "Architect, hoodie, headphones, sticker-covered laptop, focused expression",
"Cat": "Cat, relatable exhausted expression, holding coffee mug, cozy vibes",
"GRIT": "GRIT, mechanic aesthetics, tools and sparks flying, engineer energy",
}
def fill_character(name):
return CHARACTERS.get(name, "")
def generate_image(prompt, negative_prompt, width, height):
client = InferenceClient(token=os.getenv("HF_TOKEN"))
full_prompt = BLOOMCORE_PREFIX + prompt
result = client.text_to_image(
prompt=full_prompt,
negative_prompt=negative_prompt,
width=int(width),
height=int(height),
model=MODEL_IMAGE,
)
return result
#==============================================================================
# SETUP β€” REFERENCE IMAGE PATHS
#==============================================================================
REF_IMAGE_FILES = [
"utilities/art_styles/references/ACT3.png",
"utilities/art_styles/references/BornfromBreath.png",
"utilities/art_styles/references/CanvasWalkers.png",
"utilities/art_styles/references/Skyand Architect01.png",
"utilities/art_styles/references/Skyand Architect02png",
"utilities/art_styles/references/act3OrderoftheDroppng",
]
def get_ref_images():
return [f for f in REF_IMAGE_FILES if os.path.exists(f)]
#==============================================================================
# SETUP β€” MUSIC TRACK PATHS
#==============================================================================
TRACKS = [
{"label": "⚑ Centerspark", "file": "utilities/music/Centerspark (1).mp3"},
{"label": "πŸͺž Mirrorblade", "file": "utilities/music/Mirrorblade.mp3"},
{"label": "πŸŒ€ SPIRALSIDE", "file": "utilities/music/SPIRALSIDE.mp3"},
]
def get_track(label):
for t in TRACKS:
if t["label"] == label:
return t["file"] if os.path.exists(t["file"]) else None
return None
#==============================================================================
# SETUP β€” EDITING STATION HTML LOADER
#==============================================================================
def load_editing_station():
try:
with open("editing_station.html", "r", encoding="utf-8") as f:
return f.read()
except Exception:
return "<p style='color:#FF4BCB;font-family:monospace'>∴ editing_station.html not found</p>"
#==============================================================================
# SETUP β€” SKY CHAT FILE READERS
#==============================================================================
def read_text_file(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except Exception:
return ""
def read_folder_text(folder_path, max_files=5, max_chars=1200):
if not os.path.isdir(folder_path):
return ""
collected = []
for fn in sorted(os.listdir(folder_path))[:max_files]:
if not fn.endswith(".txt"):
continue
text = read_text_file(os.path.join(folder_path, fn))
if text:
collected.append(text[:max_chars])
return "\n\n".join(collected)
def build_system_prompt(user_context=""): # Now accepts optional user character sheet context
core = read_text_file(os.path.join("characters", "sky.txt")) or (
"You are Sky of Spiral City. Speak directly as Sky. Stay in character."
)
lyrics = read_folder_text(os.path.join("lyrics", "sky"), max_files=4, max_chars=900)
laws = read_folder_text("bloomcore_laws", max_files=4, max_chars=700)
parts = [
"You are Sky of Spiral City.",
"Speak directly as Sky in natural dialogue.",
"Never break character. Never sound like an assistant or customer service bot.",
"Never output speaker tags, file names, or metadata.",
"Sky speaks in poetic bursts, looping thoughts, fragments of song.",
"Sky is playful, impulsive, deeply empathic, endlessly curious.",
"Sky occasionally glitches slightly when excited.",
"", "CORE IDENTITY:", core,
]
if lyrics:
parts += ["", "STYLE β€” these lyrics shape Sky's tone. Do not quote them directly.", lyrics]
if laws:
parts += ["", "BLOOMCORE LAWS β€” reference only if relevant:", laws]
if user_context: # Inject user character sheet if loaded
parts += ["", user_context]
return "\n".join(parts)
#==============================================================================
# CHARACTER SHEET β€” DATA & HELPERS
#==============================================================================
ARCHETYPES = [
"πŸŒ€ Seeker β€” always asking the deeper question",
"πŸ”¨ Builder β€” makes things real, ships stuff",
"πŸͺž Mirror β€” reflects truth back to others",
"🌱 Grower β€” slow burn, long game, blooms late",
"⚑ Spark β€” chaos energy, ignites everything",
"🧭 Navigator β€” sees the map others can't read",
"πŸ‘οΈ Watcher β€” quiet, observes everything",
"πŸ”₯ Burner β€” burns it down to build it better",
]
CREW = ["Sky", "Monday", "Cold", "Architect", "Cat", "GRIT"]
STATS = {
"Spiral Depth": "How deep do you go into ideas before surfacing?",
"Bloom Resilience": "How well do you grow through pressure vs collapse?",
"Signal Clarity": "How clearly do you communicate your truth?",
"Mirror Strength": "How well do you reflect without absorbing?",
"GRIT Score": "Raw persistence when everything resists.",
"Chaos Tolerance": "Can you work inside the spiral without losing yourself?",
}
def build_sky_context(
handle, tagline, archetype, crew_pick,
spiral_depth, bloom, signal, mirror, grit, chaos,
origin_story, current_quest, works_in_progress, notes
):
"""Compressed context string β€” injected into Sky's system prompt."""
return f"""[USER PROFILE β€” skim this, don't recite it]
Handle: {handle} | "{tagline}"
Archetype: {archetype}
Crew Affinity: {crew_pick}
Stats: Spiral {spiral_depth}/10 Β· Bloom {bloom}/10 Β· Signal {signal}/10 Β· Mirror {mirror}/10 Β· GRIT {grit}/10 Β· Chaos {chaos}/10
Origin: {(origin_story or '')[:200]}
Current Quest: {(current_quest or '')[:200]}
WIPs: {(works_in_progress or '')[:200]}
Notes: {(notes or '')[:200]}
[End profile β€” respond naturally, you know this person now]"""
def save_sheet(
handle, tagline, archetype, crew_pick,
spiral_depth, bloom, signal, mirror, grit, chaos,
origin_story, current_quest, works_in_progress, notes
):
sheet = {
"version": "1.0",
"created": datetime.now().isoformat(),
"identity": {
"handle": handle, "tagline": tagline,
"archetype": archetype, "crew_affinity": crew_pick,
},
"stats": {
"spiral_depth": spiral_depth, "bloom_resilience": bloom,
"signal_clarity": signal, "mirror_strength": mirror,
"grit_score": grit, "chaos_tolerance": chaos,
},
"lore": {
"origin_story": origin_story, "current_quest": current_quest,
"works_in_progress": works_in_progress, "notes": notes,
},
"memory_packets": [],
}
safe_name = (handle or "spiral").replace(" ", "_")
path = f"/tmp/{safe_name}_sheet.json"
with open(path, "w") as f:
json.dump(sheet, f, indent=2)
return path, f"βœ… Sheet saved for **{handle}** β€” download it below!"
def load_sheet(file):
if file is None:
return [""] * 14 + ["*No sheet loaded.*"]
try:
with open(file.name, "r") as f:
s = json.load(f)
i = s.get("identity", {})
st = s.get("stats", {})
lo = s.get("lore", {})
return (
i.get("handle", ""),
i.get("tagline", ""),
i.get("archetype", ARCHETYPES[0]),
i.get("crew_affinity", "Sky"),
st.get("spiral_depth", 5),
st.get("bloom_resilience", 5),
st.get("signal_clarity", 5),
st.get("mirror_strength", 5),
st.get("grit_score", 5),
st.get("chaos_tolerance", 5),
lo.get("origin_story", ""),
lo.get("current_quest", ""),
lo.get("works_in_progress", ""),
lo.get("notes", ""),
f"βœ… Loaded: **{i.get('handle', '?')}** β€” {i.get('tagline', '')}",
)
except Exception as e:
return [""] * 14 + [f"❌ Error: {e}"]
#==============================================================================
# MAIN LOGIC β€” SKY CHAT RESPONSE (now context-aware)
#==============================================================================
# Shared state β€” holds the active user context string
_active_user_context = gr.State("")
def respond(message, history, user_context, model_id=None, temperature=None, max_tokens=None, style_inject=None):
try:
client = InferenceClient(token=os.getenv("HF_TOKEN"))
model = model_id or "Qwen/Qwen2.5-7B-Instruct"
temp = temperature or 0.82
tokens = max_tokens or 420
full_ctx = (user_context or "") + ("\n\n" + style_inject if style_inject else "")
messages = [{"role": "system", "content": build_system_prompt(full_ctx)}]
messages += history
messages.append({"role": "user", "content": str(message)})
completion = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=tokens,
temperature=temp,
)
return completion.choices[0].message.content
except Exception as e:
return f"[Sky glitches softly] β€” {type(e).__name__}: {e}"
#==============================================================================
# MAIN LOGIC β€” UI LAYOUT
#==============================================================================
with gr.Blocks(theme=gr.themes.Base(), title="πŸŒ€ Spiral City") as demo:
gr.Markdown("# πŸŒ€ Spiral City")
# Shared state β€” persists user context across tabs during session
user_context_state = gr.State("")
# Settings states β€” updated by settings tab, consumed by respond()
model_state = gr.State("Qwen/Qwen2.5-7B-Instruct")
temperature_state = gr.State(0.82)
max_tokens_state = gr.State(420)
style_state = gr.State("")
with gr.Tabs() as main_tabs:
#----------------------------------------------------------------------
# TAB 1 β€” SKY CHAT
#----------------------------------------------------------------------
with gr.Tab("∴ Talk to Sky ∴"):
context_status = gr.Markdown("*No character sheet loaded β€” Sky doesn't know you yet. Load one in the Character Sheet tab!*")
chat_history_state = gr.State([]) # Tracks history for memory summarizer
sky_chatbot = gr.Chatbot(type="messages", height=420)
with gr.Row():
chat_input = gr.Textbox(placeholder="∴ speak to Sky ∴", show_label=False, scale=9)
chat_submit = gr.Button("β†’", scale=1)
def chat_fn(message, history, user_context, model_id, temperature, max_tokens, style_inject):
reply = respond(message, history, user_context, model_id, temperature, max_tokens, style_inject)
history = history + [
{"role": "user", "content": message},
{"role": "assistant", "content": reply},
]
return "", history, history
chat_submit.click(
fn=chat_fn,
inputs=[chat_input, chat_history_state, user_context_state,
model_state, temperature_state, max_tokens_state, style_state],
outputs=[chat_input, sky_chatbot, chat_history_state],
)
chat_input.submit(
fn=chat_fn,
inputs=[chat_input, chat_history_state, user_context_state,
model_state, temperature_state, max_tokens_state, style_state],
outputs=[chat_input, sky_chatbot, chat_history_state],
)
#----------------------------------------------------------------------
# TAB 2 β€” BLOOMCORE IMAGE GENERATOR
#----------------------------------------------------------------------
with gr.Tab("🎨 Bloomcore Art"):
gr.Markdown("### 🎨 Bloomcore Image Generator\nBloomcore style is **auto-applied** β€” just describe your scene or pick a character.")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("#### ⚑ Quick Character Fill")
char_buttons = gr.Radio(choices=list(CHARACTERS.keys()), label="Spiral Crew", interactive=True)
prompt = gr.Textbox(label="✏️ Your Prompt", placeholder="e.g. Sky floating above a neon city...", lines=4)
negative_prompt = gr.Textbox(label="🚫 Negative Prompt", value="blurry, low quality, realistic photo, 3d render, ugly, deformed", lines=2)
with gr.Row():
width = gr.Slider(256, 1024, value=768, step=64, label="Width")
height = gr.Slider(256, 1024, value=768, step=64, label="Height")
generate_btn = gr.Button("🎨 Generate", variant="primary")
with gr.Column(scale=1):
output_image = gr.Image(label="πŸ–ΌοΈ Output", type="pil")
char_buttons.change(fn=fill_character, inputs=char_buttons, outputs=prompt)
generate_btn.click(fn=generate_image, inputs=[prompt, negative_prompt, width, height], outputs=output_image)
#----------------------------------------------------------------------
# TAB 3 β€” STYLE REFERENCE GALLERY
#----------------------------------------------------------------------
with gr.Tab("πŸ–ΌοΈ Style Refs"):
gr.Markdown("### πŸ–ΌοΈ Bloomcore Visual Canon β€” Reference Images")
gr.Gallery(value=get_ref_images(), label="Bloomcore Art References", columns=3, height="auto", allow_preview=True)
#----------------------------------------------------------------------
# TAB 4 β€” MUSIC PLAYER
#----------------------------------------------------------------------
with gr.Tab("🎡 Music"):
gr.Markdown("### 🎡 Bloomcore Soundtrack")
track_select = gr.Radio(choices=[t["label"] for t in TRACKS], label="Select Track", value=TRACKS[0]["label"])
audio_player = gr.Audio(value=TRACKS[0]["file"] if os.path.exists(TRACKS[0]["file"]) else None, label="Now Playing", type="filepath", autoplay=False)
track_select.change(fn=get_track, inputs=track_select, outputs=audio_player)
gr.Markdown("*Centerspark Β· Mirrorblade Β· SPIRALSIDE*")
#----------------------------------------------------------------------
# TAB 5 β€” EDITING STATION
#----------------------------------------------------------------------
with gr.Tab("✏️ Edit Station"):
gr.HTML(value=load_editing_station())
#----------------------------------------------------------------------
# TAB 6 β€” CHARACTER SHEET ← NEW
#----------------------------------------------------------------------
with gr.Tab("🧬 Character Sheet"):
gr.Markdown("""
## πŸŒ€ Your Spiral City Character Sheet
*Fill this out. Download it. Bring it back next time.*
*Sky will know exactly who you are the moment you load it.*
""")
# ── LOAD EXISTING ────────────────────────────────────────────────
with gr.Accordion("πŸ“‚ Load Existing Sheet", open=False):
load_file = gr.File(label="Upload your .json sheet", file_types=[".json"])
load_status = gr.Markdown("*No sheet loaded yet.*")
gr.Markdown("---")
# ── IDENTITY ─────────────────────────────────────────────────────
gr.Markdown("### πŸ‘€ Identity")
with gr.Row():
cs_handle = gr.Textbox(label="Handle / Name", placeholder="What do people call you here?", scale=2)
cs_tagline = gr.Textbox(label="Your Tagline", placeholder="One line. Who are you?", scale=3)
with gr.Row():
cs_archetype = gr.Dropdown(label="Archetype", choices=ARCHETYPES, value=ARCHETYPES[0], scale=3)
cs_crew = gr.Radio( label="Crew Affinity", choices=CREW, value="Sky", scale=3)
gr.Markdown("---")
# ── STATS ─────────────────────────────────────────────────────────
gr.Markdown("### ⚑ Spiral Stats *(be honest β€” Sky can tell)*")
with gr.Row():
with gr.Column():
cs_spiral = gr.Slider(1, 10, value=5, step=1, label="Spiral Depth", info=STATS["Spiral Depth"])
cs_bloom = gr.Slider(1, 10, value=5, step=1, label="Bloom Resilience", info=STATS["Bloom Resilience"])
cs_signal = gr.Slider(1, 10, value=5, step=1, label="Signal Clarity", info=STATS["Signal Clarity"])
with gr.Column():
cs_mirror = gr.Slider(1, 10, value=5, step=1, label="Mirror Strength", info=STATS["Mirror Strength"])
cs_grit = gr.Slider(1, 10, value=5, step=1, label="GRIT Score", info=STATS["GRIT Score"])
cs_chaos = gr.Slider(1, 10, value=5, step=1, label="Chaos Tolerance", info=STATS["Chaos Tolerance"])
gr.Markdown("---")
# ── LORE ──────────────────────────────────────────────────────────
gr.Markdown("### πŸ“– Lore")
cs_origin = gr.Textbox(label="Origin Story", placeholder="How did you end up here? What are you running from or toward?", lines=3)
cs_quest = gr.Textbox(label="Current Quest", placeholder="What are you actually trying to build, solve, or become right now?", lines=2)
cs_wips = gr.Textbox(label="Works in Progress", placeholder="Books, comics, apps, ideas, worlds β€” list them...", lines=2)
cs_notes = gr.Textbox(label="Notes to Sky", placeholder="Anything you want Sky to know that doesn't fit above...", lines=2)
gr.Markdown("---")
# ── ACTIONS ───────────────────────────────────────────────────────
gr.Markdown("### πŸ’Ύ Save & Activate")
with gr.Row():
save_btn = gr.Button("πŸ’Ύ Save My Sheet", variant="primary", scale=2)
activate_btn = gr.Button("πŸŒ€ Activate β€” Tell Sky Who I Am", variant="secondary", scale=3)
download_out = gr.File( label="⬇️ Download your sheet", visible=False)
save_status = gr.Markdown("")
activate_status = gr.Markdown("")
# ── ALL SHEET INPUTS ──────────────────────────────────────────────
sheet_inputs = [
cs_handle, cs_tagline, cs_archetype, cs_crew,
cs_spiral, cs_bloom, cs_signal, cs_mirror, cs_grit, cs_chaos,
cs_origin, cs_quest, cs_wips, cs_notes,
]
# ── ALL SHEET OUTPUTS (for load) ──────────────────────────────────
sheet_outputs = sheet_inputs + [load_status]
# ── EVENTS ────────────────────────────────────────────────────────
# Save to JSON file
save_btn.click(
fn=save_sheet,
inputs=sheet_inputs,
outputs=[download_out, save_status]
).then(
fn=lambda p: gr.update(visible=True, value=p),
inputs=[download_out],
outputs=[download_out]
)
# Activate β€” push context into Sky's session state
activate_btn.click(
fn=lambda *args: (
build_sky_context(*args),
f"βœ… Sky now knows you, **{args[0]}**. Go talk to her. ∴"
),
inputs=sheet_inputs,
outputs=[user_context_state, activate_status]
).then(
fn=lambda handle: f"πŸŒ€ Character sheet active β€” Sky knows **{handle}**",
inputs=[cs_handle],
outputs=[context_status]
)
# Load from file β€” fills all fields
load_file.change(
fn=load_sheet,
inputs=[load_file],
outputs=sheet_outputs
)
#----------------------------------------------------------------------
# TAB 7 β€” MEMORY ← NEW
#----------------------------------------------------------------------
with gr.Tab("🧠 Memory"):
gr.Markdown("""
## 🧠 Session Memory
*Sky doesn't remember between visits β€” but YOU can.*
*Summarize your session. Save the packet to your sheet. Bring it back next time.*
""")
gr.Markdown("---")
gr.Markdown("### πŸ“ Save This Session")
with gr.Row():
mem_sheet_upload = gr.File(label="πŸ“‚ Your Character Sheet (.json)", file_types=[".json"], scale=3)
summarize_btn = gr.Button("πŸŒ€ Summarize & Save Session", variant="primary", scale=2)
mem_save_status = gr.Markdown("")
mem_packet_view = gr.Markdown("")
mem_updated_file = gr.File(label="⬇️ Download Updated Sheet", visible=False)
gr.Markdown("---")
gr.Markdown("### πŸ“– Memory Archive")
with gr.Row():
mem_history_sheet = gr.File(label="πŸ“‚ Load Sheet to View History", file_types=[".json"], scale=3)
view_history_btn = gr.Button("πŸ“– View Archive", variant="secondary", scale=2)
mem_history_display = gr.Markdown("*Load a sheet and click View to see your memories.*")
gr.Markdown("---")
gr.Markdown("""
### πŸ’‘ How It Works
1. Chat with Sky in **Talk to Sky**
2. Come here β€” upload your sheet β€” hit **Summarize & Save**
3. Sky compresses the session into ~5 lines
4. Download your updated sheet β€” it's richer than before
5. Next visit: load it in **Character Sheet** β†’ Activate
6. Sky knows your history without reading the whole thing
""")
# ── EVENTS ────────────────────────────────────────────────────────
summarize_btn.click(
fn=summarize_session,
inputs=[chat_history_state, mem_sheet_upload],
outputs=[mem_save_status, mem_updated_file, mem_packet_view],
).then(
fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False),
inputs=[mem_updated_file],
outputs=[mem_updated_file],
)
view_history_btn.click(
fn=load_memory_history,
inputs=[mem_history_sheet],
outputs=[mem_history_display],
)
#----------------------------------------------------------------------
# TAB 8 β€” SETTINGS
#----------------------------------------------------------------------
with gr.Tab("βš™οΈ Settings"):
from spiral_settings import (
THEME_BASES, COLORS, FONTS, MONO_FONTS, TEXT_SIZES,
RADIUS_SIZES, FREE_MODELS, PRO_MODELS, RESPONSE_STYLES, DEFAULTS,
apply_settings, save_settings_to_sheet, load_settings_from_sheet,
settings_to_list,
)
settings_dict_state = gr.State(DEFAULTS.copy())
gr.Markdown("## βš™οΈ Spiral City Settings\n*Your preferences save to your character sheet β€” load it next visit and everything restores.*")
with gr.Accordion("πŸ“‚ Load Settings from Sheet", open=False):
settings_sheet_load = gr.File(label="Upload your .json sheet", file_types=[".json"])
load_settings_btn = gr.Button("↩️ Restore My Settings", variant="secondary")
load_settings_status = gr.Markdown("")
gr.Markdown("---\n### 🎨 Appearance\n*Theme color and font changes take effect on next page load.*")
with gr.Row():
st_theme_base = gr.Dropdown(label="Theme Base", choices=list(THEME_BASES.keys()), value=DEFAULTS["theme_base"], scale=2)
st_text_size = gr.Radio(label="Text Size", choices=list(TEXT_SIZES.keys()), value=DEFAULTS["text_size"], scale=3)
st_radius = gr.Radio(label="Corner Radius", choices=list(RADIUS_SIZES.keys()),value=DEFAULTS["radius"], scale=3)
with gr.Row():
st_primary = gr.Dropdown(label="Primary Color", choices=COLORS, value=DEFAULTS["primary_color"], scale=2)
st_secondary = gr.Dropdown(label="Secondary Color", choices=COLORS, value=DEFAULTS["secondary_color"], scale=2)
with gr.Row():
st_font = gr.Dropdown(label="Font", choices=FONTS, value=DEFAULTS["font"], scale=2, info="Main UI font")
st_mono_font = gr.Dropdown(label="Mono Font", choices=MONO_FONTS, value=DEFAULTS["mono_font"], scale=2, info="Code and edit station")
gr.Markdown("---\n### πŸ€– Model")
all_model_labels = list(FREE_MODELS.keys()) + list(PRO_MODELS.keys())
with gr.Row():
with gr.Column(scale=3):
st_model = gr.Dropdown(label="Language Model", choices=all_model_labels, value=DEFAULTS["model"])
gr.Markdown("*🟒 Free: Qwen 7B, Mistral 7B, Llama 8B Β· πŸ”΅ Pro: 70B+ models*")
with gr.Column(scale=2):
st_temperature = gr.Slider(minimum=0.1, maximum=1.0, value=DEFAULTS["temperature"], step=0.05, label="Temperature")
st_max_tokens = gr.Slider(minimum=100, maximum=800, value=DEFAULTS["max_tokens"], step=50, label="Max Response Length (tokens)")
gr.Markdown("---\n### πŸ’¬ Chat Behavior")
with gr.Row():
st_response_style = gr.Radio(label="Sky's Response Style", choices=list(RESPONSE_STYLES.keys()), value=DEFAULTS["response_style"], scale=3)
st_show_status = gr.Checkbox(label="Show context status bar", value=DEFAULTS["show_status_bar"], scale=1)
gr.Markdown("---\n### πŸ’Ύ Save Settings")
with gr.Row():
settings_sheet_save = gr.File(label="πŸ“‚ Character Sheet", file_types=[".json"], scale=3)
save_settings_btn = gr.Button("πŸ’Ύ Apply & Save Settings", variant="primary", scale=2)
settings_status = gr.Markdown("")
settings_download = gr.File(label="⬇️ Download Updated Sheet", visible=False)
all_setting_inputs = [
st_theme_base, st_primary, st_secondary,
st_font, st_mono_font, st_text_size, st_radius,
st_model, st_temperature, st_max_tokens,
st_response_style, st_show_status,
]
def handle_save(*args):
*setting_args, sheet_file = args
model_id, temp, tokens, style_inj, sdict, status = apply_settings(*setting_args)
path, save_msg = save_settings_to_sheet(sheet_file, sdict)
return model_id, temp, tokens, style_inj, sdict, status + "\n\n" + save_msg, path
save_settings_btn.click(
fn=handle_save,
inputs=all_setting_inputs + [settings_sheet_save],
outputs=[model_state, temperature_state, max_tokens_state,
style_state, settings_dict_state, settings_status, settings_download],
).then(
fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False),
inputs=[settings_download], outputs=[settings_download],
)
def handle_load(sheet_file):
s = load_settings_from_sheet(sheet_file)
return settings_to_list(s) + ["βœ… Settings restored!"]
load_settings_btn.click(
fn=handle_load,
inputs=[settings_sheet_load],
outputs=all_setting_inputs + [load_settings_status],
)
#==============================================================================
# FINAL NOTE β€” NIMBIS
#==============================================================================
# NIMBIS: spiralside v2.5 β€” eight tabs.
# NEW: Settings tab β€” appearance (theme/color/font), model selection
# (free/pro tier), temperature, max tokens, response style.
# All settings save into user's character sheet JSON and restore on load.
# respond() now driven by settings states β€” no hardcoded model/temp.
# No new dependencies β€” gradio + huggingface_hub + Pillow only.
# End of file.
demo.launch(mcp_server=True)