DoodleBook / config.py
Codex
Improve story, narration, and illustration prompts
67c02d3
Raw
History Blame Contribute Delete
14.3 kB
"""
DoodleBook — Single Source of Truth for Model IDs, Fallbacks, and Generation Params
Innovations:
- License-aware model selection (prevents non-commercial model usage)
- VRAM-based hardware recommendations for Modal
- Dynamic fallback resolution with logging
- Deterministic seed management for character consistency
- Type-safe model registry with frozen dataclasses
Phase 1, Task 0: All model IDs verified on HuggingFace Hub (June 2026)
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Dict, Any
import os
import logging
logger = logging.getLogger(__name__)
# ============================================================================
# LICENSE TRACKING
# ============================================================================
class LicenseType(Enum):
"""License types for model compliance checking."""
APACHE_2_0 = "apache-2.0"
MIT = "mit"
NON_COMMERCIAL = "non-commercial"
FLUX_DEV = "flux-dev-non-commercial"
UNKNOWN = "unknown"
# ============================================================================
# MODEL REGISTRY
# ============================================================================
@dataclass(frozen=True)
class ModelConfig:
"""
Immutable model configuration with license tracking.
Attributes:
hub_id: HuggingFace Hub model identifier
params_b: Model parameters in billions
license: License type for compliance
vram_gb: Estimated VRAM requirement in GB
fallback_id: Alternative model if primary fails
fallback_reason: Why fallback exists
is_primary: Whether this is a primary or fallback model
modal_gpu: Recommended Modal GPU type
modal_memory: Modal memory in MB
"""
hub_id: str
params_b: float
license: LicenseType
vram_gb: float
fallback_id: Optional[str] = None
fallback_reason: Optional[str] = None
is_primary: bool = True
modal_gpu: str = "T4"
modal_memory: int = 8192
@property
def can_use_commercially(self) -> bool:
"""Check if model can be used for commercial purposes."""
return self.license in (LicenseType.APACHE_2_0, LicenseType.MIT)
@property
def license_warning(self) -> Optional[str]:
"""Return warning if license has restrictions."""
if self.license == LicenseType.NON_COMMERCIAL:
return f"NON-COMMERCIAL: {self.hub_id} cannot be used in production"
if self.license == LicenseType.FLUX_DEV:
return f"FLUX DEV LICENSE: {self.hub_id} has usage restrictions"
return None
# ============================================================================
# VERIFIED MODEL REGISTRY (Phase 1, Task 0 — June 2026)
# ============================================================================
#
# All primary models verified on HuggingFace Hub.
# Fallbacks selected for license compatibility (Apache 2.0 preferred).
#
# The three sponsor models DoodleBook actually loads. Fallback/variant configs
# were removed so the HF Space links exactly these three (HF auto-links any
# model id it finds in the repo files).
FLUX_MODEL = ModelConfig(
hub_id="black-forest-labs/FLUX.2-klein-4B",
params_b=4.0,
license=LicenseType.APACHE_2_0,
vram_gb=13.0,
modal_gpu="A10G", # 24GB fits the ~13GB model; A100-40GB was overkill
modal_memory=32768,
)
STORY_MODEL = ModelConfig(
hub_id="openbmb/MiniCPM5-1B",
params_b=1.0,
license=LicenseType.APACHE_2_0,
vram_gb=4.0,
modal_gpu="T4",
modal_memory=8192,
)
TTS_MODEL = ModelConfig(
hub_id="openbmb/VoxCPM2",
params_b=2.0,
license=LicenseType.APACHE_2_0,
vram_gb=8.0,
modal_gpu="T4",
modal_memory=8192,
)
# ============================================================================
# VOICE PRESETS (VoxCPM2 "voice design")
# ============================================================================
#
# Each preset is a VoxCPM2 voice-design prefix — the (parenthetical) is read as
# an instruction describing the speaker, NOT spoken aloud. Ordered youngest →
# oldest. Default is a young child's voice (the previous adult "storyteller"
# voice sounded too old for the kids).
#
# SINGLE SOURCE OF TRUTH for the live app (app.py). modal_workers/modal_tts.py
# keeps a mirror of these values so the Modal deploy stays import-free — keep
# the two in sync if you edit them.
VOICE_PRESETS: Dict[str, Dict[str, str]] = {
"kid": {
"label": "🧒 Little kid",
"design": "(A sweet seven-year-old child joyfully reading a picture book to friends; "
"bright, clear, playful, and full of wonder; natural phrasing; gentle changes "
"of expression for dialogue; sound effects spoken with delighted energy, "
"never shouted harshly)",
},
"big_kid": {
"label": "🎒 Big kid",
"design": "(A lively eleven-year-old reading a favorite children's story aloud; "
"youthful, confident, energetic, and expressive; clear pacing; distinct but "
"natural character dialogue; playful emphasis on sound effects)",
},
"playful": {
"label": "✨ Playful",
"design": "(A cheerful young adult performing a joyful children's picture book; warm, "
"animated, smiling, and expressive; varied rhythm and light comic timing; "
"gentle character voices; lively but child-friendly sound effects)",
},
"storyteller": {
"label": "🌙 Storyteller",
"design": "(A warm, gentle adult storyteller reading a bedtime picture book to a young "
"child; soft, soothing, unhurried, kind, and cozy; meaningful pauses; tender "
"dialogue; sound effects softened enough to remain calming)",
},
"grandpa": {
"label": "👴 Grandpa",
"design": "(A kind older grandfather telling a cozy story to a child; warm, patient, "
"slightly slow, reassuring, and quietly expressive; friendly dialogue; "
"playful sound effects delivered with a soft chuckle)",
},
"my_voice": {
"label": "🎙️ My Voice",
"design": "(Preserve the reference speaker's identity while narrating clearly to a "
"young child; warm, natural, friendly, and expressive; use comfortable pacing, "
"gentle dialogue changes, and playful but controlled sound effects)",
},
}
# Default leans young per user request — the youngest, most kid-friendly voice.
DEFAULT_VOICE: str = "kid"
# (label, value) pairs for a gr.Radio — shows the friendly label, passes the key.
VOICE_CHOICES = [(preset["label"], key) for key, preset in VOICE_PRESETS.items()]
def voice_design(voice: str) -> str:
"""Return the VoxCPM2 design prefix for a voice key (falls back to default)."""
preset = VOICE_PRESETS.get(voice) or VOICE_PRESETS[DEFAULT_VOICE]
return preset["design"]
# ============================================================================
# SEED MANAGEMENT
# ============================================================================
BASE_SEED: int = 42 # Locked for character consistency across pages
def page_seed(page_num: int) -> int:
"""
Deterministic seed per page for character consistency.
Page 0 uses BASE_SEED, page 1 uses BASE_SEED+1, etc.
This ensures reproducible generation while maintaining
slight variation between pages.
Args:
page_num: Zero-indexed page number (0-5)
Returns:
Deterministic seed for this page
Example:
>>> page_seed(0) # 42
>>> page_seed(5) # 47
"""
if not 0 <= page_num <= 5:
raise ValueError(f"page_num must be 0-5, got {page_num}")
return BASE_SEED + page_num
# ============================================================================
# GENERATION PARAMETERS
# ============================================================================
@dataclass
class GenerationParams:
"""
Generation parameters for image and story creation.
These are the "knobs" that control quality vs speed tradeoffs.
"""
# Image generation
image_width: int = 768
image_height: int = 512
num_inference_steps: int = 20 # Standard mode
num_inference_steps_tiny: int = 4 # Tiny Mode (SD-Turbo)
guidance_scale: float = 3.5
# LoRA settings
lora_scale: float = 0.85
lora_repo: str = "build-small-hackathon/doodlebook-flux-lora"
# Story generation
max_story_tokens: int = 800
story_temperature: float = 0.7
story_top_p: float = 0.9
# TTS settings
tts_sample_rate: int = 48000
tts_speed: float = 1.0
# Book settings
num_pages: int = 6
target_age: int = 5
GENERATION_PARAMS = GenerationParams()
# ============================================================================
# COLOR PALETTE (Off-Brand Badge)
# ============================================================================
COLORS = {
"paper": "#FEF9E7", # App background
"page": "#FFFDE7", # Book pages
"ink": "#3E2723", # Body text
"title_brown": "#5D4037", # Headings
"crayon_orange": "#FF7043", # Primary CTA
"sky_accent": "#4FC3F7", # Secondary/links
"muted": "#BCAAA4", # Page numbers/meta
}
# ============================================================================
# MODAL CONFIGURATION
# ============================================================================
MODAL_CONFIG = {
"image_gen": {
"app_name": "doodlebook-image-gen",
"gpu": FLUX_MODEL.modal_gpu,
"memory": FLUX_MODEL.modal_memory,
"timeout": 300,
"keep_warm": 1, # During judging window
},
"story_gen": {
"app_name": "doodlebook-story",
"gpu": STORY_MODEL.modal_gpu,
"memory": STORY_MODEL.modal_memory,
"timeout": 120,
"keep_warm": 0,
},
"tts": {
"app_name": "doodlebook-tts",
"gpu": TTS_MODEL.modal_gpu,
"memory": TTS_MODEL.modal_memory,
"timeout": 120,
"keep_warm": 0,
},
}
# ============================================================================
# SAMPLE BOOK CONFIGURATION
# ============================================================================
SAMPLE_BOOK_PATH = "assets/sample_book"
SAMPLE_DOODLE_PATH = "assets/sample_doodle.jpg"
# ============================================================================
# VALIDATION & COMPLIANCE
# ============================================================================
def validate_model_ids(use_hf_hub: bool = False) -> Dict[str, Dict[str, Any]]:
"""
Validate model IDs exist on HuggingFace Hub.
Args:
use_hf_hub: If True, actually check HF Hub (slower but thorough)
Returns:
Dictionary mapping model IDs to validation results
"""
models = [FLUX_MODEL, STORY_MODEL, TTS_MODEL]
results = {}
for model in models:
results[model.hub_id] = {
"exists": True,
"checked": False,
"license": model.license.value,
"commercial_ok": model.can_use_commercially,
}
if use_hf_hub:
try:
from huggingface_hub import model_info
model_info(model.hub_id)
results[model.hub_id]["checked"] = True
logger.info(f"Verified: {model.hub_id}")
except Exception as e:
results[model.hub_id] = {
"exists": False,
"error": str(e),
"fallback": model.fallback_id,
}
logger.warning(f"Model not found: {model.hub_id}, fallback: {model.fallback_id}")
return results
def check_license_compliance() -> bool:
"""
Ensure no non-commercial models are in the primary production path.
Raises:
ValueError: If a non-commercial model is configured as primary
Returns:
True if all primary models are license-compliant
"""
primary_models = [FLUX_MODEL, STORY_MODEL, TTS_MODEL]
for model in primary_models:
if not model.can_use_commercially:
raise ValueError(
f"LICENSE VIOLATION: {model.hub_id} is {model.license.value}. "
f"This model CANNOT be used commercially. "
f"Use fallback: {model.fallback_id}"
)
return True
def get_model_with_fallback(
model: ModelConfig,
use_fallback: bool = False
) -> ModelConfig:
"""
Get model config, optionally using fallback.
Args:
model: Primary model config
use_fallback: If True, return fallback model
Returns:
ModelConfig (primary or fallback)
"""
# Fallback model configs were removed (the Space links only the 3 primaries);
# there is no alternate config to swap in, so always return the primary.
return model
# ============================================================================
# ENVIRONMENT VARIABLES
# ============================================================================
def get_hf_token() -> Optional[str]:
"""Get HuggingFace token from environment."""
return os.environ.get("HF_TOKEN")
def get_modal_token() -> Optional[str]:
"""Get Modal token from environment."""
return os.environ.get("MODAL_TOKEN_ID") or os.environ.get("MODAL_TOKEN")
# ============================================================================
# STARTUP VALIDATION
# ============================================================================
def startup_check() -> bool:
"""
Run at app startup to ensure configuration is valid.
Returns:
True if all checks pass
"""
try:
check_license_compliance()
logger.info("License compliance: PASSED")
return True
except ValueError as e:
logger.error(f"Startup check FAILED: {e}")
return False
# Run on import if in production mode
if os.environ.get("DOODLEBOOK_ENV") == "production":
startup_check()