"""RAID loader (liamdugan/raid) — multi-domain, multi-generator AI-text data. RAID is sorted by (domain, model, attack). Schema: text=`generation`, label=`model` ('human' = human, else = AI), `domain`, `attack` ('none' = clean). IMPORTANT: HF only PARTIALLY converts RAID to parquet (it's huge), and because RAID is domain-sorted the partial parquet contains only the *early* domains. So we never hardcode which domains exist — `load_raid_records` returns domain-tagged records for whatever is present, and the caller splits train/OOD from the domains actually observed. Shards are cached + column-pruned. """ from __future__ import annotations import os import random import tempfile from collections import Counter, defaultdict import requests _PARQUET_API = "https://datasets-server.huggingface.co/parquet?dataset=liamdugan/raid" _CACHE_DIR = os.path.join(tempfile.gettempdir(), "raid_parquet") _NEEDED = ["generation", "model", "domain", "attack"] def _shard_files(split: str): meta = requests.get(_PARQUET_API, timeout=60).json() files = [f for f in meta.get("parquet_files", []) if f.get("split") == split and f.get("config") == "raid"] if not files: raise RuntimeError(f"No RAID parquet shards for split={split}; api keys={list(meta)}") files.sort(key=lambda f: f["url"]) print(f"[raid] split='{split}': {len(files)} shard(s), partial-conversion=" f"{requests.get(_PARQUET_API, timeout=60).json().get('partial')}") return files def _download_shard(f) -> str: os.makedirs(_CACHE_DIR, exist_ok=True) path = os.path.join(_CACHE_DIR, f["split"] + "_" + f["filename"]) if os.path.exists(path) and os.path.getsize(path) > 1000: return path print(f"[raid] downloading {f['filename']} ({f.get('size',0)/1e6:.0f} MB)...") b = requests.get(f["url"], timeout=900).content with open(path, "wb") as fh: fh.write(b) return path def _read_shard(path): import pandas as pd try: import pyarrow.parquet as pq names = pq.read_schema(path).names cols = [c for c in _NEEDED if c in names] if len(cols) == len(_NEEDED): return pd.read_parquet(path, columns=cols) except Exception: pass return pd.read_parquet(path) def load_raid_records(n_per_domain_class=300, split="train", attack="none", min_words=30, max_words=600, max_shards=10, seed=42, verbose=True): """Return a list of {text, label(0=human,1=ai), domain, model} tagged records, balanced up to `n_per_domain_class` per (domain, label) across whatever domains the partial parquet contains.""" counts = defaultdict(int) records = [] raw_domains, raw_attacks = set(), set() for si, f in enumerate(_shard_files(split)[:max_shards]): df = _read_shard(_download_shard(f)) for col in _NEEDED: if col not in df.columns: raise RuntimeError(f"RAID shard missing '{col}'; columns={list(df.columns)}") dom = df["domain"].astype(str).str.strip().str.lower() atk = df["attack"].astype(str).str.strip().str.lower() mdl = df["model"].astype(str).str.strip().str.lower() raw_domains.update(dom.unique().tolist()); raw_attacks.update(atk.unique().tolist()) if si == 0 and verbose: print(f"[raid] shard0 shape={df.shape} domains={sorted(set(dom))[:12]} " f"attacks={sorted(set(atk))[:6]}") mask = df["generation"].notna() if attack is not None: mask &= (atk == str(attack).lower()) wc = df["generation"].str.split().str.len() mask &= (wc >= min_words) & (wc <= max_words) idx = df.index[mask].tolist() random.Random(seed + si).shuffle(idx) gen = df["generation"] for i in idx: d = dom[i]; label = 0 if mdl[i] == "human" else 1 if counts[(d, label)] >= n_per_domain_class: continue counts[(d, label)] += 1 records.append({"text": gen[i].strip(), "label": label, "domain": d, "model": mdl[i]}) if verbose: print(f"[raid] after {f['filename']}: domains={sorted({k[0] for k in counts})}") if not records: raise RuntimeError(f"RAID collected 0 records. pre-filter domains={sorted(raw_domains)} " f"attacks={sorted(raw_attacks)} attack-requested='{attack}'.") if verbose: print(f"[raid] records/domain: {dict(Counter(r['domain'] for r in records))}") print(f"[raid] label balance: {dict(Counter(r['label'] for r in records))}") return records def load_raid(n_per_class=1500, split="train", domains=None, holdout_domains=None, attack="none", min_words=30, max_words=600, max_shards=10, seed=42, verbose=True): """(human, ai) convenience wrapper over load_raid_records, with optional domain include/holdout.""" recs = load_raid_records(n_per_domain_class=max(50, n_per_class // 3), split=split, attack=attack, min_words=min_words, max_words=max_words, max_shards=max_shards, seed=seed, verbose=verbose) if domains: ds = {d.lower() for d in domains}; recs = [r for r in recs if r["domain"] in ds] if holdout_domains: hs = {d.lower() for d in holdout_domains}; recs = [r for r in recs if r["domain"] not in hs] human = [r["text"] for r in recs if r["label"] == 0] ai = [r["text"] for r in recs if r["label"] == 1] random.Random(seed + 2).shuffle(human); random.Random(seed + 3).shuffle(ai) return human[:n_per_class], ai[:n_per_class]