"""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 ('
' f'{p[0]}{p[1:]}
') else: styled.append(f'{p}
') body = "".join(styled) theme_short = theme[:64] + ("…" if len(theme) > 64 else "") return f""" """ def _render_outline(outline): lines = [l.strip() for l in outline.strip().split("\n") if l.strip()] items = "".join( f'; 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('