cdr6934's picture
Updated download button
d7c6e3b
Raw
History Blame Contribute Delete
12.1 kB
"""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()