Spaces:
Running on Zero
Running on Zero
| """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( | |
| """ | |
| <div class="constellation-header"> | |
| <h1>Constellation of Hope</h1> | |
| <p> | |
| 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. | |
| </p> | |
| </div> | |
| """ | |
| ) | |
| 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("<div class='progress-modal-title'>Your wish drifts toward the stars…</div>") | |
| progress_text = gr.Markdown("") | |
| with gr.Column(visible=False, elem_classes=["wisdom-modal"]) as wisdom_modal: | |
| gr.HTML("<div class='wisdom-modal-title'>A Star's Wisdom</div>") | |
| 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("<div class='wisdom-modal-title'>A Gentle Pause</div>") | |
| 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( | |
| "<div class='tts-player-header'>" | |
| "<span class='tts-player-indicator'>♪</span>" | |
| "<span class='tts-player-title'>Now whispering</span>" | |
| "</div>" | |
| "<div class='tts-player-text' id='tts-player-text'></div>" | |
| ) | |
| 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() | |