"""Constellation of Hope — wishes become stars in a cosmic sky.""" from __future__ import annotations import json import math import os import random import uuid from datetime import datetime, timezone from functools import partial from pathlib import Path import gradio as gr from model import generate_poetic_saying, review_wish, synthesize_whisper CATEGORY_HUES = { "SHAPE": 0.55, # teal "BOON": 0.12, # warm gold "JOURNEY": 0.70, # indigo "BOND": 0.95, # rose "TRIBUTE": 0.38, # green } from starscape import Starscape def _stars_file() -> Path: data_dir = Path("/data") if data_dir.is_dir() and os.access(data_dir, os.W_OK): return data_dir / "stars.json" return Path(__file__).parent / "stars.json" STARS_FILE = _stars_file() print(f"[stars] persistence path: {STARS_FILE}", flush=True) def load_stars() -> list[dict]: if not STARS_FILE.exists(): return [] try: return json.loads(STARS_FILE.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return [] def save_stars(stars: list[dict]) -> None: tmp = STARS_FILE.with_suffix(".json.tmp") tmp.write_text(json.dumps(stars, indent=2), encoding="utf-8") tmp.replace(STARS_FILE) def random_star_position() -> tuple[float, float, float]: radius = 6 + random.random() * 14 theta = random.random() * 2 * 3.141592653589793 phi = math.acos(2 * random.random() - 1) x = radius * math.sin(phi) * math.cos(theta) y = radius * math.sin(phi) * math.sin(theta) z = radius * math.cos(phi) return round(x, 2), round(y, 2), round(z, 2) def add_wish(wish: str, state: dict): wish_text = (wish or "").strip() hide = gr.update(visible=False) show = gr.update(visible=True) if not wish_text: return ( state, state, "Please share a wish before sending it to the stars.", "", hide, hide, "", ) verdict = review_wish(wish_text) print(f"[add_wish] verdict={verdict}", flush=True) if verdict["kind"] == "nonsense": content = ( "This wish didn't quite take shape. Try sharing a hope, dream, " "or intention you'd like to send among the stars.\n\n" f"**You wrote:** _{wish_text}_" ) return state, state, "", "", hide, show, content final_wish = wish_text rewritten = verdict["kind"] == "rewrite" if rewritten: final_wish = verdict["rewrite"] category = verdict.get("category") or "SHAPE" hue = CATEGORY_HUES.get(category, 0.55) saying = generate_poetic_saying(final_wish) x, y, z = random_star_position() star = { "id": str(uuid.uuid4()), "wish": final_wish, "saying": saying, "category": category, "created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "x": x, "y": y, "z": z, "hue": hue, "size": round(0.18 + random.random() * 0.22, 3), } stars = list(state.get("stars", [])) stars.append(star) save_stars(stars) new_state = {"stars": stars, "highlight_id": star["id"]} if rewritten: content = ( "Your wish was gently reshaped into a kinder form before it joined the stars.\n\n" f"**Original:** _{wish_text}_\n\n" f"**Reshaped:** _{final_wish}_" ) return ( new_state, new_state, "Your star has joined the constellation.", saying, hide, show, content, ) return ( new_state, new_state, "Your star has joined the constellation.", saying, show, hide, "", ) CUSTOM_CSS = """ .gradio-container { background: radial-gradient(circle at top, #0f1028 0%, #050510 45%, #000 100%) !important; } .constellation-header { text-align: center; color: #eef0ff; margin-bottom: 0.5rem; } .constellation-header h1 { font-weight: 300; letter-spacing: 0.06em; margin-bottom: 0.25rem; } .constellation-header p { color: rgba(210, 215, 245, 0.75); max-width: 640px; margin: 0 auto; } .gradio-container textarea, .gradio-container input[type="text"] { color: #eef0ff !important; background: rgba(4, 6, 20, 0.85) !important; caret-color: #eef0ff !important; } .gradio-container textarea::placeholder, .gradio-container input[type="text"]::placeholder { color: rgba(180, 190, 230, 0.45) !important; } .wisdom-modal { width: min(640px, 100%); margin: 1rem auto !important; padding: 1.4rem 1.6rem !important; border-radius: 16px !important; background: rgba(8, 10, 28, 0.92) !important; border: 1px solid rgba(147, 160, 255, 0.4) !important; backdrop-filter: blur(14px); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); } .wisdom-modal-title { font-size: 0.8rem; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(200, 210, 255, 0.85); margin-bottom: 0.85rem; text-align: center; } .wisdom-modal-close { margin-top: 0.5rem; } .progress-modal { width: min(640px, 100%); margin: 1.5rem auto !important; padding: 1.6rem 1.8rem !important; border-radius: 16px !important; background: rgba(8, 10, 28, 0.7) !important; border: 1px solid rgba(180, 200, 255, 0.35) !important; text-align: center; animation: progressGlow 3.2s ease-in-out infinite; } .progress-modal-title { font-size: 0.78rem; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(190, 205, 255, 0.85); margin-bottom: 0.9rem; animation: progressShimmer 2.6s ease-in-out infinite; } .progress-modal p, .progress-modal .prose, .progress-modal .prose p { font-size: 1.1rem; font-style: italic; color: rgba(230, 235, 255, 0.92) !important; margin: 0; } @keyframes progressGlow { 0%, 100% { box-shadow: 0 0 30px rgba(147, 160, 255, 0.15); transform: translateY(0); } 50% { box-shadow: 0 0 60px rgba(147, 160, 255, 0.4); transform: translateY(-4px); } } @keyframes progressShimmer { 0%, 100% { opacity: 0.65; } 50% { opacity: 1; } } .tts-bridge { opacity: 0.001; height: 1px !important; overflow: hidden; margin: 0 !important; padding: 0 !important; } .tts-player { display: none !important; width: 100%; margin: 1rem 0 !important; padding: 1.1rem 1.4rem !important; border-radius: 14px !important; background: rgba(8, 10, 28, 0.85) !important; border: 1px solid rgba(180, 200, 255, 0.32) !important; box-shadow: 0 12px 36px rgba(0, 0, 0, 0.4); animation: ttsGlow 2.8s ease-in-out infinite; } .tts-player.visible { display: block !important; } .tts-player-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.55rem; } .tts-player-indicator { font-size: 1.25rem; color: rgba(220, 225, 255, 0.95); animation: ttsBeat 0.9s ease-in-out infinite; } .tts-player-title { font-size: 0.78rem; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(190, 205, 255, 0.85); } .tts-player-text { font-size: 1.0rem; font-style: italic; color: rgba(230, 235, 255, 0.92); margin-bottom: 0.75rem; } @keyframes ttsGlow { 0%, 100% { box-shadow: 0 12px 36px rgba(0, 0, 0, 0.4); } 50% { box-shadow: 0 12px 36px rgba(147, 160, 255, 0.32); } } @keyframes ttsBeat { 0%, 100% { transform: scale(1); opacity: 0.75; } 50% { transform: scale(1.2); opacity: 1; } } """ APP_THEME = gr.themes.Base( primary_hue="indigo", secondary_hue="purple", neutral_hue="slate", ).set( body_background_fill="transparent", block_background_fill="rgba(8, 10, 28, 0.72)", block_border_color="rgba(147, 160, 255, 0.25)", body_text_color="#eef0ff", input_background_fill="rgba(4, 6, 20, 0.85)", input_background_fill_focus="rgba(4, 6, 20, 0.95)", input_border_color="rgba(147, 160, 255, 0.35)", input_placeholder_color="rgba(180, 190, 230, 0.45)", ) def build_app() -> gr.Blocks: initial_state = {"stars": load_stars()} with gr.Blocks(title="Constellation of Hope") as demo: gr.HTML( """

Constellation of Hope

Share a wish with the cosmos. Qwen 2.5 will weave it into a poetic star and place it among the constellations for all to see.

""" ) sky_state = gr.State(initial_state) sky = Starscape(value=initial_state, height=1120, elem_id="constellation-sky") with gr.Column(visible=False, elem_classes=["progress-modal"]) as progress_modal: gr.HTML("
Your wish drifts toward the stars…
") progress_text = gr.Markdown("") with gr.Column(visible=False, elem_classes=["wisdom-modal"]) as wisdom_modal: gr.HTML("
A Star's Wisdom
") status = gr.Textbox(label="Status", interactive=False) latest_saying = gr.Textbox( label="Star wisdom", interactive=False, lines=4, ) close_modal_btn = gr.Button("Close", elem_classes=["wisdom-modal-close"]) with gr.Column(visible=False, elem_classes=["wisdom-modal", "review-modal"]) as review_modal: gr.HTML("
A Gentle Pause
") review_content = gr.Markdown("") close_review_btn = gr.Button("Close", elem_classes=["wisdom-modal-close"]) # Visible "Now whispering" player — appears above the wish input while # audio plays, hidden by default via the CSS .tts-player rule. with gr.Column(elem_id="tts-player", elem_classes=["tts-player"]) as tts_player: gr.HTML( "
" "" "Now whispering" "
" "
" ) tts_audio = gr.Audio( elem_id="tts-audio", elem_classes=["tts-audio"], autoplay=True, interactive=False, label="", ) wish_input = gr.Textbox( label="Your wish", placeholder="I hope my family finds peace this year...", lines=3, max_lines=5, ) submit_btn = gr.Button("Release your wish to the stars", variant="primary") # Hidden TTS bridge — the starscape JS sets the text and clicks the trigger. with gr.Row(elem_classes=["tts-bridge"]): tts_text_in = gr.Textbox( elem_id="tts-text-in", elem_classes=["tts-text-in"], label="", ) tts_trigger = gr.Button( "Speak", elem_id="tts-trigger", elem_classes=["tts-trigger"], ) tts_trigger.click(fn=synthesize_whisper, inputs=tts_text_in, outputs=tts_audio) submit_btn.click( fn=lambda w: (gr.update(visible=True), f"_{(w or '').strip()}_"), inputs=wish_input, outputs=[progress_modal, progress_text], ).then( fn=add_wish, inputs=[wish_input, sky_state], outputs=[sky, sky_state, status, latest_saying, wisdom_modal, review_modal, review_content], ).then( fn=lambda: gr.update(visible=False), outputs=progress_modal, ).then(lambda: "", outputs=wish_input) close_modal_btn.click(lambda: gr.update(visible=False), outputs=wisdom_modal) close_review_btn.click(lambda: gr.update(visible=False), outputs=review_modal) demo.load(lambda s: s, inputs=sky_state, outputs=sky) return demo demo = build_app() LAUNCH_KWARGS = { "theme": APP_THEME, "css": CUSTOM_CSS, "ssr_mode": False, } demo.launch = partial(demo.launch, **LAUNCH_KWARGS) if __name__ == "__main__": demo.launch()