initial upload

This commit is contained in:
sam-paech
2025-10-17 03:44:24 +11:00
parent 49cff43c50
commit b480cb72f7
33 changed files with 4007542 additions and 0 deletions

163
utils/whitelist.py Normal file
View File

@@ -0,0 +1,163 @@
# utils/whitelist.py
"""
Constructs a global whitelist of strings that must never be placed in
any ban list.
Sources
-------
1. All special-token texts exposed by the models tokenizer.
2. Every phrase (entire line) and every word inside those phrases
that appears *after* the assistant-message placeholder in a single
user→assistant chat-template example.
3. Optional user-supplied strings from the YAML / CLI configuration.
"""
from __future__ import annotations
import json
import threading
from pathlib import Path
from typing import Iterable, Set
from transformers import AutoTokenizer
from slop_forensics.utils import normalize_text as normalise_keep_marks
from slop_forensics.utils import extract_words
# ------------------------------------------------------------------------------
# Helper class
# ------------------------------------------------------------------------------
class WhitelistBuilder:
"""
Static helper for creating and persisting the whitelist.
All strings are:
* converted to lowercase
* normalised via `normalise_keep_marks`
* deduplicated
"""
_tokenizer_cache: dict[str, "AutoTokenizer"] = {}
_cache_lock = threading.Lock()
# ------------------------------------------------------------------ #
# Public API #
# ------------------------------------------------------------------ #
@classmethod
def build(
cls,
model_id: str,
*,
extra_user_items: Iterable[str] | None = None,
) -> Set[str]:
"""
Parameters
----------
model_id
Hugging Face model ID or local checkpoint directory.
extra_user_items
Arbitrary strings provided by the user that must also be whitelisted.
Returns
-------
Set[str]
Normalised whitelist entries (lower-cased, no duplicates, no blanks).
"""
tokenizer = cls._get_tokenizer(model_id)
whitelist: set[str] = set()
# 1. Special token texts -----------------------------------------
special_token_texts = [
tokenizer.bos_token,
tokenizer.eos_token,
tokenizer.unk_token,
tokenizer.pad_token,
tokenizer.cls_token,
tokenizer.sep_token,
*(tokenizer.additional_special_tokens or []),
]
for raw_text in special_token_texts:
if not raw_text:
continue
cls._add_phrase_and_words(whitelist, raw_text)
# 2. Tail of the chat template -----------------------------------
template_tail_text = cls._get_chat_template_tail(tokenizer)
for line in template_tail_text.splitlines():
cls._add_phrase_and_words(whitelist, line)
# 3. User-supplied extras ----------------------------------------
if extra_user_items:
for item in extra_user_items:
cls._add_phrase_and_words(whitelist, str(item))
# Final clean-up: remove any empty strings that might have slipped in
whitelist.discard("")
return whitelist
@classmethod
def write(cls, file_path: Path, whitelist: Iterable[str]) -> None:
"""Write the whitelist to *file_path* as pretty-printed JSON."""
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(
json.dumps(sorted(whitelist), indent=2, ensure_ascii=False),
encoding="utf-8",
)
# ------------------------------------------------------------------ #
# Internal helpers #
# ------------------------------------------------------------------ #
@classmethod
def _get_tokenizer(cls, model_id: str):
"""Thread-safe one-time load of the tokenizer."""
with cls._cache_lock:
tokenizer = cls._tokenizer_cache.get(model_id)
if tokenizer is None:
tokenizer = AutoTokenizer.from_pretrained(
model_id, trust_remote_code=True
)
cls._tokenizer_cache[model_id] = tokenizer
return tokenizer
@staticmethod
def _add_phrase_and_words(target_set: set[str], raw_text: str) -> None:
"""
Normalise *raw_text*, add the whole phrase, then add each individual
word extracted from the phrase.
"""
normalised = normalise_keep_marks(raw_text)
if not normalised:
return
target_set.add(normalised)
target_set.update(extract_words(normalised))
@staticmethod
def _get_chat_template_tail(tokenizer) -> str:
"""
Build one user→assistant chat-template instance and return only the
text *after* the assistant placeholder. That is the scaffold the
model tends to emit, so its words must be whitelisted.
"""
placeholder_user = "__USER__"
placeholder_assistant = "__ASSISTANT__"
messages = [
{"role": "user", "content": placeholder_user},
{"role": "assistant", "content": placeholder_assistant},
]
full_template: str = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
assistant_pos = full_template.find(placeholder_assistant)
if assistant_pos == -1:
# Fallback: return the whole template if the placeholder wasn't found
return full_template.strip()
tail = full_template[assistant_pos + len(placeholder_assistant):].strip()
return tail