lolaby / utils /safety.py
AndrΓ© Oliveira
v0
6472a71
Raw
History Blame Contribute Delete
3.84 kB
"""
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, "", [])