| """Minimal self-contained inference model for the Booru prompt generator release.""" |
| import math |
| import re |
| from typing import Dict, List, Optional, Set |
|
|
| import numpy as np |
| import torch |
| import torch.nn as nn |
|
|
|
|
| PAD_ID = 0 |
| BOS_ID = 1 |
| EOS_ID = 2 |
| RATING_G = 3 |
| RATING_S = 4 |
| RATING_Q = 5 |
| RATING_E = 6 |
| OFFSET = 7 |
|
|
| _RATING_TOKENS = {"g": RATING_G, "s": RATING_S, "q": RATING_Q, "e": RATING_E} |
|
|
|
|
| def _apply_top_k_top_p(probs: torch.Tensor, top_k: int, top_p: float) -> torch.Tensor: |
| """Filter a probability distribution with top-k and/or nucleus (top-p) sampling.""" |
| if top_k > 0: |
| k = min(top_k, probs.size(0)) |
| threshold = torch.topk(probs, k).values[-1] |
| probs = probs.where(probs >= threshold, torch.zeros_like(probs)) |
| if top_p < 1.0: |
| sorted_probs, sorted_idx = torch.sort(probs, descending=True) |
| cumsum = torch.cumsum(sorted_probs, dim=0) |
| nucleus_mask = cumsum <= top_p |
| if nucleus_mask.any(): |
| nucleus_mask[0] = True |
| kept = torch.zeros_like(probs, dtype=torch.bool) |
| kept.scatter_(0, sorted_idx, nucleus_mask) |
| probs = probs.where(kept, torch.zeros_like(probs)) |
| return probs |
|
|
|
|
| class Vocab: |
| def __init__(self, tags: List[str], counts: List[int]): |
| self.tags = tags |
| self.tag_to_idx = {tag: i for i, tag in enumerate(tags)} |
| self.counts = np.array(counts, dtype=np.int64) |
| self.total = int(self.counts.sum()) |
| self.freqs = self.counts.astype(np.float64) / max(self.total, 1) |
|
|
| def __len__(self) -> int: |
| return len(self.tags) |
|
|
|
|
| class SimpleGraph: |
| def __init__(self, vocab: Vocab, mutex: List[List[int]]): |
| self.vocab = vocab |
| self.mutex = mutex |
|
|
|
|
| class TagTransformer(nn.Module): |
| """Permutation-equivariant Transformer for tag-set generation.""" |
|
|
| def __init__( |
| self, |
| vocab_size: int, |
| d_model: int = 256, |
| nhead: int = 4, |
| num_layers: int = 4, |
| dim_feedforward: int = 1024, |
| dropout: float = 0.1, |
| max_len: int = 256, |
| ): |
| super().__init__() |
| self.vocab_size = vocab_size |
| self.d_model = d_model |
| self.max_len = max_len |
| self.embedding = nn.Embedding(vocab_size + OFFSET, d_model, padding_idx=PAD_ID) |
| layer = nn.TransformerEncoderLayer( |
| d_model=d_model, |
| nhead=nhead, |
| dim_feedforward=dim_feedforward, |
| dropout=dropout, |
| batch_first=True, |
| norm_first=True, |
| ) |
| self.transformer = nn.TransformerEncoder(layer, num_layers=num_layers, enable_nested_tensor=False) |
| self.output = nn.Linear(d_model, vocab_size + OFFSET) |
| self._init_weights() |
|
|
| def _init_weights(self): |
| for p in self.parameters(): |
| if p.dim() > 1: |
| nn.init.xavier_uniform_(p) |
|
|
| def forward(self, input_ids: torch.Tensor) -> torch.Tensor: |
| seq_len = input_ids.size(1) |
| mask = nn.Transformer.generate_square_subsequent_mask(seq_len, device=input_ids.device).bool() |
| x = self.embedding(input_ids) |
| padding_mask = input_ids == PAD_ID |
| x = self.transformer( |
| x, |
| mask=mask, |
| src_key_padding_mask=padding_mask, |
| is_causal=True, |
| ) |
| return self.output(x) |
|
|
|
|
| class NeuralPromptGenerator: |
| GROUP_SUFFIXES = ["_hair", "_eyes"] |
|
|
| def __init__( |
| self, |
| model: TagTransformer, |
| graph: SimpleGraph, |
| device: Optional[torch.device] = None, |
| seed: Optional[int] = None, |
| distribution_weight: float = 0.75, |
| fallback_top_k: int = 20, |
| ): |
| self.model = model |
| self.graph = graph |
| self.vocab = graph.vocab |
| self.device = device or torch.device("cpu") |
| self.model.to(self.device) |
| self.model.eval() |
| self.rng = np.random.default_rng(seed) |
| self.distribution_weight = distribution_weight |
| self.fallback_top_k = fallback_top_k |
|
|
| self._n_real = len(self.vocab) |
| self._real_token_ids = torch.arange(self._n_real, device=self.device) + OFFSET |
| self._log_uniform = -np.log(max(self._n_real, 1)) |
| self._special_ids = {PAD_ID, BOS_ID, EOS_ID} |
|
|
| self._group_ids = np.full(self._n_real, -1, dtype=np.int32) |
| for i, tag in enumerate(self.vocab.tags): |
| for gid, suffix in enumerate(self.GROUP_SUFFIXES): |
| if tag.endswith(suffix): |
| self._group_ids[i] = gid |
| break |
| self._group_ids_tensor = torch.from_numpy(self._group_ids).to(self.device) |
|
|
| def _target_log_prob(self, idx: int, alpha: float) -> float: |
| log_emp = np.log(max(self.vocab.freqs[idx], 1e-12)) |
| return (1.0 - 2.0 * alpha) * log_emp |
|
|
| def _num_people(self, tag_idxs: Set[int]) -> int: |
| n = 0 |
| for idx in tag_idxs: |
| tag = self.vocab.tags[idx] |
| if tag == "solo": |
| n = max(n, 1) |
| elif tag in ("1girl", "1boy", "1other"): |
| n = max(n, 1) |
| elif tag in ("2girls", "2boys", "2others"): |
| n = max(n, 2) |
| elif tag in ("3girls", "3boys", "3others"): |
| n = max(n, 3) |
| elif tag in ("4girls", "4boys", "4others"): |
| n = max(n, 4) |
| elif tag in ("5girls", "5boys", "5others"): |
| n = max(n, 5) |
| elif tag in ( |
| "6+girls", "6+boys", "6+others", |
| "multiple_girls", "multiple_boys", "multiple_others", |
| ): |
| n = max(n, 2) |
| return max(n, 1) |
|
|
| def generate( |
| self, |
| alpha: float, |
| count: int, |
| length: int = 30, |
| anchor: Optional[List[str]] = None, |
| blacklist: Optional[List[str]] = None, |
| min_prob: float = 0.0, |
| temperature: float = 1.0, |
| top_k: int = 0, |
| top_p: float = 1.0, |
| rating: Optional[str] = None, |
| ) -> List[List[str]]: |
| anchor = anchor or [] |
| anchor_indices = [self.vocab.tag_to_idx[t] for t in anchor if t in self.vocab.tag_to_idx] |
| blacklist = blacklist or [] |
| blacklist_indices = {self.vocab.tag_to_idx[t] for t in blacklist if t in self.vocab.tag_to_idx} |
| rating_token = _RATING_TOKENS.get((rating or "g").lower(), RATING_G) |
|
|
| target_bias = torch.tensor( |
| [self._target_log_prob(i, alpha) for i in range(self._n_real)], |
| dtype=torch.float32, |
| device=self.device, |
| ) |
|
|
| results: List[List[str]] = [] |
| with torch.no_grad(): |
| for _ in range(count): |
| prompt_tokens: List[int] = [BOS_ID, rating_token] + [idx + OFFSET for idx in anchor_indices] |
| present_tag_idxs: Set[int] = set(anchor_indices) |
| excluded_tag_idxs: Set[int] = set(blacklist_indices) |
| group_counts: Dict[int, int] = {} |
| for idx in anchor_indices: |
| excluded_tag_idxs.update(self.graph.mutex[idx]) |
| gid = self._group_ids[idx] |
| if gid >= 0: |
| group_counts[gid] = group_counts.get(gid, 0) + 1 |
|
|
| target_len = max(length, len(anchor_indices)) |
| max_len = getattr(self.model, "max_len", 256) |
| if target_len > max_len - 2: |
| target_len = max_len - 2 |
|
|
| while len(prompt_tokens) - 1 < target_len: |
| input_ids = torch.tensor([prompt_tokens], dtype=torch.long, device=self.device) |
| logits = self.model(input_ids)[:, -1, :] |
|
|
| model_probs = torch.softmax(logits / max(temperature, 1e-6), dim=-1).squeeze(0) |
| allowed = model_probs >= min_prob |
|
|
| real_allowed = allowed[self._real_token_ids] |
| if not real_allowed.any(): |
| k = min(self.fallback_top_k, self._n_real) |
| topk = torch.topk(model_probs[self._real_token_ids], k=k).indices |
| real_allowed = torch.zeros(self._n_real, dtype=torch.bool, device=self.device) |
| real_allowed[topk] = True |
| allowed = allowed.clone() |
| allowed[self._real_token_ids] = real_allowed |
|
|
| max_people = self._num_people(present_tag_idxs) |
| full_group_ids = [gid for gid, c in group_counts.items() if c >= max_people] |
|
|
| biased_logits = logits.squeeze(0).clone() |
| biased_logits[self._real_token_ids] += self.distribution_weight * target_bias |
|
|
| biased_logits[~allowed] = -float("inf") |
| for tid in self._special_ids: |
| biased_logits[tid] = -float("inf") |
| for idx in present_tag_idxs: |
| biased_logits[idx + OFFSET] = -float("inf") |
| for idx in excluded_tag_idxs: |
| biased_logits[idx + OFFSET] = -float("inf") |
| if full_group_ids: |
| full_groups_tensor = torch.tensor(full_group_ids, dtype=torch.int32, device=self.device) |
| group_full_mask = torch.isin(self._group_ids_tensor, full_groups_tensor) |
| biased_logits[self._real_token_ids[group_full_mask]] = -float("inf") |
|
|
| probs = torch.softmax(biased_logits / max(temperature, 1e-6), dim=0) |
| probs = _apply_top_k_top_p(probs, top_k, top_p) |
| if not torch.isfinite(probs).all() or probs.sum() <= 0: |
| break |
| probs = probs / probs.sum() |
| probs = probs.cpu().numpy() |
| probs = probs / probs.sum() |
|
|
| token_id = int(self.rng.choice(len(probs), p=probs)) |
| if token_id in self._special_ids: |
| break |
| tag_idx = token_id - OFFSET |
| if tag_idx < 0 or tag_idx >= self._n_real: |
| break |
| if tag_idx in present_tag_idxs or tag_idx in excluded_tag_idxs: |
| break |
|
|
| prompt_tokens.append(token_id) |
| present_tag_idxs.add(tag_idx) |
| excluded_tag_idxs.update(self.graph.mutex[tag_idx]) |
| gid = self._group_ids[tag_idx] |
| if gid >= 0: |
| group_counts[gid] = group_counts.get(gid, 0) + 1 |
|
|
| results.append([ |
| self.vocab.tags[i - OFFSET] |
| for i in prompt_tokens |
| if i >= OFFSET |
| ]) |
|
|
| return results |
|
|