| """Keyword-based text encoder for conditioning the diffusion model.""" |
|
|
| import csv |
| from pathlib import Path |
|
|
| import torch |
| import torch.nn as nn |
|
|
|
|
| def build_vocab(metadata_csv: Path, min_count: int = 5) -> list[str]: |
| """Build vocabulary from metadata CSV keywords, filtering rare tokens.""" |
| counts: dict[str, int] = {} |
| with open(metadata_csv) as f: |
| reader = csv.DictReader(f) |
| for row in reader: |
| for kw in row["keywords"].split(","): |
| kw = kw.strip().lower() |
| if kw: |
| counts[kw] = counts.get(kw, 0) + 1 |
|
|
| vocab = sorted(kw for kw, c in counts.items() if c >= min_count) |
| return vocab |
|
|
|
|
| class KeywordEncoder(nn.Module): |
| """Encodes a set of keyword indices into a fixed-size conditioning vector.""" |
|
|
| def __init__( |
| self, |
| vocab_size: int, |
| embed_dim: int = 64, |
| cond_dim: int = 256, |
| ) -> None: |
| super().__init__() |
| self.embedding = nn.Embedding(vocab_size, embed_dim) |
| self.proj = nn.Linear(embed_dim, cond_dim) |
| self.null_embedding = nn.Parameter(torch.randn(cond_dim)) |
|
|
| def forward(self, token_ids: list[list[int]], device: torch.device) -> torch.Tensor: |
| """Encode batch of keyword lists into conditioning vectors. |
| |
| Args: |
| token_ids: List of lists of vocab indices per sample. |
| device: Target device. |
| |
| Returns: |
| (batch, cond_dim) conditioning tensor. |
| """ |
| batch_embs = [] |
| for ids in token_ids: |
| if len(ids) == 0: |
| batch_embs.append(self.null_embedding) |
| else: |
| idx = torch.tensor(ids, device=device) |
| emb = self.embedding(idx).mean(dim=0) |
| batch_embs.append(self.proj(emb)) |
| return torch.stack(batch_embs) |
|
|