Spaces:
Running
Running
| """ | |
| 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, "", []) | |