# app.py — Gradio Blocks entry point. UI + wiring only. ZERO model references. """Rupkotha (রূপকথা) — a bedtime-story app for kids. This file orchestrates the UI and chains core functions: transcribe() → generate_story() → speak() It must contain no model names, paths, or model logic — those live only in core/. Layout: a two-panel "studio" — a Create panel (language/style, pictures, ask) and a Story panel (text + audio + save) — over a night-sky theme. Session memory uses gr.State, never browser storage (CLAUDE.md §11). """ from pathlib import Path import gradio as gr from core.vision_story import generate_story from core.stt import transcribe from core.tts import speak from core.prompts import STYLES # Language radio: display label → internal code passed to core functions. _LANGUAGES = [("English", "en"), ("বাংলা", "bn")] _STYLE_CHOICES = {lang: list(styles.keys()) for lang, styles in STYLES.items()} _CSS_PATH = Path(__file__).parent / "assets" / "styles.css" HISTORY_SIZE = 3 # how many recent stories to keep (CLAUDE.md §11: last 3) def _styles_for(language: str): """Return a style-dropdown update for the chosen language.""" choices = _STYLE_CHOICES.get(language, _STYLE_CHOICES["en"]) return gr.update(choices=choices, value=choices[0]) def _preview(files): """Show uploaded images in the preview gallery; hide it when empty.""" files = files or [] return gr.update(value=files, visible=bool(files)) def _voice_to_text(audio_path, language): """Transcribe a mic recording into the instruction box. On empty/failed transcription, leave whatever the child already typed untouched.""" text = transcribe(audio_path, language) return text if text else gr.update() def _tell_a_story(images, instruction, language, style, child_name): """Chain: images + instruction → story text → motherly-voice audio. Each core call degrades gracefully (never raises), so the UI always shows a story even if Modal is unreachable or audio synthesis fails. Also returns a `current` dict so the Save button can capture the exact result shown. """ image_paths = [img for img in (images or [])] story, model_label = generate_story( image_paths=image_paths, instruction=instruction or "", language=language, style=style, child_name=child_name or "", ) wav_path, tts_label = speak(story, language) badge = f"📖 {model_label} · 🔊 {tts_label}" current = {"story": story, "audio": wav_path, "badge": badge} return story, wav_path, badge, current def _history_updates(history): """Flatten `history` into per-slot updates: (group, markdown, audio) × N.""" updates = [] for i in range(HISTORY_SIZE): if i < len(history): entry = history[i] body = f"{entry['story']}\n\n{entry['badge']}" updates += [ gr.update(visible=True), gr.update(value=body), gr.update(value=entry.get("audio")), ] else: updates += [ gr.update(visible=False), gr.update(value=""), gr.update(value=None), ] return updates def _save_story(current, history): """Prepend the current story to the session history (newest first, max N).""" history = list(history or []) if current and current.get("story"): history = ([current] + history)[:HISTORY_SIZE] return [history, *_history_updates(history)] def build_ui() -> gr.Blocks: theme = gr.themes.Soft( primary_hue="amber", secondary_hue="orange", neutral_hue="slate", radius_size="lg", font=[gr.themes.GoogleFont("Nunito"), "ui-sans-serif", "sans-serif"], ) css_kw = {"css_paths": [str(_CSS_PATH)]} if _CSS_PATH.exists() else {} with gr.Blocks(title="রূপকথা · Rupkotha", theme=theme, fill_width=True, **css_kw) as demo: # ── Hero ───────────────────────────────────────────────────────── gr.HTML( """
Show a picture, ask for a story — and hear it told in a warm motherly voice.