File size: 7,920 Bytes
dd1362a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# bora_dataset.py -- dataset for the Bora latent shards (latents_256/ on Ami1ne/Bora)
#
# mode="clip":  whole clip, [16,10,32,32]
# mode="block": block_len latent frames, [16,block_len,32,32]; block starts step by block_len - overlap (0, 3, 6 for 4/1)
# Latents are already normalized. Decode with vae.decode(z * ds.latents_std + ds.latents_mean).
#
# Manifest: latents_256_manifest_labeled.csv (one row per unique clip, written by cell 6 of bora_load_latents.ipynb)
#   global_idx, latent_file, latent_row, clip_path, source_file, clip_idx, source, category, n_copies, caption_ids
#   caption_ids is a JSON list of indices into captions.json; index 0 is the empty caption "" (for classifier-free guidance).
# Falls back to latents_256_manifest.csv / latents_256_manifests/part_*.csv (no captions, duplicates kept) if the labeled one is absent.
#
# Validation: a whole source video is held out when crc32(source_file) % val_mod == 0 (val_mod=100 -> about 1% of the videos).
#
# Captions per clip are ordered most reliable first; an item takes caption 0 with probability primary_p, else a uniform pick.
#
# text_embeds: optional path to a safetensors file with "emb" [N_captions, L, D] float16 and "mask" [N_captions, L] bool
# (written by cell 9). The item then carries text_emb [L,D] and text_mask [L] for its chosen caption.
import json
import zlib
from pathlib import Path

import numpy as np
import pandas as pd
import torch
from safetensors import safe_open
from torch.utils.data import Dataset

LABELED = "latents_256_manifest_labeled.csv"
MERGED = "latents_256_manifest.csv"
PARTS_DIR = "latents_256_manifests"
CAPTIONS = "captions.json"


def source_key(df):
    """A string per row that identifies the source video (the split must keep every clip of a video on one side)."""
    if "source_file" in df.columns:
        return df["source_file"]
    # clip_path looks like clips_5s/shard_00000/0000000__pexels-25560546__c0000.mp4 -> pexels-25560546
    return df["clip_path"].str.rsplit("/", n=1).str[-1].str.split("__").str[1]


def is_val(source, val_mod):
    return np.fromiter((zlib.crc32(s.encode()) % val_mod == 0 for s in source), dtype=bool, count=len(source))


class BoraLatents(Dataset):
    def __init__(self, root, split="train", mode="block", block_len=4, overlap=1, random_block=True,

                 dtype=torch.float32, val_mod=100, manifest=None, captions_file=None, text_embeds=None,

                 random_caption=None, primary_p=0.5):
        assert mode in ("clip", "block") and split in ("train", "val") and 0 <= overlap < block_len
        root = Path(root)
        self.root, self.mode, self.dtype = root, mode, dtype
        self.block_len, self.random_block = block_len, random_block
        self.random_caption = random_block if random_caption is None else random_caption
        self.primary_p = primary_p  # chance of taking the first (most reliable) caption; otherwise uniform over all of them

        # ---- manifest ----
        if manifest is not None:
            manifest = Path(manifest)
        elif (root / LABELED).exists():
            manifest = root / LABELED
        elif (root / MERGED).exists():
            manifest = root / MERGED
        if manifest is not None:
            df = pd.read_csv(manifest, dtype=str, keep_default_na=False)
            self.manifest_source = manifest.name
        else:  # per-shard ledger
            parts = sorted((root / PARTS_DIR).glob("part_*.csv"))
            assert parts, f"no manifest found under {root}"
            df = pd.concat([pd.read_csv(p, dtype=str, keep_default_na=False) for p in parts], ignore_index=True)
            self.manifest_source = "part_*.csv"
        df["_g"] = df["global_idx"].astype(int)
        df["latent_row"] = df["latent_row"].astype(int)
        df = df.sort_values("_g").reset_index(drop=True)
        self.manifest = df
        self.labeled = "caption_ids" in df.columns

        # ---- captions ----
        self.captions = None
        if self.labeled:
            cf = Path(captions_file) if captions_file else (manifest.parent / CAPTIONS)
            assert cf.exists(), f"{cf} not found (it is written next to the labeled manifest)"
            self.captions = json.load(open(cf, encoding="utf-8"))
            assert self.captions[0] == "", "captions.json must start with the empty caption"

        # ---- split by source video ----
        val = is_val(source_key(df), val_mod)
        sub = (df[val] if split == "val" else df[~val]).reset_index(drop=True)
        assert len(sub), f"empty {split} split"
        self.n_videos = int(source_key(sub).nunique())

        # ---- shards ----
        self._handles = {}  # opened lazily, so every DataLoader worker owns its own handles
        first = sub["latent_file"].iloc[0]
        self.latents_mean, self.latents_std = self._read_stats(first)
        _, self.C, self.T, self.H, self.W = self._open(first).get_slice("latents").get_shape()
        self.starts = list(range(0, self.T - block_len + 1, block_len - overlap))

        if mode == "block" and not random_block:  # eval: every block of every clip, in order
            sub = sub.loc[sub.index.repeat(len(self.starts))].reset_index(drop=True)
            self._bidx = [i % len(self.starts) for i in range(len(sub))]
        self._file = sub["latent_file"].tolist()
        self._row = sub["latent_row"].tolist()
        self._gidx = sub["_g"].tolist()
        self._cids = [json.loads(s) for s in sub["caption_ids"]] if self.labeled else None

        # ---- precomputed text embeddings ----
        self.text_embeds = Path(text_embeds) if text_embeds else None
        self._emb = None
        if self.text_embeds is not None:
            assert self.labeled, "text_embeds needs the labeled manifest"
            with safe_open(str(self.text_embeds), framework="pt") as f:
                n, self.L, self.D = f.get_slice("emb").get_shape()
            assert n == len(self.captions), f"{self.text_embeds} has {n} rows, captions.json has {len(self.captions)}"

    def _open(self, rel):
        h = self._handles.get(rel)
        if h is None:
            h = self._handles[rel] = safe_open(str(self.root / rel), framework="pt")
        return h

    def _read_stats(self, rel):
        meta = self._open(rel).metadata()
        shape = (1, -1, 1, 1, 1)
        return (torch.tensor(json.loads(meta["latents_mean"])).view(shape),
                torch.tensor(json.loads(meta["latents_std"])).view(shape))

    def __len__(self):
        return len(self._file)

    def __getitem__(self, i):
        row = self._row[i]
        z = self._open(self._file[i]).get_slice("latents")[row:row + 1][0]  # [16,10,32,32] float16, only this row is read
        item = {"global_idx": self._gidx[i]}
        if self.mode == "block":
            b = int(torch.randint(len(self.starts), (1,))) if self.random_block else self._bidx[i]
            s = self.starts[b]
            z = z[:, s:s + self.block_len]
            item["block_index"], item["start"] = b, s
        item["latents"] = z.to(self.dtype)
        if self._cids is not None:
            cids = self._cids[i]
            if self.random_caption and len(cids) > 1 and torch.rand(()) >= self.primary_p:
                cid = cids[int(torch.randint(len(cids), (1,)))]
            else:
                cid = cids[0]
            item["caption_id"] = cid
            item["text"] = self.captions[cid]
            if self.text_embeds is not None:
                if self._emb is None:
                    self._emb = safe_open(str(self.text_embeds), framework="pt")
                item["text_emb"] = self._emb.get_slice("emb")[cid:cid + 1][0]    # [L,D] float16
                item["text_mask"] = self._emb.get_slice("mask")[cid:cid + 1][0]  # [L] bool
        return item