""" Claude / LLM Text-Watermark Checker (KGW-Stil / Green-Red-List) ----------------------------------------------------------------- Implementiert das statistische Grundprinzip, das Anthropic fuer Claude-Wasserzeichen beschreibt (SynthID-Text / KGW-Familie): Bei jedem Generierungsschritt wird das Vokabular pseudorandom - gesteuert durch einen Secret Key + die vorherigen Tokens - in eine "Green List" und eine "Red List" gesplittet. Ein wasserzeichen- behaftetes Modell bevorzugt statistisch die Green-List-Tokens. Ohne den Key ist dieser Bias fuer Menschen unsichtbar, aber mit genuegend Text statistisch nachweisbar (z-Test). WICHTIG: Ohne Anthropics echten Secret Key kann dieses Skript Claude-Wasserzeichen NICHT verifizieren. Es demonstriert die Methodik und kann fuer eigene, selbst erzeugte Wasserzeichen (z.B. in eigenen LLM-Pipelines) produktiv genutzt werden. """ import hashlib import math import re from typing import List, Dict, Optional def _hash_token(seed_str: str, token: str) -> int: return int(hashlib.sha256(f"{seed_str}:{token.lower()}".encode()).hexdigest(), 16) def get_green_list(prev_token: str, key: str, vocab: List[str], gamma: float = 0.5) -> set: """Pseudorandomer Split des Vokabulars in Green/Red, seeded durch prev_token + Key.""" seed_str = f"{key}:{prev_token.lower()}" scored = sorted(vocab, key=lambda w: _hash_token(seed_str, w)) n_green = int(len(vocab) * gamma) return set(scored[:n_green]) def tokenize(text: str) -> List[str]: """Einfache Wort-Tokenisierung (fuer Produktionseinsatz: echten LLM-Tokenizer verwenden).""" return re.findall(r"[A-Za-zÀ-ÿ]+(?:'[a-z]+)?", text) def detect_watermark( tokens: List[str], key: str, gamma: float = 0.5, vocab: Optional[List[str]] = None, z_threshold: float = 4.0, ) -> Dict: """ Fuehrt den z-Test auf der Green-List-Traefferquote durch. - gamma: Erwarteter Anteil Green-Tokens unter Nullhypothese (kein Wasserzeichen). - z_threshold: Klassischer Schwellenwert (~4.0) fuer "wahrscheinlich wasserzeichenbehaftet". """ if vocab is None: vocab = list(set(t.lower() for t in tokens)) if len(tokens) < 2: return {"error": "Zu wenige Tokens fuer verlaessliche Erkennung (mind. ~75-100 empfohlen)."} green_hits = 0 n = 0 for i in range(1, len(tokens)): prev_tok, cur_tok = tokens[i - 1], tokens[i] green = get_green_list(prev_tok, key, vocab, gamma) if cur_tok.lower() in green: green_hits += 1 n += 1 observed_frac = green_hits / n se = math.sqrt(gamma * (1 - gamma) / n) if n > 0 else 0 z = (observed_frac - gamma) / se if se > 0 else 0.0 p_value = 0.5 * math.erfc(z / math.sqrt(2)) return { "tokens_tested": n, "green_hits": green_hits, "observed_fraction": round(observed_frac, 4), "expected_fraction_null": gamma, "z_score": round(z, 3), "p_value_one_sided": round(p_value, 6), "likely_watermarked": z > z_threshold, "reliability_note": ( "Ausreichend Text (>=100 Woerter empfohlen)." if n >= 100 else "Warnung: Textmenge unter empfohlenem Minimum, Ergebnis unsicher." ), } def check_text(text: str, key: str, gamma: float = 0.5) -> Dict: """High-Level-Funktion: Text -> Tokenisierung -> Watermark-Check.""" tokens = tokenize(text) return detect_watermark(tokens, key, gamma=gamma) if __name__ == "__main__": # Beispieltext (menschlich verfasst, kein echtes Anthropic-Wasserzeichen) demo_key = "beispiel-secret-key" demo_text = ( "Der Regen fiel leise auf das Dach, waehrend die Katze am Fenster sass " "und nach draussen schaute. Es war ein ruhiger Nachmittag, an dem nichts " "Aussergewoehnliches geschah, ausser dem gleichmaessigen Klang des Wassers." ) result = check_text(demo_text, demo_key) for k, v in result.items(): print(f"{k}: {v}")