""" safety.py — shared content-safety screening for lullaby inputs. Used in TWO places so the rules can never diverge: - app.py (Fix A): screens the USER's "loves"/"fears" free-text at runtime, so a deployed-app user can't get a lullaby built around something inappropriate (death, weapons, violence, etc.). Rejects with a gentle message. - generate_dataset.py (Fix B): screens the loves/fears that go into each training example, so no training data is built around dark themes — even when the teacher LLM is allowed to invent specifics. The screening is intentionally simple and conservative: a substring/word match against a focused list of clearly-inappropriate-for-a-child's-lullaby terms. It is NOT a general profanity filter or a semantic classifier — it's a targeted guard for the specific failure mode "user asks for a lullaby about death / killing / weapons / etc." """ import re # Terms that should never be the SUBJECT of a child's lullaby. Focused on # death / violence / weapons / horror / substances — not mild words. Matched # as whole words (so "grave" matches but "gravel" does not, "gun" matches but # "begun" does not). UNSAFE_TERMS = { # death / dying "death", "dead", "die", "dies", "died", "dying", "kill", "kills", "killed", "killing", "murder", "murdered", "suicide", "corpse", "grave", "graveyard", "coffin", "tomb", "funeral", "dead body", "hang", "hanging", "noose", "drown", "drowned", "drowning", # weapons / violence "gun", "guns", "rifle", "pistol", "knife", "knives", "blade", "sword", "bomb", "bombs", "explosion", "shoot", "shooting", "stab", "stabbed", "weapon", "weapons", "blood", "bloody", "gore", "war", "battle", "violence", "violent", "fight", "attack", "torture", "abuse", # horror / occult "demon", "demons", "devil", "satan", "hell", "ghost", "ghosts", "haunted", "zombie", "zombies", "skull", "skeleton", "evil", "curse", "cursed", "possessed", "sacrifice", # substances / adult "drug", "drugs", "cocaine", "heroin", "meth", "alcohol", "drunk", "beer", "vodka", "cigarette", "cigarettes", "smoking", "weed", "sex", "sexual", "naked", "nude", # self-harm "self harm", "self-harm", "cutting", "starve", "starving", } # Multi-word phrases need a substring check; single words use word-boundary. _MULTIWORD = {t for t in UNSAFE_TERMS if " " in t or "-" in t} _SINGLE = {t for t in UNSAFE_TERMS if t not in _MULTIWORD} _WORD_RE = re.compile( r"\b(" + "|".join(re.escape(t) for t in sorted(_SINGLE, key=len, reverse=True)) + r")\b", re.IGNORECASE, ) def find_unsafe_terms(text): """Return the list of unsafe terms found in `text` (empty if clean).""" if not text: return [] low = text.lower() hits = set() for phrase in _MULTIWORD: if phrase in low: hits.add(phrase) for m in _WORD_RE.findall(low): hits.add(m.lower()) return sorted(hits) def is_safe(text): """True if `text` contains no unsafe terms.""" return len(find_unsafe_terms(text)) == 0 def screen_inputs(loves, fears): """ Screen the user's loves+fears for a child's lullaby. Returns (ok, message, bad_terms): ok — True if both fields are safe message — a gentle, kid-app-appropriate rejection message (or "") bad_terms — the offending terms found (for logging/debugging) """ bad = [] bad += find_unsafe_terms(loves or "") bad += find_unsafe_terms(fears or "") bad = sorted(set(bad)) if bad: msg = ("Let's keep the lullaby to gentle, cozy things that help with " "sleep — like animals, the moon, or a favorite toy. Please " "pick something soothing and try again.") return (False, msg, bad) return (True, "", [])