"""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'