Spaces:
Running
Running
| """ | |
| ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: | |
| 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) |