"""funnies — builds the newspaper's fun section from the content itself. Orchestrates theme derivation, find-a-word, crossword (AI-drafted clues), cartoon/comic selection into a FunniesResult. Graceful empty/fallback when no viable content (Q5=A / BR3.2); the emitted result stays self-contained (BR4.1). """ from __future__ import annotations from .crossword import ClueDrafter, build_crossword from .find_a_word import build_find_a_word, derive_theme_words from .cartoon import CartoonSource, Comic, build_comic, select_cartoon from .model import FunniesBrief, FunniesResult class FunniesBuilder: """Funnies library entry point — assemble the funnies section.""" def __init__( self, clue_drafter: ClueDrafter | None = None, cartoon_source: CartoonSource | None = None, ) -> None: self._clue_drafter = clue_drafter self._cartoon_source = cartoon_source def build(self, brief: FunniesBrief) -> FunniesResult: if not brief.theme_words and not brief.context.strip(): # BR3.2 / Q5=A: nothing viable — graceful empty, never a broken block return FunniesResult( funnies_result_id="funnies-1", status="empty", status_message="No theme words or context provided; funnies skipped.", ) words = brief.theme_words or derive_theme_words(brief.context) if not words: return FunniesResult( funnies_result_id="funnies-1", status="empty", status_message="Could not derive any theme words from content.", ) find_word = build_find_a_word(words) crossword = build_crossword(words, brief.context, self._clue_drafter) cartoon = select_cartoon(brief.context, self._cartoon_source) result = FunniesResult( funnies_result_id="funnies-1", crossword=crossword, find_a_word=find_word, cartoons=[cartoon], comics=[], status="ready", ) # If the cartoon had to fall back, mark fallback (Q5=A) if cartoon.source == "generated": result.status = "fallback" result.status_message = "No xkcd found; used a content-derived strip." return result