"""Nidra — personalised bedtime story, narrated in your voice. A two-step agent (NVIDIA Nemotron-Nano-9B: outline → full story) narrates via VoxCPM2. All GPU work on Modal; this Space is CPU-only. Hackathon: Thousand Token Wood · Agentic · NVIDIA Nemotron · OpenBMB. """ from __future__ import annotations import io, re, random import gradio as gr import modal, numpy as np, soundfile as sf from theme import build_css MODAL_APP = "praxy-voice" DEFAULT_REF = "ashwin_10s.wav" _STORY = modal.Cls.from_name(MODAL_APP, "NemotronStoryteller") _VOX = modal.Cls.from_name(MODAL_APP, "VoxCPM2Cloner") def _sentences(t): return [s.strip() for s in re.split(r"(?<=[.!?])\s+", t) if s.strip()] def make_story(name, age, theme, ref_audio_path): if not (name and name.strip()) or not (theme and theme.strip()): return None, _empty_book(), _empty_outline(), None ref_path = ref_audio_path or DEFAULT_REF with open(ref_path, "rb") as f: ref_bytes = f.read() res = _STORY().write_story.remote(name=name, age=int(age), theme=theme) outline, story = res["outline"], res["story"] parts, sr = [], 48000 for s in _sentences(story): wb, sr = _VOX().clone.remote(text=s, ref_audio_bytes=ref_bytes) arr, _ = sf.read(io.BytesIO(wb), dtype="float32") parts.append(arr); parts.append(np.zeros(int(sr*.3), dtype="float32")) full = np.concatenate(parts) if parts else np.zeros(1, dtype="float32") out_path = "/tmp/nidra_story.wav" sf.write(out_path, full, sr) trace_path = "/tmp/nidra_trace.md" with open(trace_path, "w") as f: f.write(f"# Nidra trace\n\n**For:** {name} (age {age})\n**Theme:** {theme}\n\n" f"## Agent step 1 — outline\n{outline}\n\n## Step 2 — story\n{story}\n") return out_path, _render_book(name, age, theme, story), _render_outline(outline), trace_path def _empty_book(): return ('
" '✦ Your story will appear here, once the words are woven…
') def _empty_outline(): return ('
" "The agent's outline will appear here.
") def _render_book(name, age, theme, story): paras = [p.strip() for p in story.split("\n\n") if p.strip()] styled = [] for i, p in enumerate(paras): if i == 0 and p: styled.append( f'

' f'{p[0]}{p[1:]}

') else: styled.append(f'

{p}

') body = "".join(styled) theme_short = theme[:64] + ("…" if len(theme) > 64 else "") return f"""
A bedtime story for
{name}
age {age} · {theme_short}
{body}
✦  THE END  ✦
""" def _render_outline(outline): lines = [l.strip() for l in outline.strip().split("\n") if l.strip()] items = "".join( f'
{l}
' for l in lines) return ( '
' '
" '◆ Agent outline · step 1
' f'{items}
') # ── night-sky hero (80 seeded CSS stars + mountains + moon) ────────────────── random.seed(42) _STARS = "".join( f'
' for _ in range(80)) _MOUNTAINS = ('' '' '') HERO = f"""
{_STARS}
{_MOUNTAINS}
🌙

Nidra

A bedtime story woven for your child — written by an AI agent, narrated in your own voice.

NVIDIA NEMOTRON VoxCPM2 · OPENBMB AGENTIC · 2-STEP

⏳ First run warms the models on Modal (~3–5 min). After that it's quick.

""" EXTRA = """ @keyframes tw {0%{opacity:.15;transform:scale(1)}100%{opacity:1;transform:scale(2.1)}} @keyframes mf {0%,100%{transform:translateY(0) rotate(-5deg)}50%{transform:translateY(-10px) rotate(5deg)}} @keyframes shoot {0%{opacity:0;transform:translate(0,0)}6%{opacity:1}80%{opacity:.5}100%{opacity:0;transform:translate(220px,108px)}} /* Gradio's prose styling forces a dark colour on

; keep the story text light. */ #story-col p { color:#F4EFDE !important; } """ with gr.Blocks(title="Nidra — Bedtime Story Maker") as demo: gr.HTML(f"") gr.HTML(HERO) with gr.Row(): name_in = gr.Textbox(label="Child's name", placeholder="Arjun", scale=2) age_in = gr.Slider(2, 10, value=5, step=1, label="Age", scale=1) theme_in = gr.Textbox( label="Story theme", placeholder="a curious mongoose who finds a magical library under a banyan tree", lines=2) ref_in = gr.Audio(sources=["upload", "microphone"], type="filepath", label="Narrator's voice (optional — leave empty for the default warm voice)") gen_btn = gr.Button("✨ Weave the story", variant="primary", elem_id="cta") audio_out = gr.Audio(label="Listen", type="filepath") gr.HTML('


') with gr.Row(): story_out = gr.HTML(value=_empty_book(), elem_id="story-col") outline_out = gr.HTML(value=_empty_outline(), elem_id="outline-col") trace_out = gr.File(label="Download the agent trace (Markdown)") gen_btn.click(make_story, inputs=[name_in, age_in, theme_in, ref_in], outputs=[audio_out, story_out, outline_out, trace_out]) if __name__ == "__main__": demo.launch()