Atlas Ai commited on
Upload evaluated Quasar weights with portable inference and pinned codecs
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +62 -24
- codecs/audio/OFFICIAL_LICENSE +21 -0
- codecs/audio/OFFICIAL_WEIGHTS_TERMS.md +189 -0
- codecs/audio/README.md +199 -0
- codecs/audio/config.json +31 -0
- codecs/audio/model.safetensors +3 -0
- codecs/vision/LICENSE +201 -0
- codecs/vision/README.md +92 -0
- codecs/vision/config.json +31 -0
- codecs/vision/configuration_emu3visionvq.py +106 -0
- codecs/vision/image_processing_emu3visionvq.py +442 -0
- codecs/vision/model.safetensors +3 -0
- codecs/vision/modeling_emu3visionvq.py +822 -0
- codecs/vision/preprocessor_config.json +29 -0
- config.json +174 -0
- files.sha256.json +302 -0
- infer.py +60 -0
- model.safetensors +3 -0
- pyproject.toml +24 -0
- quasar/__init__.py +0 -0
- quasar/data/__init__.py +0 -0
- quasar/data/adapters.py +87 -0
- quasar/data/collator.py +35 -0
- quasar/data/curated.py +195 -0
- quasar/data/datasets.py +249 -0
- quasar/data/download.py +180 -0
- quasar/data/preprocessing.py +250 -0
- quasar/data/synthetic.py +383 -0
- quasar/data/text_preparation.py +514 -0
- quasar/evaluation/__init__.py +0 -0
- quasar/inference/__init__.py +0 -0
- quasar/inference/decoding.py +163 -0
- quasar/inference/extended.py +148 -0
- quasar/inference/generate.py +285 -0
- quasar/inference/universal.py +103 -0
- quasar/model/__init__.py +0 -0
- quasar/model/attention.py +58 -0
- quasar/model/axiom.py +124 -0
- quasar/model/config.py +73 -0
- quasar/model/layers.py +42 -0
- quasar/model/transformer.py +119 -0
- quasar/tokenizers/__init__.py +0 -0
- quasar/tokenizers/audio.py +48 -0
- quasar/tokenizers/audio_aligned.py +35 -0
- quasar/tokenizers/audio_complete.py +23 -0
- quasar/tokenizers/mesh.py +35 -0
- quasar/tokenizers/multimodal.py +197 -0
- quasar/tokenizers/multires_emu.py +49 -0
- quasar/tokenizers/restricted_emu.py +52 -0
- quasar/tokenizers/text.py +47 -0
README.md
CHANGED
|
@@ -31,10 +31,12 @@ hybrid recurrence/attention backbone. Text, visual codes, audio codes, triangle
|
|
| 31 |
meshes and tool-call proposals share one vocabulary and model. Frozen codecs
|
| 32 |
convert between media and discrete tokens.
|
| 33 |
|
| 34 |
-
**Release status:
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
experiments are still being evaluated and are excluded from these results.
|
| 39 |
|
| 40 |
**Quality status:** a focused research branch produces recognizable images and
|
|
@@ -71,13 +73,14 @@ excludes that auxiliary term.
|
|
| 71 |
|
| 72 |
## Checkpoint identity
|
| 73 |
|
| 74 |
-
The
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
| Artifact | Role | SHA-256 |
|
| 78 |
|---|---|---|
|
| 79 |
-
| v5
|
| 80 |
-
| `
|
| 81 |
|
| 82 |
## Intended use and limitations
|
| 83 |
|
|
@@ -218,23 +221,39 @@ research session and is **not generated-image quality**. The earlier 64px
|
|
| 218 |
real-photo probe measured 18.1176 dB; its original aspirational 20 dB gate remained
|
| 219 |
false and was not relabeled as passed.
|
| 220 |
|
| 221 |
-
##
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
```python
|
| 232 |
from PIL import Image
|
| 233 |
from quasar.inference.extended import ExtendedGenerator
|
| 234 |
from quasar.inference.decoding import Sampling
|
| 235 |
|
| 236 |
-
#
|
| 237 |
-
generator = ExtendedGenerator("
|
| 238 |
sampling = Sampling(temperature=0.5, top_k=64, top_p=0.9, seed=20261021)
|
| 239 |
pixels = generator.generate_image("make a picture of a cat", sampling, resolution=128)
|
| 240 |
Image.fromarray(pixels).save("quasar_cat.png")
|
|
@@ -243,15 +262,30 @@ frames = generator.generate_video(
|
|
| 243 |
)
|
| 244 |
```
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
## Licensing and provenance
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
| 255 |
|
| 256 |
The existing frozen Emu3 visual tokenizer is pinned at revision
|
| 257 |
`c81f916ad371289e205310a7539255e8a9396488` under Apache-2.0. The local Descript DAC
|
|
@@ -261,6 +295,10 @@ Quasar weights license. Dataset, underlying page, photograph and per-object term
|
|
| 261 |
remain separate; for example, the FineWeb database license does not replace
|
| 262 |
underlying web-page rights.
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
## Reproducibility and next work
|
| 265 |
|
| 266 |
The completed R&D snapshot passed 146 CPU tests with 1 skip; the legacy integration
|
|
|
|
| 31 |
meshes and tool-call proposals share one vocabulary and model. Frozen codecs
|
| 32 |
convert between media and discrete tokens.
|
| 33 |
|
| 34 |
+
**Release status: private, runnable research checkpoint release.** The default
|
| 35 |
+
root weights are the completed curriculum-research checkpoint shown below; the
|
| 36 |
+
preserved v5 comparison weights are in `variants/v5`. Matching source code,
|
| 37 |
+
text tokenizers and pinned frozen visual/audio codecs are included. Training
|
| 38 |
+
datasets, optimizer states and an inference service are not included.
|
| 39 |
+
This card describes these two snapshots as of **21 October 2026**. Separate architecture
|
| 40 |
experiments are still being evaluated and are excluded from these results.
|
| 41 |
|
| 42 |
**Quality status:** a focused research branch produces recognizable images and
|
|
|
|
| 73 |
|
| 74 |
## Checkpoint identity
|
| 75 |
|
| 76 |
+
The downloadable weights match the exact artifacts measured below. SHA-256 values
|
| 77 |
+
identify each unchanged `model.safetensors` file; packaging only makes metadata
|
| 78 |
+
paths portable and supplies the matching runtime.
|
| 79 |
|
| 80 |
| Artifact | Role | SHA-256 |
|
| 81 |
|---|---|---|
|
| 82 |
+
| `variants/v5/model.safetensors` | Preserved v5 comparison model | `e20242e9971b6a7cef7ca23a65c25faa70a007807a55a2c0cfe35f4f5682a643` |
|
| 83 |
+
| Root `model.safetensors` | Unpromoted `joint_repair/final` research model | `de4846e5e63c0004cf0b6331127ab421d6843f6efde04243c6d5448ab4545240` |
|
| 84 |
|
| 85 |
## Intended use and limitations
|
| 86 |
|
|
|
|
| 221 |
real-photo probe measured 18.1176 dB; its original aspirational 20 dB gate remained
|
| 222 |
false and was not relabeled as passed.
|
| 223 |
|
| 224 |
+
## Download and run
|
| 225 |
+
|
| 226 |
+
This is a custom PyTorch checkpoint package, not a Transformers `AutoModel`.
|
| 227 |
+
Use the included `quasar` implementation rather than
|
| 228 |
+
`AutoModel.from_pretrained("Atlas-AI-research/Quasar")`. Authentication is needed
|
| 229 |
+
while the repository is private. Preserve a working CUDA-compatible PyTorch
|
| 230 |
+
installation; the measured environment used PyTorch 2.11.0+cu128. Install a suitable
|
| 231 |
+
PyTorch build separately when needed, then install the remaining inference dependencies:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
hf auth login
|
| 235 |
+
hf download Atlas-AI-research/Quasar --local-dir Quasar
|
| 236 |
+
cd Quasar
|
| 237 |
+
python -m pip install -r requirements-inference.txt
|
| 238 |
+
python verify_release.py --codecs
|
| 239 |
+
python infer.py --task image --prompt "make a picture of a cat" --output cat.png
|
| 240 |
+
python infer.py --task video --prompt "a small cyan square moving left on a gray background" --output motion.gif
|
| 241 |
+
```
|
| 242 |
|
| 243 |
+
The default inference device is CUDA; the CLI refuses a busy GPU. Verification
|
| 244 |
+
uses CPU only. CPU inference is available with `--device cpu`; it does not train
|
| 245 |
+
the model. Select the preserved baseline with `--checkpoint variants/v5`.
|
| 246 |
+
CPU-only entry points hide CUDA and exclude the unused optional TorchAO quantizer
|
| 247 |
+
within their process, avoiding GPU probing by an unrelated installed backend.
|
| 248 |
+
The included package can also be imported directly from the downloaded directory:
|
| 249 |
|
| 250 |
```python
|
| 251 |
from PIL import Image
|
| 252 |
from quasar.inference.extended import ExtendedGenerator
|
| 253 |
from quasar.inference.decoding import Sampling
|
| 254 |
|
| 255 |
+
# Root contains the exact research model shown in this card.
|
| 256 |
+
generator = ExtendedGenerator(".", "cuda")
|
| 257 |
sampling = Sampling(temperature=0.5, top_k=64, top_p=0.9, seed=20261021)
|
| 258 |
pixels = generator.generate_image("make a picture of a cat", sampling, resolution=128)
|
| 259 |
Image.fromarray(pixels).save("quasar_cat.png")
|
|
|
|
| 262 |
)
|
| 263 |
```
|
| 264 |
|
| 265 |
+
These examples do not establish acceptable output for arbitrary prompts. GPU use
|
| 266 |
+
must be exclusive with local training/evaluation workers. No inference server is
|
| 267 |
+
deployed. About 1.96 GiB of model/package files include both 361 MB Quasar weight
|
| 268 |
+
files and one shared copy of each frozen codec; training data and optimizer states
|
| 269 |
+
are omitted. The image/code mappings and original trained weights are unchanged.
|
| 270 |
+
|
| 271 |
+
The runtime is frozen from source commit
|
| 272 |
+
`4afbdccdbd3b0967b161a68658cfd3b92c42636f`, before the separate architecture
|
| 273 |
+
experiments. Two packaging-only edits resolve visual/audio codec paths relative
|
| 274 |
+
to the downloaded checkpoint. The included pinned visual codec Python is loaded
|
| 275 |
+
locally with `trust_remote_code=True`; review it as executable source. Inference
|
| 276 |
+
does not fetch additional model code or weights. Source identities and file hashes
|
| 277 |
+
are in [release.json](release.json) and [files.sha256.json](files.sha256.json).
|
| 278 |
+
The packaged release passed CPU finite-forward/parameter/hash checks for both
|
| 279 |
+
checkpoints and CPU encode/decode checks for both pinned codecs; see
|
| 280 |
+
[release_validation.json](release_validation.json). This verification does not
|
| 281 |
+
claim new semantic quality or replace the held-out results above.
|
| 282 |
|
| 283 |
## Licensing and provenance
|
| 284 |
|
| 285 |
+
This private upload does not select a distribution license for Quasar's code or
|
| 286 |
+
weights. No permissive license badge is implied by using PyTorch or permissively
|
| 287 |
+
licensed components. Component notices remain separate and are included with
|
| 288 |
+
their codec files; this upload does not make the repository public.
|
| 289 |
|
| 290 |
The existing frozen Emu3 visual tokenizer is pinned at revision
|
| 291 |
`c81f916ad371289e205310a7539255e8a9396488` under Apache-2.0. The local Descript DAC
|
|
|
|
| 295 |
remain separate; for example, the FineWeb database license does not replace
|
| 296 |
underlying web-page rights.
|
| 297 |
|
| 298 |
+
The visual component retains upstream copyright headers and Apache-2.0 terms in
|
| 299 |
+
`codecs/vision/LICENSE`. The audio component retains the official MIT license and
|
| 300 |
+
the pinned upstream weights statement under `codecs/audio`.
|
| 301 |
+
|
| 302 |
## Reproducibility and next work
|
| 303 |
|
| 304 |
The completed R&D snapshot passed 146 CPU tests with 1 skip; the legacy integration
|
codecs/audio/OFFICIAL_LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023-present, Descript
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
codecs/audio/OFFICIAL_WEIGHTS_TERMS.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Descript Audio Codec (.dac): High-Fidelity Audio Compression with Improved RVQGAN
|
| 2 |
+
|
| 3 |
+
This repository contains training and inference scripts
|
| 4 |
+
for the Descript Audio Codec (.dac), a high fidelity general
|
| 5 |
+
neural audio codec, introduced in the paper titled **High-Fidelity Audio Compression with Improved RVQGAN**.
|
| 6 |
+
|
| 7 |
+
 [arXiv Paper: High-Fidelity Audio Compression with Improved RVQGAN
|
| 8 |
+
](http://arxiv.org/abs/2306.06546) <br>
|
| 9 |
+
📈 [Demo Site](https://descript.notion.site/Descript-Audio-Codec-11389fce0ce2419891d6591a68f814d5)<br>
|
| 10 |
+
⚙ [Model Weights](https://github.com/descriptinc/descript-audio-codec/releases/download/0.0.1/weights.pth)
|
| 11 |
+
|
| 12 |
+
👉 With Descript Audio Codec, you can compress **44.1 KHz audio** into discrete codes at a **low 8 kbps bitrate**. <br>
|
| 13 |
+
🤌 That's approximately **90x compression** while maintaining exceptional fidelity and minimizing artifacts. <br>
|
| 14 |
+
💪 Our universal model works on all domains (speech, environment, music, etc.), making it widely applicable to generative modeling of all audio. <br>
|
| 15 |
+
👌 It can be used as a drop-in replacement for EnCodec for all audio language modeling applications (such as AudioLMs, MusicLMs, MusicGen, etc.) <br>
|
| 16 |
+
|
| 17 |
+
<p align="center">
|
| 18 |
+
<img src="./assets/comparsion_stats.png" alt="Comparison of compressions approaches. Our model achieves a higher compression factor compared to all baseline methods. Our model has a ~90x compression factor compared to 32x compression factor of EnCodec and 64x of SoundStream. Note that we operate at a target bitrate of 8 kbps, whereas EnCodec operates at 24 kbps and SoundStream at 6 kbps. We also operate at 44.1 kHz, whereas EnCodec operates at 48 kHz and SoundStream operates at 24 kHz." width=35%></p>
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
## Usage
|
| 22 |
+
|
| 23 |
+
### Installation
|
| 24 |
+
```
|
| 25 |
+
pip install descript-audio-codec
|
| 26 |
+
```
|
| 27 |
+
OR
|
| 28 |
+
|
| 29 |
+
```
|
| 30 |
+
pip install git+https://github.com/descriptinc/descript-audio-codec
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### Weights
|
| 34 |
+
Weights are released as part of this repo under MIT license.
|
| 35 |
+
We release weights for models that can natively support 16 kHz, 24kHz, and 44.1kHz sampling rates.
|
| 36 |
+
Weights are automatically downloaded when you first run `encode` or `decode` command. You can cache them using one of the following commands
|
| 37 |
+
```bash
|
| 38 |
+
python3 -m dac download # downloads the default 44kHz variant
|
| 39 |
+
python3 -m dac download --model_type 44khz # downloads the 44kHz variant
|
| 40 |
+
python3 -m dac download --model_type 24khz # downloads the 24kHz variant
|
| 41 |
+
python3 -m dac download --model_type 16khz # downloads the 16kHz variant
|
| 42 |
+
```
|
| 43 |
+
We provide a Dockerfile that installs all required dependencies for encoding and decoding. The build process caches the default model weights inside the image. This allows the image to be used without an internet connection. [Please refer to instructions below.](#docker-image)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
### Compress audio
|
| 47 |
+
```
|
| 48 |
+
python3 -m dac encode /path/to/input --output /path/to/output/codes
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
This command will create `.dac` files with the same name as the input files.
|
| 52 |
+
It will also preserve the directory structure relative to input root and
|
| 53 |
+
re-create it in the output directory. Please use `python -m dac encode --help`
|
| 54 |
+
for more options.
|
| 55 |
+
|
| 56 |
+
### Reconstruct audio from compressed codes
|
| 57 |
+
```
|
| 58 |
+
python3 -m dac decode /path/to/output/codes --output /path/to/reconstructed_input
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
This command will create `.wav` files with the same name as the input files.
|
| 62 |
+
It will also preserve the directory structure relative to input root and
|
| 63 |
+
re-create it in the output directory. Please use `python -m dac decode --help`
|
| 64 |
+
for more options.
|
| 65 |
+
|
| 66 |
+
### Programmatic Usage
|
| 67 |
+
```py
|
| 68 |
+
import dac
|
| 69 |
+
from audiotools import AudioSignal
|
| 70 |
+
|
| 71 |
+
# Download a model
|
| 72 |
+
model_path = dac.utils.download(model_type="44khz")
|
| 73 |
+
model = dac.DAC.load(model_path)
|
| 74 |
+
|
| 75 |
+
model.to('cuda')
|
| 76 |
+
|
| 77 |
+
# Load audio signal file
|
| 78 |
+
signal = AudioSignal('input.wav')
|
| 79 |
+
|
| 80 |
+
# Encode audio signal as one long file
|
| 81 |
+
# (may run out of GPU memory on long files)
|
| 82 |
+
signal.to(model.device)
|
| 83 |
+
|
| 84 |
+
x = model.preprocess(signal.audio_data, signal.sample_rate)
|
| 85 |
+
z, codes, latents, _, _ = model.encode(x)
|
| 86 |
+
|
| 87 |
+
# Decode audio signal
|
| 88 |
+
y = model.decode(z)
|
| 89 |
+
|
| 90 |
+
# Alternatively, use the `compress` and `decompress` functions
|
| 91 |
+
# to compress long files.
|
| 92 |
+
|
| 93 |
+
signal = signal.cpu()
|
| 94 |
+
x = model.compress(signal)
|
| 95 |
+
|
| 96 |
+
# Save and load to and from disk
|
| 97 |
+
x.save("compressed.dac")
|
| 98 |
+
x = dac.DACFile.load("compressed.dac")
|
| 99 |
+
|
| 100 |
+
# Decompress it back to an AudioSignal
|
| 101 |
+
y = model.decompress(x)
|
| 102 |
+
|
| 103 |
+
# Write to file
|
| 104 |
+
y.write('output.wav')
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Docker image
|
| 108 |
+
We provide a dockerfile to build a docker image with all the necessary
|
| 109 |
+
dependencies.
|
| 110 |
+
1. Building the image.
|
| 111 |
+
```
|
| 112 |
+
docker build -t dac .
|
| 113 |
+
```
|
| 114 |
+
2. Using the image.
|
| 115 |
+
|
| 116 |
+
Usage on CPU:
|
| 117 |
+
```
|
| 118 |
+
docker run dac <command>
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Usage on GPU:
|
| 122 |
+
```
|
| 123 |
+
docker run --gpus=all dac <command>
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
`<command>` can be one of the compression and reconstruction commands listed
|
| 127 |
+
above. For example, if you want to run compression,
|
| 128 |
+
|
| 129 |
+
```
|
| 130 |
+
docker run --gpus=all dac python3 -m dac encode ...
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
## Training
|
| 135 |
+
The baseline model configuration can be trained using the following commands.
|
| 136 |
+
|
| 137 |
+
### Pre-requisites
|
| 138 |
+
Please install the correct dependencies
|
| 139 |
+
```
|
| 140 |
+
pip install -e ".[dev]"
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
## Environment setup
|
| 144 |
+
|
| 145 |
+
We have provided a Dockerfile and docker compose setup that makes running experiments easy.
|
| 146 |
+
|
| 147 |
+
To build the docker image do:
|
| 148 |
+
|
| 149 |
+
```
|
| 150 |
+
docker compose build
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
Then, to launch a container, do:
|
| 154 |
+
|
| 155 |
+
```
|
| 156 |
+
docker compose run -p 8888:8888 -p 6006:6006 dev
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
The port arguments (`-p`) are optional, but useful if you want to launch a Jupyter and Tensorboard instances within the container. The
|
| 160 |
+
default password for Jupyter is `password`, and the current directory
|
| 161 |
+
is mounted to `/u/home/src`, which also becomes the working directory.
|
| 162 |
+
|
| 163 |
+
Then, run your training command.
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
### Single GPU training
|
| 167 |
+
```
|
| 168 |
+
export CUDA_VISIBLE_DEVICES=0
|
| 169 |
+
python scripts/train.py --args.load conf/ablations/baseline.yml --save_path runs/baseline/
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### Multi GPU training
|
| 173 |
+
```
|
| 174 |
+
export CUDA_VISIBLE_DEVICES=0,1
|
| 175 |
+
torchrun --nproc_per_node gpu scripts/train.py --args.load conf/ablations/baseline.yml --save_path runs/baseline/
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
## Testing
|
| 179 |
+
We provide two test scripts to test CLI + training functionality. Please
|
| 180 |
+
make sure that the trainig pre-requisites are satisfied before launching these
|
| 181 |
+
tests. To launch these tests please run
|
| 182 |
+
```
|
| 183 |
+
python -m pytest tests
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
## Results
|
| 187 |
+
|
| 188 |
+
<p align="left">
|
| 189 |
+
<img src="./assets/objective_comparisons.png" width=75%></p>
|
codecs/audio/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
pipeline_tag: feature-extraction
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Model Card for Model ID
|
| 7 |
+
|
| 8 |
+
<!-- Provide a quick summary of what the model is/does. -->
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
## Model Details
|
| 13 |
+
|
| 14 |
+
### Model Description
|
| 15 |
+
|
| 16 |
+
<!-- Provide a longer summary of what this model is. -->
|
| 17 |
+
|
| 18 |
+
This is the model card of a 🤗 transformers model that has been pushed on the Hub. This model card has been automatically generated.
|
| 19 |
+
|
| 20 |
+
- **Developed by:** [More Information Needed]
|
| 21 |
+
- **Funded by [optional]:** [More Information Needed]
|
| 22 |
+
- **Shared by [optional]:** [More Information Needed]
|
| 23 |
+
- **Model type:** [More Information Needed]
|
| 24 |
+
- **Language(s) (NLP):** [More Information Needed]
|
| 25 |
+
- **License:** [More Information Needed]
|
| 26 |
+
- **Finetuned from model [optional]:** [More Information Needed]
|
| 27 |
+
|
| 28 |
+
### Model Sources [optional]
|
| 29 |
+
|
| 30 |
+
<!-- Provide the basic links for the model. -->
|
| 31 |
+
|
| 32 |
+
- **Repository:** [More Information Needed]
|
| 33 |
+
- **Paper [optional]:** [More Information Needed]
|
| 34 |
+
- **Demo [optional]:** [More Information Needed]
|
| 35 |
+
|
| 36 |
+
## Uses
|
| 37 |
+
|
| 38 |
+
<!-- Address questions around how the model is intended to be used, including the foreseeable users of the model and those affected by the model. -->
|
| 39 |
+
|
| 40 |
+
### Direct Use
|
| 41 |
+
|
| 42 |
+
<!-- This section is for the model use without fine-tuning or plugging into a larger ecosystem/app. -->
|
| 43 |
+
|
| 44 |
+
[More Information Needed]
|
| 45 |
+
|
| 46 |
+
### Downstream Use [optional]
|
| 47 |
+
|
| 48 |
+
<!-- This section is for the model use when fine-tuned for a task, or when plugged into a larger ecosystem/app -->
|
| 49 |
+
|
| 50 |
+
[More Information Needed]
|
| 51 |
+
|
| 52 |
+
### Out-of-Scope Use
|
| 53 |
+
|
| 54 |
+
<!-- This section addresses misuse, malicious use, and uses that the model will not work well for. -->
|
| 55 |
+
|
| 56 |
+
[More Information Needed]
|
| 57 |
+
|
| 58 |
+
## Bias, Risks, and Limitations
|
| 59 |
+
|
| 60 |
+
<!-- This section is meant to convey both technical and sociotechnical limitations. -->
|
| 61 |
+
|
| 62 |
+
[More Information Needed]
|
| 63 |
+
|
| 64 |
+
### Recommendations
|
| 65 |
+
|
| 66 |
+
<!-- This section is meant to convey recommendations with respect to the bias, risk, and technical limitations. -->
|
| 67 |
+
|
| 68 |
+
Users (both direct and downstream) should be made aware of the risks, biases and limitations of the model. More information needed for further recommendations.
|
| 69 |
+
|
| 70 |
+
## How to Get Started with the Model
|
| 71 |
+
|
| 72 |
+
Use the code below to get started with the model.
|
| 73 |
+
|
| 74 |
+
[More Information Needed]
|
| 75 |
+
|
| 76 |
+
## Training Details
|
| 77 |
+
|
| 78 |
+
### Training Data
|
| 79 |
+
|
| 80 |
+
<!-- This should link to a Dataset Card, perhaps with a short stub of information on what the training data is all about as well as documentation related to data pre-processing or additional filtering. -->
|
| 81 |
+
|
| 82 |
+
[More Information Needed]
|
| 83 |
+
|
| 84 |
+
### Training Procedure
|
| 85 |
+
|
| 86 |
+
<!-- This relates heavily to the Technical Specifications. Content here should link to that section when it is relevant to the training procedure. -->
|
| 87 |
+
|
| 88 |
+
#### Preprocessing [optional]
|
| 89 |
+
|
| 90 |
+
[More Information Needed]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
#### Training Hyperparameters
|
| 94 |
+
|
| 95 |
+
- **Training regime:** [More Information Needed] <!--fp32, fp16 mixed precision, bf16 mixed precision, bf16 non-mixed precision, fp16 non-mixed precision, fp8 mixed precision -->
|
| 96 |
+
|
| 97 |
+
#### Speeds, Sizes, Times [optional]
|
| 98 |
+
|
| 99 |
+
<!-- This section provides information about throughput, start/end time, checkpoint size if relevant, etc. -->
|
| 100 |
+
|
| 101 |
+
[More Information Needed]
|
| 102 |
+
|
| 103 |
+
## Evaluation
|
| 104 |
+
|
| 105 |
+
<!-- This section describes the evaluation protocols and provides the results. -->
|
| 106 |
+
|
| 107 |
+
### Testing Data, Factors & Metrics
|
| 108 |
+
|
| 109 |
+
#### Testing Data
|
| 110 |
+
|
| 111 |
+
<!-- This should link to a Dataset Card if possible. -->
|
| 112 |
+
|
| 113 |
+
[More Information Needed]
|
| 114 |
+
|
| 115 |
+
#### Factors
|
| 116 |
+
|
| 117 |
+
<!-- These are the things the evaluation is disaggregating by, e.g., subpopulations or domains. -->
|
| 118 |
+
|
| 119 |
+
[More Information Needed]
|
| 120 |
+
|
| 121 |
+
#### Metrics
|
| 122 |
+
|
| 123 |
+
<!-- These are the evaluation metrics being used, ideally with a description of why. -->
|
| 124 |
+
|
| 125 |
+
[More Information Needed]
|
| 126 |
+
|
| 127 |
+
### Results
|
| 128 |
+
|
| 129 |
+
[More Information Needed]
|
| 130 |
+
|
| 131 |
+
#### Summary
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
## Model Examination [optional]
|
| 136 |
+
|
| 137 |
+
<!-- Relevant interpretability work for the model goes here -->
|
| 138 |
+
|
| 139 |
+
[More Information Needed]
|
| 140 |
+
|
| 141 |
+
## Environmental Impact
|
| 142 |
+
|
| 143 |
+
<!-- Total emissions (in grams of CO2eq) and additional considerations, such as electricity usage, go here. Edit the suggested text below accordingly -->
|
| 144 |
+
|
| 145 |
+
Carbon emissions can be estimated using the [Machine Learning Impact calculator](https://mlco2.github.io/impact#compute) presented in [Lacoste et al. (2019)](https://arxiv.org/abs/1910.09700).
|
| 146 |
+
|
| 147 |
+
- **Hardware Type:** [More Information Needed]
|
| 148 |
+
- **Hours used:** [More Information Needed]
|
| 149 |
+
- **Cloud Provider:** [More Information Needed]
|
| 150 |
+
- **Compute Region:** [More Information Needed]
|
| 151 |
+
- **Carbon Emitted:** [More Information Needed]
|
| 152 |
+
|
| 153 |
+
## Technical Specifications [optional]
|
| 154 |
+
|
| 155 |
+
### Model Architecture and Objective
|
| 156 |
+
|
| 157 |
+
[More Information Needed]
|
| 158 |
+
|
| 159 |
+
### Compute Infrastructure
|
| 160 |
+
|
| 161 |
+
[More Information Needed]
|
| 162 |
+
|
| 163 |
+
#### Hardware
|
| 164 |
+
|
| 165 |
+
[More Information Needed]
|
| 166 |
+
|
| 167 |
+
#### Software
|
| 168 |
+
|
| 169 |
+
[More Information Needed]
|
| 170 |
+
|
| 171 |
+
## Citation [optional]
|
| 172 |
+
|
| 173 |
+
<!-- If there is a paper or blog post introducing the model, the APA and Bibtex information for that should go in this section. -->
|
| 174 |
+
|
| 175 |
+
**BibTeX:**
|
| 176 |
+
|
| 177 |
+
[More Information Needed]
|
| 178 |
+
|
| 179 |
+
**APA:**
|
| 180 |
+
|
| 181 |
+
[More Information Needed]
|
| 182 |
+
|
| 183 |
+
## Glossary [optional]
|
| 184 |
+
|
| 185 |
+
<!-- If relevant, include terms and calculations in this section that can help readers understand the model or model card. -->
|
| 186 |
+
|
| 187 |
+
[More Information Needed]
|
| 188 |
+
|
| 189 |
+
## More Information [optional]
|
| 190 |
+
|
| 191 |
+
[More Information Needed]
|
| 192 |
+
|
| 193 |
+
## Model Card Authors [optional]
|
| 194 |
+
|
| 195 |
+
[More Information Needed]
|
| 196 |
+
|
| 197 |
+
## Model Card Contact
|
| 198 |
+
|
| 199 |
+
[More Information Needed]
|
codecs/audio/config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"DacModel"
|
| 4 |
+
],
|
| 5 |
+
"codebook_dim": 8,
|
| 6 |
+
"codebook_loss_weight": 1.0,
|
| 7 |
+
"codebook_size": 1024,
|
| 8 |
+
"commitment_loss_weight": 0.25,
|
| 9 |
+
"decoder_hidden_size": 1536,
|
| 10 |
+
"downsampling_ratios": [
|
| 11 |
+
2,
|
| 12 |
+
4,
|
| 13 |
+
5,
|
| 14 |
+
8
|
| 15 |
+
],
|
| 16 |
+
"encoder_hidden_size": 64,
|
| 17 |
+
"hidden_size": 1024,
|
| 18 |
+
"hop_length": 512,
|
| 19 |
+
"model_type": "dac",
|
| 20 |
+
"n_codebooks": 32,
|
| 21 |
+
"quantizer_dropout": 0.0,
|
| 22 |
+
"sampling_rate": 24000,
|
| 23 |
+
"torch_dtype": "float32",
|
| 24 |
+
"transformers_version": "4.42.0.dev0",
|
| 25 |
+
"upsampling_ratios": [
|
| 26 |
+
8,
|
| 27 |
+
5,
|
| 28 |
+
4,
|
| 29 |
+
2
|
| 30 |
+
]
|
| 31 |
+
}
|
codecs/audio/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7452e3fc6972991da871ae1a1c9d3e8e219aa247cc0f757018867d6a7fe59aaf
|
| 3 |
+
size 298652268
|
codecs/vision/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
codecs/vision/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: transformers
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
<div align='center'>
|
| 7 |
+
<h1>Emu3: Next-Token Prediction is All You Need</h1h1>
|
| 8 |
+
<h3></h3>
|
| 9 |
+
|
| 10 |
+
[Emu3 Team, BAAI](https://www.baai.ac.cn/english.html)
|
| 11 |
+
|
| 12 |
+
| [Project Page](https://emu.baai.ac.cn) | [Paper](https://huggingface.co/papers/2409.18869) | [🤗HF Models](https://huggingface.co/collections/BAAI/emu3-66f4e64f70850ff358a2e60f) | [github](https://github.com/baaivision/Emu3) | [Demo](https://huggingface.co/spaces/BAAI/Emu3) |
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<div align='center'>
|
| 18 |
+
<img src="https://github.com/baaivision/Emu3/blob/main/assets/arch.png?raw=True" class="interpolation-image" alt="arch." height="80%" width="70%" />
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
We introduce **Emu3**, a new suite of state-of-the-art multimodal models trained solely with **<i>next-token prediction</i>**! By tokenizing images, text, and videos into a discrete space, we train a single transformer from scratch on a mixture of multimodal sequences.
|
| 22 |
+
|
| 23 |
+
### Emu3 excels in both generation and perception
|
| 24 |
+
**Emu3** outperforms several well-established task-specific models in both generation and perception tasks, surpassing flagship open models such as SDXL, LLaVA-1.6 and OpenSora-1.2, while eliminating the need for diffusion or compositional architectures.
|
| 25 |
+
|
| 26 |
+
<div align='center'>
|
| 27 |
+
<img src="https://github.com/baaivision/Emu3/blob/main//assets/comparison.png?raw=True" class="interpolation-image" alt="comparison." height="80%" width="80%" />
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
### Highlights
|
| 31 |
+
|
| 32 |
+
- **Emu3** is capable of generating high-quality images following the text input, by simply predicting the next vision token. The model naturally supports flexible resolutions and styles.
|
| 33 |
+
- **Emu3** shows strong vision-language understanding capabilities to see the physical world and provides coherent text responses. Notably, this capability is achieved without depending on a CLIP and a pretrained LLM.
|
| 34 |
+
- **Emu3** simply generates a video causally by predicting the next token in a video sequence, unlike the video diffusion model as in Sora. With a video in context, Emu3 can also naturally extend the video and predict what will happen next.
|
| 35 |
+
|
| 36 |
+
### Quickstart for Autoencoding
|
| 37 |
+
```python
|
| 38 |
+
import os
|
| 39 |
+
import os.path as osp
|
| 40 |
+
|
| 41 |
+
from PIL import Image
|
| 42 |
+
import torch
|
| 43 |
+
from transformers import AutoModel, AutoImageProcessor
|
| 44 |
+
|
| 45 |
+
MODEL_HUB = "BAAI/Emu3-VisionTokenizer"
|
| 46 |
+
|
| 47 |
+
model = AutoModel.from_pretrained(MODEL_HUB, trust_remote_code=True).eval().cuda()
|
| 48 |
+
processor = AutoImageProcessor.from_pretrained(MODEL_HUB, trust_remote_code=True)
|
| 49 |
+
|
| 50 |
+
# TODO: you need to modify the path here
|
| 51 |
+
VIDEO_FRAMES_PATH = "YOUR_VIDEO_FRAMES_PATH"
|
| 52 |
+
|
| 53 |
+
video = os.listdir(VIDEO_FRAMES_PATH)
|
| 54 |
+
video.sort()
|
| 55 |
+
video = [Image.open(osp.join(VIDEO_FRAMES_PATH, v)) for v in video]
|
| 56 |
+
|
| 57 |
+
images = processor(video, return_tensors="pt")["pixel_values"]
|
| 58 |
+
images = images.unsqueeze(0).cuda()
|
| 59 |
+
|
| 60 |
+
# image autoencode
|
| 61 |
+
image = images[:, 0]
|
| 62 |
+
print(image.shape)
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
# encode
|
| 65 |
+
codes = model.encode(image)
|
| 66 |
+
# decode
|
| 67 |
+
recon = model.decode(codes)
|
| 68 |
+
|
| 69 |
+
recon = recon.view(-1, *recon.shape[2:])
|
| 70 |
+
recon_image = processor.postprocess(recon)["pixel_values"][0]
|
| 71 |
+
recon_image.save("recon_image.png")
|
| 72 |
+
|
| 73 |
+
# video autoencode
|
| 74 |
+
images = images.view(
|
| 75 |
+
-1,
|
| 76 |
+
model.config.temporal_downsample_factor,
|
| 77 |
+
*images.shape[2:],
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
print(images.shape)
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
# encode
|
| 83 |
+
codes = model.encode(images)
|
| 84 |
+
# decode
|
| 85 |
+
recon = model.decode(codes)
|
| 86 |
+
|
| 87 |
+
recon = recon.view(-1, *recon.shape[2:])
|
| 88 |
+
recon_images = processor.postprocess(recon)["pixel_values"]
|
| 89 |
+
for idx, im in enumerate(recon_images):
|
| 90 |
+
im.save(f"recon_video_{idx}.png")
|
| 91 |
+
|
| 92 |
+
```
|
codecs/vision/config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Emu3VisionVQModel"
|
| 4 |
+
],
|
| 5 |
+
"attn_resolutions": [
|
| 6 |
+
3
|
| 7 |
+
],
|
| 8 |
+
"auto_map": {
|
| 9 |
+
"AutoConfig": "configuration_emu3visionvq.Emu3VisionVQConfig",
|
| 10 |
+
"AutoModel": "modeling_emu3visionvq.Emu3VisionVQModel"
|
| 11 |
+
},
|
| 12 |
+
"ch": 256,
|
| 13 |
+
"ch_mult": [
|
| 14 |
+
1,
|
| 15 |
+
2,
|
| 16 |
+
2,
|
| 17 |
+
4
|
| 18 |
+
],
|
| 19 |
+
"codebook_size": 32768,
|
| 20 |
+
"double_z": false,
|
| 21 |
+
"dropout": 0.0,
|
| 22 |
+
"embed_dim": 4,
|
| 23 |
+
"in_channels": 3,
|
| 24 |
+
"model_type": "Emu3VisionVQ",
|
| 25 |
+
"num_res_blocks": 2,
|
| 26 |
+
"out_channels": 3,
|
| 27 |
+
"temporal_downsample_factor": 4,
|
| 28 |
+
"torch_dtype": "float32",
|
| 29 |
+
"transformers_version": "4.44.0",
|
| 30 |
+
"z_channels": 4
|
| 31 |
+
}
|
codecs/vision/configuration_emu3visionvq.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Emu team, BAAI and The HuggingFace Inc. team. All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
""" Emu3VisionVQ model configuration """
|
| 16 |
+
|
| 17 |
+
from typing import List
|
| 18 |
+
|
| 19 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 20 |
+
from transformers.utils import logging
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
logger = logging.get_logger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Emu3VisionVQConfig(PretrainedConfig):
|
| 27 |
+
r"""
|
| 28 |
+
This is the configuration class to store the configuration of a [`Emu3VisionVQ`]. It is used to instantiate an video movq
|
| 29 |
+
model according to the specified arguments, defining the model architecture. Instantiating a configuration with the
|
| 30 |
+
defaults will yield a configuration to the VQ model presented in Emu3 paper.
|
| 31 |
+
|
| 32 |
+
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
|
| 33 |
+
documentation from [`PretrainedConfig`] for more information.
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
codebook_size (`int`, *optional*, defaults to 32768):
|
| 38 |
+
Codebook size of the VQ model.
|
| 39 |
+
embed_dim (`int`, *optional*, defaults to 4):
|
| 40 |
+
Dimension of the quantized vector in codebook.
|
| 41 |
+
z_channels (`int`, *optional*, defaults to 4):
|
| 42 |
+
Dimension of the output channel of encoder and the input channel of decoder
|
| 43 |
+
double_z (`bool`, *optional*, defaults to False):
|
| 44 |
+
Whether double the output dim of the encoder.
|
| 45 |
+
in_channels (`int`, *optional*, defaults to 3):
|
| 46 |
+
Input channel of encoder.
|
| 47 |
+
out_channels (`int`, *optional*, defaults to 3):
|
| 48 |
+
Output channel of decoder.
|
| 49 |
+
temporal_downsample_factor (`int`, *optional*, defaults to 4):
|
| 50 |
+
Temporal downsample factor.
|
| 51 |
+
ch (`int`, *optional*, defaults to 256):
|
| 52 |
+
Basic channel number of the intermediate blocks.
|
| 53 |
+
ch_mult (`List[int]`, *optional*, defaults to `[1, 2, 2, 4]`):
|
| 54 |
+
Channel scaling factor of the intermediate blocks.
|
| 55 |
+
num_res_blocks (`int`, *optional*, defaults to 2):
|
| 56 |
+
Residual block number in each stage.
|
| 57 |
+
attn_resolutions (`List[int]`, *optional*, defaults to 3):
|
| 58 |
+
Stage indices to apply attention.
|
| 59 |
+
dropout (`float`, *optional*, defaults to 0.0):
|
| 60 |
+
Dropout probability.
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
>>> from transformers import Emu3VisionVQ, Emu3VisionVQConfig
|
| 64 |
+
|
| 65 |
+
>>> # Initializing a video VQ model of Emu3 configuration
|
| 66 |
+
>>> configuration = Emu3VisionVQConfig()
|
| 67 |
+
|
| 68 |
+
>>> # Initializing a model from the Emu3 VQ model style configuration
|
| 69 |
+
>>> model = Emu3VisionVQModel(configuration)
|
| 70 |
+
|
| 71 |
+
>>> # Accessing the model configuration
|
| 72 |
+
>>> configuration = model.config
|
| 73 |
+
```"""
|
| 74 |
+
|
| 75 |
+
model_type = "Emu3VisionVQ"
|
| 76 |
+
|
| 77 |
+
def __init__(
|
| 78 |
+
self,
|
| 79 |
+
codebook_size: int = 32768,
|
| 80 |
+
embed_dim: int = 4,
|
| 81 |
+
z_channels: int = 4,
|
| 82 |
+
double_z: bool = False,
|
| 83 |
+
in_channels: int = 3,
|
| 84 |
+
out_channels: int = 3,
|
| 85 |
+
temporal_downsample_factor: int = 4,
|
| 86 |
+
ch: int = 256,
|
| 87 |
+
ch_mult: List[int] = [1, 2, 2, 4],
|
| 88 |
+
num_res_blocks: int = 2,
|
| 89 |
+
attn_resolutions: List[int] = [3],
|
| 90 |
+
dropout: float = 0.0,
|
| 91 |
+
**kwargs,
|
| 92 |
+
):
|
| 93 |
+
super().__init__(**kwargs)
|
| 94 |
+
|
| 95 |
+
self.codebook_size = codebook_size
|
| 96 |
+
self.embed_dim = embed_dim
|
| 97 |
+
self.z_channels = z_channels
|
| 98 |
+
self.double_z = double_z
|
| 99 |
+
self.in_channels = in_channels
|
| 100 |
+
self.out_channels = out_channels
|
| 101 |
+
self.temporal_downsample_factor = temporal_downsample_factor
|
| 102 |
+
self.ch = ch
|
| 103 |
+
self.ch_mult = ch_mult
|
| 104 |
+
self.num_res_blocks = num_res_blocks
|
| 105 |
+
self.attn_resolutions = attn_resolutions
|
| 106 |
+
self.dropout = dropout
|
codecs/vision/image_processing_emu3visionvq.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Emu team, BAAI and The HuggingFace Inc. team. All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""Image processor class for Emu3VisionVQ."""
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
import math
|
| 19 |
+
from typing import Dict, List, Optional, Union
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
|
| 24 |
+
from transformers.image_transforms import (
|
| 25 |
+
convert_to_rgb,
|
| 26 |
+
resize,
|
| 27 |
+
to_channel_dimension_format,
|
| 28 |
+
)
|
| 29 |
+
from transformers.image_utils import (
|
| 30 |
+
IMAGENET_STANDARD_MEAN,
|
| 31 |
+
IMAGENET_STANDARD_STD,
|
| 32 |
+
ChannelDimension,
|
| 33 |
+
ImageInput,
|
| 34 |
+
PILImageResampling,
|
| 35 |
+
get_image_size,
|
| 36 |
+
infer_channel_dimension_format,
|
| 37 |
+
is_scaled_image,
|
| 38 |
+
make_list_of_images,
|
| 39 |
+
to_numpy_array,
|
| 40 |
+
valid_images,
|
| 41 |
+
validate_preprocess_arguments,
|
| 42 |
+
)
|
| 43 |
+
from transformers.utils import TensorType, is_vision_available, logging
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
logger = logging.get_logger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if is_vision_available():
|
| 50 |
+
from PIL import Image
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def smart_resize(
|
| 54 |
+
height: int, width: int, factor: int = 8, min_pixels: int = 512 * 512, max_pixels: int = 1024 * 1024
|
| 55 |
+
):
|
| 56 |
+
"""Rescales the image so that the following conditions are met:
|
| 57 |
+
|
| 58 |
+
1. Both dimensions (height and width) are divisible by 'factor'.
|
| 59 |
+
|
| 60 |
+
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
|
| 61 |
+
|
| 62 |
+
3. The aspect ratio of the image is maintained as closely as possible.
|
| 63 |
+
|
| 64 |
+
"""
|
| 65 |
+
if height < factor or width < factor:
|
| 66 |
+
raise ValueError(f"height:{height} or width:{width} must be larger than factor:{factor}")
|
| 67 |
+
elif max(height, width) / min(height, width) > 5:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
f"absolute aspect ratio must be smaller than 5, got {max(height, width) / min(height, width)}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
h_bar = round(height / factor) * factor
|
| 73 |
+
w_bar = round(width / factor) * factor
|
| 74 |
+
if h_bar * w_bar > max_pixels:
|
| 75 |
+
beta = math.sqrt((height * width) / max_pixels)
|
| 76 |
+
h_bar = math.floor(height / beta / factor) * factor
|
| 77 |
+
w_bar = math.floor(width / beta / factor) * factor
|
| 78 |
+
elif h_bar * w_bar < min_pixels:
|
| 79 |
+
beta = math.sqrt(min_pixels / (height * width))
|
| 80 |
+
h_bar = math.ceil(height * beta / factor) * factor
|
| 81 |
+
w_bar = math.ceil(width * beta / factor) * factor
|
| 82 |
+
|
| 83 |
+
return h_bar, w_bar
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class Emu3VisionVQImageProcessor(BaseImageProcessor):
|
| 87 |
+
r"""
|
| 88 |
+
Constructs a Emu3VisionVQ image processor that dynamically resizes images based on the original images.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 92 |
+
Whether to resize the image's (height, width) dimensions.
|
| 93 |
+
resample (`PILImageResampling`, *optional*, defaults to `Resampling.BICUBIC`):
|
| 94 |
+
Resampling filter to use when resizing the image.
|
| 95 |
+
do_rescale (`bool`, *optional*, defaults to `True`):
|
| 96 |
+
Whether to rescale the image by the specified scale `rescale_factor`.
|
| 97 |
+
rescale_factor (`int` or `float`, *optional*, defaults to `1/255`):
|
| 98 |
+
Scale factor to use if rescaling the image.
|
| 99 |
+
do_normalize (`bool`, *optional*, defaults to `True`):
|
| 100 |
+
Whether to normalize the image.
|
| 101 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `[0.5, 0.5, 0.5]`):
|
| 102 |
+
Mean to use if normalizing the image. This is a float or list of floats for each channel in the image.
|
| 103 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `[0.5, 0.5, 0.5]`):
|
| 104 |
+
Standard deviation to use if normalizing the image. This is a float or list of floats for each channel in the image.
|
| 105 |
+
do_convert_rgb (`bool`, *optional*, defaults to `True`):
|
| 106 |
+
Whether to convert the image to RGB.
|
| 107 |
+
min_pixels (`int`, *optional*, defaults to `512 * 512`):
|
| 108 |
+
The min pixels of the image to resize the image.
|
| 109 |
+
max_pixels (`int`, *optional*, defaults to `1024 * 1024`):
|
| 110 |
+
The max pixels of the image to resize the image.
|
| 111 |
+
spatial_factor (`int`, *optional*, defautls to 8):
|
| 112 |
+
The spatial downsample factor the image will be downsampled in feature extracting phase
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
model_input_names = ["pixel_values"]
|
| 116 |
+
|
| 117 |
+
def __init__(
|
| 118 |
+
self,
|
| 119 |
+
do_resize: bool = True,
|
| 120 |
+
resample: PILImageResampling = PILImageResampling.BICUBIC,
|
| 121 |
+
do_rescale: bool = True,
|
| 122 |
+
rescale_factor: Union[int, float] = 1 / 255,
|
| 123 |
+
do_normalize: bool = True,
|
| 124 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 125 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 126 |
+
do_convert_rgb: bool = True,
|
| 127 |
+
min_pixels: int = 512 * 512,
|
| 128 |
+
max_pixels: int = 1024 * 1024,
|
| 129 |
+
spatial_factor: int = 8,
|
| 130 |
+
**kwargs,
|
| 131 |
+
) -> None:
|
| 132 |
+
super().__init__(**kwargs)
|
| 133 |
+
self.do_resize = do_resize
|
| 134 |
+
self.resample = resample
|
| 135 |
+
self.do_rescale = do_rescale
|
| 136 |
+
self.rescale_factor = rescale_factor
|
| 137 |
+
self.do_normalize = do_normalize
|
| 138 |
+
self.image_mean = image_mean if image_mean is not None else IMAGENET_STANDARD_MEAN
|
| 139 |
+
self.image_std = image_std if image_std is not None else IMAGENET_STANDARD_STD
|
| 140 |
+
self.min_pixels = min_pixels
|
| 141 |
+
self.max_pixels = max_pixels
|
| 142 |
+
self.size = {"min_pixels": min_pixels, "max_pixels": max_pixels}
|
| 143 |
+
self.do_convert_rgb = do_convert_rgb
|
| 144 |
+
self.spatial_factor = spatial_factor
|
| 145 |
+
|
| 146 |
+
def _preprocess(
|
| 147 |
+
self,
|
| 148 |
+
images: ImageInput,
|
| 149 |
+
do_resize: Optional[bool] = None,
|
| 150 |
+
resample: PILImageResampling = None,
|
| 151 |
+
do_rescale: Optional[bool] = None,
|
| 152 |
+
rescale_factor: Optional[float] = None,
|
| 153 |
+
do_normalize: Optional[bool] = None,
|
| 154 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 155 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 156 |
+
do_convert_rgb: Optional[bool] = None,
|
| 157 |
+
spatial_factor: Optional[int] = None,
|
| 158 |
+
input_data_format: Optional[Union[str, ChannelDimension]] = None,
|
| 159 |
+
output_data_format: Optional[Union[str, ChannelDimension]] = ChannelDimension.FIRST,
|
| 160 |
+
):
|
| 161 |
+
"""
|
| 162 |
+
Preprocess an image or batch of images. Copy of the `preprocess` method from `CLIPImageProcessor`.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
images (`ImageInput`):
|
| 166 |
+
Image or batch of images to preprocess. Expects pixel values ranging from 0 to 255. If pixel values range from 0 to 1, set `do_rescale=False`.
|
| 167 |
+
do_resize (`bool`, *optional*, defaults to `self.do_resize`):
|
| 168 |
+
Whether to resize the image.
|
| 169 |
+
resample (`PILImageResampling`, *optional*, defaults to `self.resample`):
|
| 170 |
+
Resampling filter to use if resizing the image. This can be one of the `PILImageResampling` enums.
|
| 171 |
+
do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
|
| 172 |
+
Whether to rescale the image.
|
| 173 |
+
rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
|
| 174 |
+
Scale factor to use if rescaling the image.
|
| 175 |
+
do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
|
| 176 |
+
Whether to normalize the image.
|
| 177 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
|
| 178 |
+
Mean to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
|
| 179 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
|
| 180 |
+
Standard deviation to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
|
| 181 |
+
do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
|
| 182 |
+
Whether to convert the image to RGB.
|
| 183 |
+
spatial_factor (`int`, *optional*, defaults to `self.spatial_factor`):
|
| 184 |
+
The spatial downsample factor the image will be downsampled in feature extracting phase
|
| 185 |
+
input_data_format (`ChannelDimension` or `str`, *optional*):
|
| 186 |
+
The channel dimension format for the input image. Can be one of:
|
| 187 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 188 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 189 |
+
- `"none"` or `ChannelDimension.NONE`: image in (height, width) format. - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
|
| 190 |
+
output_data_format (`ChannelDimension`, *optional*, defaults to `ChannelDimension.FIRST`):
|
| 191 |
+
The channel dimension format for the output image. Can be one of:
|
| 192 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 193 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 194 |
+
- Unset: Use the channel dimension format of the input image.
|
| 195 |
+
"""
|
| 196 |
+
spatial_factor = spatial_factor if spatial_factor is not None else self.spatial_factor
|
| 197 |
+
|
| 198 |
+
images = make_list_of_images(images)
|
| 199 |
+
if do_convert_rgb:
|
| 200 |
+
images = [convert_to_rgb(image) for image in images]
|
| 201 |
+
|
| 202 |
+
# All transformations expect numpy arrays.
|
| 203 |
+
images = [to_numpy_array(image) for image in images]
|
| 204 |
+
|
| 205 |
+
if is_scaled_image(images[0]) and do_rescale:
|
| 206 |
+
logger.warning_once(
|
| 207 |
+
"It looks like you are trying to rescale already rescaled images. If the input"
|
| 208 |
+
"pixel_values.append()images have pixel values between 0 and 1, set `do_rescale=False` to avoid rescaling them again."
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if input_data_format is None:
|
| 212 |
+
# We assume that all images have the same channel dimension format.
|
| 213 |
+
input_data_format = infer_channel_dimension_format(images[0])
|
| 214 |
+
|
| 215 |
+
height, width = get_image_size(images[0], channel_dim=input_data_format)
|
| 216 |
+
resized_height, resized_width = height, width
|
| 217 |
+
processed_images = []
|
| 218 |
+
for image in images:
|
| 219 |
+
if do_resize:
|
| 220 |
+
resized_height, resized_width = smart_resize(
|
| 221 |
+
height,
|
| 222 |
+
width,
|
| 223 |
+
factor=spatial_factor,
|
| 224 |
+
min_pixels=self.min_pixels,
|
| 225 |
+
max_pixels=self.max_pixels,
|
| 226 |
+
)
|
| 227 |
+
image = resize(
|
| 228 |
+
image, size=(resized_height, resized_width), resample=resample, input_data_format=input_data_format
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if do_rescale:
|
| 232 |
+
image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)
|
| 233 |
+
|
| 234 |
+
if do_normalize:
|
| 235 |
+
image = self.normalize(
|
| 236 |
+
image=image, mean=image_mean, std=image_std, input_data_format=input_data_format
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
image = to_channel_dimension_format(image, output_data_format, input_channel_dim=input_data_format)
|
| 240 |
+
processed_images.append(image)
|
| 241 |
+
|
| 242 |
+
image = np.array(processed_images)
|
| 243 |
+
return image
|
| 244 |
+
|
| 245 |
+
def preprocess(
|
| 246 |
+
self,
|
| 247 |
+
images: ImageInput,
|
| 248 |
+
do_resize: Optional[bool] = None,
|
| 249 |
+
resample: PILImageResampling = None,
|
| 250 |
+
do_rescale: Optional[bool] = None,
|
| 251 |
+
rescale_factor: Optional[float] = None,
|
| 252 |
+
do_normalize: Optional[bool] = None,
|
| 253 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 254 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 255 |
+
do_convert_rgb: Optional[bool] = None,
|
| 256 |
+
spatial_factor: Optional[int] = None,
|
| 257 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 258 |
+
input_data_format: Optional[Union[str, ChannelDimension]] = None,
|
| 259 |
+
output_data_format: Optional[Union[str, ChannelDimension]] = ChannelDimension.FIRST,
|
| 260 |
+
):
|
| 261 |
+
"""
|
| 262 |
+
Args:
|
| 263 |
+
images (`ImageInput`):
|
| 264 |
+
Image to preprocess. Expects a single or batch of images with pixel values ranging from 0 to 255. If
|
| 265 |
+
passing in images with pixel values between 0 and 1, set `do_rescale=False`.
|
| 266 |
+
do_resize (`bool`, *optional*, defaults to `self.do_resize`):
|
| 267 |
+
Whether to resize the image.
|
| 268 |
+
resample (`int`, *optional*, defaults to `self.resample`):
|
| 269 |
+
Resampling filter to use if resizing the image. This can be one of the enum `PILImageResampling`. Only
|
| 270 |
+
has an effect if `do_resize` is set to `True`.
|
| 271 |
+
do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
|
| 272 |
+
Whether to rescale the image.
|
| 273 |
+
rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
|
| 274 |
+
Rescale factor to rescale the image by if `do_rescale` is set to `True`.
|
| 275 |
+
do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
|
| 276 |
+
Whether to normalize the image.
|
| 277 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
|
| 278 |
+
Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
|
| 279 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
|
| 280 |
+
Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to `True`.
|
| 281 |
+
do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
|
| 282 |
+
Whether to convert the image to RGB.
|
| 283 |
+
spatial_factor (`int`, *optional*, defaults to `self.spatial_factor`):
|
| 284 |
+
The spatial downsample factor the image will be downsampled in feature extracting phase
|
| 285 |
+
return_tensors (`str` or `TensorType`, *optional*):
|
| 286 |
+
The type of tensors to return. Can be one of:
|
| 287 |
+
- Unset: Return a list of `np.ndarray`.
|
| 288 |
+
- `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
|
| 289 |
+
- `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
|
| 290 |
+
input_data_format (`ChannelDimension` or `str`, *optional*):
|
| 291 |
+
The channel dimension format for the input image. If unset, the channel dimension format is inferred
|
| 292 |
+
from the input image. Can be one of:
|
| 293 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 294 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 295 |
+
- `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
|
| 296 |
+
output_data_format (`ChannelDimension` or `str`, *optional*, defaults to `ChannelDimension.FIRST`):
|
| 297 |
+
The channel dimension format for the output image. Can be one of:
|
| 298 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 299 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 300 |
+
- Unset: Use the channel dimension format of the input image.
|
| 301 |
+
"""
|
| 302 |
+
do_resize = do_resize if do_resize is not None else self.do_resize
|
| 303 |
+
resample = resample if resample is not None else self.resample
|
| 304 |
+
do_rescale = do_rescale if do_rescale is not None else self.do_rescale
|
| 305 |
+
rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
|
| 306 |
+
do_normalize = do_normalize if do_normalize is not None else self.do_normalize
|
| 307 |
+
image_mean = image_mean if image_mean is not None else self.image_mean
|
| 308 |
+
image_std = image_std if image_std is not None else self.image_std
|
| 309 |
+
do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
|
| 310 |
+
spatial_factor = spatial_factor if spatial_factor is not None else self.spatial_factor
|
| 311 |
+
|
| 312 |
+
images = make_list_of_images(images)
|
| 313 |
+
if images is None or not valid_images(images):
|
| 314 |
+
raise ValueError(
|
| 315 |
+
"Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, "
|
| 316 |
+
"torch.Tensor, tf.Tensor or jax.ndarray."
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
validate_preprocess_arguments(
|
| 320 |
+
rescale_factor=rescale_factor,
|
| 321 |
+
do_normalize=do_normalize,
|
| 322 |
+
image_mean=image_mean,
|
| 323 |
+
image_std=image_std,
|
| 324 |
+
do_resize=do_resize,
|
| 325 |
+
size=self.size,
|
| 326 |
+
resample=resample,
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
pixel_values = []
|
| 330 |
+
for image in images:
|
| 331 |
+
norm_image = self._preprocess(
|
| 332 |
+
image,
|
| 333 |
+
do_resize=do_resize,
|
| 334 |
+
resample=resample,
|
| 335 |
+
do_rescale=do_rescale,
|
| 336 |
+
rescale_factor=rescale_factor,
|
| 337 |
+
do_normalize=do_normalize,
|
| 338 |
+
image_mean=image_mean,
|
| 339 |
+
image_std=image_std,
|
| 340 |
+
do_convert_rgb=do_convert_rgb,
|
| 341 |
+
spatial_factor=spatial_factor,
|
| 342 |
+
input_data_format=input_data_format,
|
| 343 |
+
output_data_format=output_data_format,
|
| 344 |
+
)
|
| 345 |
+
pixel_values.extend(norm_image)
|
| 346 |
+
pixel_values = np.array(pixel_values)
|
| 347 |
+
data = {"pixel_values": pixel_values}
|
| 348 |
+
|
| 349 |
+
return BatchFeature(data=data, tensor_type=return_tensors)
|
| 350 |
+
|
| 351 |
+
def postprocess(
|
| 352 |
+
self,
|
| 353 |
+
images: ImageInput,
|
| 354 |
+
do_rescale: Optional[bool] = None,
|
| 355 |
+
rescale_factor: Optional[float] = None,
|
| 356 |
+
do_normalize: Optional[bool] = None,
|
| 357 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 358 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 359 |
+
return_tensors: str | TensorType = "PIL.Image.Image",
|
| 360 |
+
input_data_format: Optional[Union[str, ChannelDimension]] = None,
|
| 361 |
+
):
|
| 362 |
+
"""
|
| 363 |
+
Postprocess an image or batch of images tensor. Postprocess is the reverse process of preprocess.
|
| 364 |
+
The parameters should be same as in preprocess.
|
| 365 |
+
|
| 366 |
+
Args:
|
| 367 |
+
images (`ImageInput`):
|
| 368 |
+
Image to postprocess. Expects a single or batch of images with pixel values ranging from -1 to 1.
|
| 369 |
+
do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
|
| 370 |
+
Whether to rescale the image.
|
| 371 |
+
rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
|
| 372 |
+
Rescale factor to rescale the image by if `do_rescale` is set to `True`.
|
| 373 |
+
do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
|
| 374 |
+
Whether to normalize the image.
|
| 375 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
|
| 376 |
+
Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
|
| 377 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
|
| 378 |
+
Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to `True`.
|
| 379 |
+
return_tensors (`str` or `TensorType`, *optional*):
|
| 380 |
+
The type of tensors to return. Can be one of:
|
| 381 |
+
- Unset: Return a list of `np.ndarray`.
|
| 382 |
+
- `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
|
| 383 |
+
- `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
|
| 384 |
+
input_data_format (`ChannelDimension` or `str`, *optional*):
|
| 385 |
+
The channel dimension format for the input image. If unset, the channel dimension format is inferred
|
| 386 |
+
from the input image. Can be one of:
|
| 387 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 388 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 389 |
+
- `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
|
| 390 |
+
"""
|
| 391 |
+
do_rescale = do_rescale if do_rescale is not None else self.do_rescale
|
| 392 |
+
rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
|
| 393 |
+
rescale_factor = 1 / rescale_factor
|
| 394 |
+
|
| 395 |
+
do_normalize = do_normalize if do_normalize is not None else self.do_normalize
|
| 396 |
+
image_mean = image_mean if image_mean is not None else self.image_mean
|
| 397 |
+
image_std = image_std if image_std is not None else self.image_std
|
| 398 |
+
image_mean, image_std = self.inverse_meanstd(image_mean, image_std)
|
| 399 |
+
|
| 400 |
+
images = make_list_of_images(images)
|
| 401 |
+
if isinstance(images[0], Image.Image):
|
| 402 |
+
return images if len(images) > 1 else images[0]
|
| 403 |
+
|
| 404 |
+
if input_data_format is None:
|
| 405 |
+
# We assume that all images have the same channel dimension format.
|
| 406 |
+
input_data_format = infer_channel_dimension_format(images[0])
|
| 407 |
+
|
| 408 |
+
pixel_values = []
|
| 409 |
+
for image in images:
|
| 410 |
+
image = to_numpy_array(image)
|
| 411 |
+
if do_normalize:
|
| 412 |
+
image = self.normalize(image=image, mean=image_mean, std=image_std, input_data_format=input_data_format)
|
| 413 |
+
|
| 414 |
+
if do_rescale:
|
| 415 |
+
image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)
|
| 416 |
+
image = image.clip(0, 255).astype(np.uint8)
|
| 417 |
+
|
| 418 |
+
if do_normalize and do_rescale and return_tensors == "PIL.Image.Image":
|
| 419 |
+
image = to_channel_dimension_format(image, ChannelDimension.LAST, input_channel_dim=input_data_format)
|
| 420 |
+
pixel_values.append(Image.fromarray(image))
|
| 421 |
+
else:
|
| 422 |
+
pixel_values.extend(image)
|
| 423 |
+
|
| 424 |
+
data = {"pixel_values": pixel_values}
|
| 425 |
+
return_tensors = return_tensors if return_tensors != "PIL.Image.Image" else None
|
| 426 |
+
|
| 427 |
+
return BatchFeature(data=data, tensor_type=return_tensors)
|
| 428 |
+
|
| 429 |
+
def inverse_meanstd(self, image_mean, image_std):
|
| 430 |
+
image_mean = self.to_tuple(image_mean)
|
| 431 |
+
image_std = self.to_tuple(image_std)
|
| 432 |
+
|
| 433 |
+
rev_image_mean = tuple(-m / s for m, s in zip(image_mean, image_std))
|
| 434 |
+
rev_image_std = tuple(1 / s for s in image_std)
|
| 435 |
+
|
| 436 |
+
return rev_image_mean, rev_image_std
|
| 437 |
+
|
| 438 |
+
def to_tuple(self, value, dim=3):
|
| 439 |
+
if isinstance(value, int | float):
|
| 440 |
+
return (value,) * dim
|
| 441 |
+
|
| 442 |
+
return tuple(value)
|
codecs/vision/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:89536431c69b08b10b449ec309f52dcea22f14b7647317f30f5715273392bbf1
|
| 3 |
+
size 1083015124
|
codecs/vision/modeling_emu3visionvq.py
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2024 The Emu team, BAAI and The HuggingFace Inc. team. All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
""" Emu3VisionVQ model """
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import Optional, Tuple, Union
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
from torch import nn
|
| 22 |
+
from torch.nn import functional as F
|
| 23 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 24 |
+
|
| 25 |
+
from .configuration_emu3visionvq import Emu3VisionVQConfig
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Emu3VisionVQActivation(nn.Module):
|
| 29 |
+
|
| 30 |
+
def __init__(self):
|
| 31 |
+
super().__init__()
|
| 32 |
+
|
| 33 |
+
def __call__(self, x: torch.Tensor):
|
| 34 |
+
return x * torch.sigmoid(x)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Emu3VisionVQUpsample(nn.Module):
|
| 38 |
+
|
| 39 |
+
def __init__(self, in_channels: int):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.conv = nn.Conv2d(
|
| 42 |
+
in_channels,
|
| 43 |
+
in_channels,
|
| 44 |
+
kernel_size=3,
|
| 45 |
+
stride=1,
|
| 46 |
+
padding=1,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def forward(self, x: torch.Tensor):
|
| 50 |
+
x = F.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 51 |
+
x = self.conv(x)
|
| 52 |
+
return x
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Emu3VisionVQDownsample(nn.Module):
|
| 56 |
+
|
| 57 |
+
def __init__(self, in_channels: int):
|
| 58 |
+
super().__init__()
|
| 59 |
+
self.conv = nn.Conv2d(
|
| 60 |
+
in_channels,
|
| 61 |
+
in_channels,
|
| 62 |
+
kernel_size=3,
|
| 63 |
+
stride=2,
|
| 64 |
+
padding=0,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
def forward(self, x: torch.Tensor):
|
| 68 |
+
pad = (0, 1, 0, 1)
|
| 69 |
+
x = F.pad(x, pad, mode="constant", value=0)
|
| 70 |
+
x = self.conv(x)
|
| 71 |
+
return x
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class Emu3VisionVQCausalConv3d(nn.Module):
|
| 75 |
+
|
| 76 |
+
def __init__(
|
| 77 |
+
self,
|
| 78 |
+
in_channel: int,
|
| 79 |
+
out_channel: int,
|
| 80 |
+
kernel_size: Union[int, Tuple[int, ...]] = (3, 1, 1),
|
| 81 |
+
stride: Union[int, Tuple[int, ...]] = (1, 1, 1),
|
| 82 |
+
):
|
| 83 |
+
super().__init__()
|
| 84 |
+
|
| 85 |
+
if isinstance(kernel_size, int):
|
| 86 |
+
kernel_size = (kernel_size,) * 3
|
| 87 |
+
if isinstance(stride, int):
|
| 88 |
+
stride = (stride,) * 3
|
| 89 |
+
|
| 90 |
+
hw_pad = [k - s for k, s in zip(kernel_size[1:], stride[1:])]
|
| 91 |
+
self.padding = tuple()
|
| 92 |
+
for p in hw_pad[::-1]:
|
| 93 |
+
self.padding += (p // 2 + p % 2, p // 2)
|
| 94 |
+
self.padding += (2, 0)
|
| 95 |
+
|
| 96 |
+
self.conv = nn.Conv3d(
|
| 97 |
+
in_channel,
|
| 98 |
+
out_channel,
|
| 99 |
+
kernel_size,
|
| 100 |
+
stride=stride,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def forward(self, x: torch.Tensor):
|
| 104 |
+
x = F.pad(x, self.padding)
|
| 105 |
+
x = self.conv(x)
|
| 106 |
+
return x
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class Emu3VisionVQResnetTemporalBlock(nn.Module):
|
| 110 |
+
|
| 111 |
+
def __init__(
|
| 112 |
+
self,
|
| 113 |
+
in_channels: int,
|
| 114 |
+
out_channels: Optional[int] = None,
|
| 115 |
+
conv_shortcut: bool = False,
|
| 116 |
+
dropout: float = 0.0,
|
| 117 |
+
):
|
| 118 |
+
super().__init__()
|
| 119 |
+
self.in_channels = in_channels
|
| 120 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 121 |
+
self.out_channels = out_channels
|
| 122 |
+
self.use_conv_shortcut = conv_shortcut
|
| 123 |
+
|
| 124 |
+
stride = (1, 1, 1)
|
| 125 |
+
kernel_size = (3, 3, 3)
|
| 126 |
+
|
| 127 |
+
self.norm1 = nn.BatchNorm3d(in_channels)
|
| 128 |
+
self.conv1 = Emu3VisionVQCausalConv3d(
|
| 129 |
+
in_channels,
|
| 130 |
+
out_channels,
|
| 131 |
+
kernel_size=kernel_size,
|
| 132 |
+
stride=stride,
|
| 133 |
+
)
|
| 134 |
+
self.norm2 = nn.BatchNorm3d(out_channels)
|
| 135 |
+
self.dropout = nn.Dropout(dropout)
|
| 136 |
+
self.conv2 = Emu3VisionVQCausalConv3d(
|
| 137 |
+
out_channels,
|
| 138 |
+
out_channels,
|
| 139 |
+
kernel_size=kernel_size,
|
| 140 |
+
stride=stride,
|
| 141 |
+
)
|
| 142 |
+
self.act = Emu3VisionVQActivation()
|
| 143 |
+
|
| 144 |
+
if self.in_channels != self.out_channels:
|
| 145 |
+
if self.use_conv_shortcut:
|
| 146 |
+
self.conv_shortcut = Emu3VisionVQCausalConv3d(
|
| 147 |
+
in_channels,
|
| 148 |
+
out_channels,
|
| 149 |
+
kernel_size=kernel_size,
|
| 150 |
+
stride=stride,
|
| 151 |
+
)
|
| 152 |
+
else:
|
| 153 |
+
self.nin_shortcut = nn.Conv3d(
|
| 154 |
+
in_channels,
|
| 155 |
+
out_channels,
|
| 156 |
+
kernel_size=1,
|
| 157 |
+
stride=1,
|
| 158 |
+
padding=0,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
def forward(self, x: torch.Tensor):
|
| 162 |
+
h = self.norm1(x)
|
| 163 |
+
h = self.act(h)
|
| 164 |
+
h = self.conv1(h)
|
| 165 |
+
|
| 166 |
+
h = self.norm2(h)
|
| 167 |
+
h = self.act(h)
|
| 168 |
+
h = self.dropout(h)
|
| 169 |
+
h = self.conv2(h)
|
| 170 |
+
|
| 171 |
+
if self.in_channels != self.out_channels:
|
| 172 |
+
if self.use_conv_shortcut:
|
| 173 |
+
x = self.conv_shortcut(x)
|
| 174 |
+
else:
|
| 175 |
+
x = self.nin_shortcut(x)
|
| 176 |
+
|
| 177 |
+
return x + h
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class Emu3VisionVQSpatialNorm(nn.Module):
|
| 181 |
+
|
| 182 |
+
def __init__(
|
| 183 |
+
self,
|
| 184 |
+
f_channels: int,
|
| 185 |
+
zq_channels: int,
|
| 186 |
+
norm_layer: nn.Module = nn.GroupNorm,
|
| 187 |
+
add_conv: bool = False,
|
| 188 |
+
num_groups: int = 32,
|
| 189 |
+
eps: float = 1e-6,
|
| 190 |
+
affine: bool = True,
|
| 191 |
+
):
|
| 192 |
+
super().__init__()
|
| 193 |
+
self.norm_layer = norm_layer(
|
| 194 |
+
num_channels=f_channels,
|
| 195 |
+
num_groups=num_groups,
|
| 196 |
+
eps=eps,
|
| 197 |
+
affine=affine,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
self.add_conv = add_conv
|
| 201 |
+
if self.add_conv:
|
| 202 |
+
self.conv = nn.Conv2d(
|
| 203 |
+
zq_channels,
|
| 204 |
+
zq_channels,
|
| 205 |
+
kernel_size=3,
|
| 206 |
+
stride=1,
|
| 207 |
+
padding=1,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
self.conv_y = nn.Conv2d(
|
| 211 |
+
zq_channels,
|
| 212 |
+
f_channels,
|
| 213 |
+
kernel_size=1,
|
| 214 |
+
stride=1,
|
| 215 |
+
padding=0,
|
| 216 |
+
)
|
| 217 |
+
self.conv_b = nn.Conv2d(
|
| 218 |
+
zq_channels,
|
| 219 |
+
f_channels,
|
| 220 |
+
kernel_size=1,
|
| 221 |
+
stride=1,
|
| 222 |
+
padding=0,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def forward(self, x: torch.Tensor, zq: torch.Tensor):
|
| 226 |
+
zq = F.interpolate(zq, size=x.shape[-2:], mode="nearest")
|
| 227 |
+
|
| 228 |
+
if self.add_conv:
|
| 229 |
+
zq = self.conv(zq)
|
| 230 |
+
|
| 231 |
+
x = self.norm_layer(x)
|
| 232 |
+
x = x * self.conv_y(zq) + self.conv_b(zq)
|
| 233 |
+
return x
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
class Emu3VisionVQResnetBlock(nn.Module):
|
| 237 |
+
|
| 238 |
+
def __init__(
|
| 239 |
+
self,
|
| 240 |
+
in_channels: int,
|
| 241 |
+
out_channels: Optional[int] = None,
|
| 242 |
+
conv_shortcut: bool = False,
|
| 243 |
+
dropout: float = 0.0,
|
| 244 |
+
zq_ch: Optional[int] = None,
|
| 245 |
+
add_conv: bool = False,
|
| 246 |
+
):
|
| 247 |
+
super().__init__()
|
| 248 |
+
self.in_channels = in_channels
|
| 249 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 250 |
+
self.out_channels = out_channels
|
| 251 |
+
self.use_conv_shortcut = conv_shortcut
|
| 252 |
+
self.zq_ch = zq_ch
|
| 253 |
+
|
| 254 |
+
if zq_ch is None:
|
| 255 |
+
norm_kwargs = dict(num_groups=32, eps=1e-6, affine=True)
|
| 256 |
+
self.norm1 = nn.GroupNorm(num_channels=in_channels, **norm_kwargs)
|
| 257 |
+
self.norm2 = nn.GroupNorm(num_channels=out_channels, **norm_kwargs)
|
| 258 |
+
else:
|
| 259 |
+
self.norm1 = Emu3VisionVQSpatialNorm(in_channels, zq_ch, add_conv=add_conv)
|
| 260 |
+
self.norm2 = Emu3VisionVQSpatialNorm(out_channels, zq_ch, add_conv=add_conv)
|
| 261 |
+
|
| 262 |
+
self.conv1 = nn.Conv2d(
|
| 263 |
+
in_channels,
|
| 264 |
+
out_channels,
|
| 265 |
+
kernel_size=3,
|
| 266 |
+
stride=1,
|
| 267 |
+
padding=1,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
self.dropout = nn.Dropout(dropout)
|
| 271 |
+
self.conv2 = nn.Conv2d(
|
| 272 |
+
out_channels,
|
| 273 |
+
out_channels,
|
| 274 |
+
kernel_size=3,
|
| 275 |
+
stride=1,
|
| 276 |
+
padding=1,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
self.act = Emu3VisionVQActivation()
|
| 280 |
+
|
| 281 |
+
if self.in_channels != self.out_channels:
|
| 282 |
+
if self.use_conv_shortcut:
|
| 283 |
+
self.conv_shortcut = nn.Conv2d(
|
| 284 |
+
in_channels,
|
| 285 |
+
out_channels,
|
| 286 |
+
kernel_size=3,
|
| 287 |
+
stride=1,
|
| 288 |
+
padding=1,
|
| 289 |
+
)
|
| 290 |
+
else:
|
| 291 |
+
self.nin_shortcut = nn.Conv2d(
|
| 292 |
+
in_channels,
|
| 293 |
+
out_channels,
|
| 294 |
+
kernel_size=1,
|
| 295 |
+
stride=1,
|
| 296 |
+
padding=0,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def forward(self, x: torch.Tensor, zq: Optional[torch.Tensor] = None):
|
| 300 |
+
norm_args = tuple() if self.zq_ch is None else (zq, )
|
| 301 |
+
|
| 302 |
+
h = self.norm1(x, *norm_args)
|
| 303 |
+
h = self.act(h)
|
| 304 |
+
h = self.conv1(h)
|
| 305 |
+
|
| 306 |
+
h = self.norm2(h, *norm_args)
|
| 307 |
+
h = self.act(h)
|
| 308 |
+
h = self.dropout(h)
|
| 309 |
+
h = self.conv2(h)
|
| 310 |
+
|
| 311 |
+
if self.in_channels != self.out_channels:
|
| 312 |
+
if self.use_conv_shortcut:
|
| 313 |
+
x = self.conv_shortcut(x)
|
| 314 |
+
else:
|
| 315 |
+
x = self.nin_shortcut(x)
|
| 316 |
+
|
| 317 |
+
return x + h
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class Emu3VisionVQAttnBlock(nn.Module):
|
| 321 |
+
|
| 322 |
+
def __init__(
|
| 323 |
+
self,
|
| 324 |
+
in_channels: int,
|
| 325 |
+
zq_ch: Optional[int] = None,
|
| 326 |
+
add_conv: bool = False
|
| 327 |
+
):
|
| 328 |
+
super().__init__()
|
| 329 |
+
self.in_channels = in_channels
|
| 330 |
+
self.zq_ch = zq_ch
|
| 331 |
+
|
| 332 |
+
if zq_ch is None:
|
| 333 |
+
norm_kwargs = dict(num_groups=32, eps=1e-6, affine=True)
|
| 334 |
+
self.norm = nn.GroupNorm(num_channels=in_channels, **norm_kwargs)
|
| 335 |
+
else:
|
| 336 |
+
self.norm = Emu3VisionVQSpatialNorm(in_channels, zq_ch, add_conv=add_conv)
|
| 337 |
+
|
| 338 |
+
self.q = nn.Conv2d(
|
| 339 |
+
in_channels,
|
| 340 |
+
in_channels,
|
| 341 |
+
kernel_size=1,
|
| 342 |
+
stride=1,
|
| 343 |
+
padding=0,
|
| 344 |
+
)
|
| 345 |
+
self.k = nn.Conv2d(
|
| 346 |
+
in_channels,
|
| 347 |
+
in_channels,
|
| 348 |
+
kernel_size=1,
|
| 349 |
+
stride=1,
|
| 350 |
+
padding=0,
|
| 351 |
+
)
|
| 352 |
+
self.v = nn.Conv2d(
|
| 353 |
+
in_channels,
|
| 354 |
+
in_channels,
|
| 355 |
+
kernel_size=1,
|
| 356 |
+
stride=1,
|
| 357 |
+
padding=0,
|
| 358 |
+
)
|
| 359 |
+
self.proj_out = nn.Conv2d(
|
| 360 |
+
in_channels,
|
| 361 |
+
in_channels,
|
| 362 |
+
kernel_size=1,
|
| 363 |
+
stride=1,
|
| 364 |
+
padding=0,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
def forward(self, x: torch.Tensor, zq: Optional[torch.Tensor] = None):
|
| 368 |
+
norm_args = tuple() if self.zq_ch is None else (zq, )
|
| 369 |
+
|
| 370 |
+
nx = self.norm(x, *norm_args)
|
| 371 |
+
q = self.q(nx)
|
| 372 |
+
k = self.k(nx)
|
| 373 |
+
v = self.v(nx)
|
| 374 |
+
|
| 375 |
+
# compute attention
|
| 376 |
+
b, c, h, w = q.shape
|
| 377 |
+
q = q.reshape(b, c, h * w)
|
| 378 |
+
k = k.reshape(b, c, h * w)
|
| 379 |
+
score = torch.bmm(q.permute(0, 2, 1), k)
|
| 380 |
+
score = score / (c ** 0.5)
|
| 381 |
+
score = F.softmax(score, dim=2)
|
| 382 |
+
|
| 383 |
+
# attend to values
|
| 384 |
+
v = v.reshape(b, c, h * w)
|
| 385 |
+
v = torch.bmm(v, score.permute(0, 2, 1))
|
| 386 |
+
v = v.reshape(b, c, h, w)
|
| 387 |
+
|
| 388 |
+
v = self.proj_out(v)
|
| 389 |
+
|
| 390 |
+
return x + v
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
class Emu3VisionVQTemporalUpsample(nn.Module):
|
| 394 |
+
|
| 395 |
+
def __init__(
|
| 396 |
+
self,
|
| 397 |
+
in_channel: int,
|
| 398 |
+
out_channel: int,
|
| 399 |
+
kernel_size: Tuple[int, ...] = (3, 3, 3),
|
| 400 |
+
stride: Tuple[int, ...] = (1, 1, 1)
|
| 401 |
+
):
|
| 402 |
+
super().__init__()
|
| 403 |
+
self.in_channel = in_channel
|
| 404 |
+
self.out_channel = out_channel
|
| 405 |
+
self.conv = Emu3VisionVQCausalConv3d(
|
| 406 |
+
in_channel,
|
| 407 |
+
out_channel,
|
| 408 |
+
kernel_size,
|
| 409 |
+
stride=stride,
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
def forward(self, x: torch.Tensor):
|
| 413 |
+
b, c, t, h, w = x.shape
|
| 414 |
+
x = x.permute(0, 1, 3, 4, 2).contiguous().view(b, -1, t)
|
| 415 |
+
x = F.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 416 |
+
x = x.view(b, c, h, w, -1).permute(0, 1, 4, 2, 3).contiguous()
|
| 417 |
+
x = self.conv(x)
|
| 418 |
+
return x
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
class Emu3VisionVQTemporalDownsample(nn.Module):
|
| 422 |
+
|
| 423 |
+
def __init__(
|
| 424 |
+
self,
|
| 425 |
+
in_channel: int,
|
| 426 |
+
out_channel: int,
|
| 427 |
+
kernel_size: Tuple[int, ...] = (4, 3, 3),
|
| 428 |
+
stride: Tuple[int, ...] = (2, 1, 1),
|
| 429 |
+
):
|
| 430 |
+
super().__init__()
|
| 431 |
+
self.in_channel = in_channel
|
| 432 |
+
self.out_channel = out_channel
|
| 433 |
+
self.kernel_size = kernel_size
|
| 434 |
+
|
| 435 |
+
self.conv = Emu3VisionVQCausalConv3d(
|
| 436 |
+
in_channel,
|
| 437 |
+
out_channel,
|
| 438 |
+
kernel_size=kernel_size,
|
| 439 |
+
stride=stride,
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
def forward(self, x: torch.Tensor):
|
| 443 |
+
x = self.conv(x)
|
| 444 |
+
return x
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
class Emu3VisionVQVectorQuantizer(nn.Module):
|
| 448 |
+
|
| 449 |
+
def __init__(self, config: Emu3VisionVQConfig):
|
| 450 |
+
super().__init__()
|
| 451 |
+
self.embedding = nn.Embedding(config.codebook_size, config.embed_dim)
|
| 452 |
+
self.embedding.weight.data.uniform_(-1.0 / config.codebook_size, 1.0 / config.codebook_size)
|
| 453 |
+
|
| 454 |
+
def forward(self, x: torch.Tensor):
|
| 455 |
+
# b t c h w -> b t h w c
|
| 456 |
+
b, t, c, h, w = x.shape
|
| 457 |
+
x = x.permute(0, 1, 3, 4, 2).contiguous()
|
| 458 |
+
x_flattened = x.view(-1, c)
|
| 459 |
+
|
| 460 |
+
codebook = self.embedding.weight
|
| 461 |
+
|
| 462 |
+
d = torch.sum(x_flattened ** 2, dim=1, keepdim=True) + \
|
| 463 |
+
torch.sum(codebook ** 2, dim=1) - 2 * \
|
| 464 |
+
torch.einsum('bd,dn->bn', x_flattened, codebook.permute(1, 0))
|
| 465 |
+
|
| 466 |
+
indices = torch.argmin(d, dim=1)
|
| 467 |
+
indices = indices.view(b, t, h, w)
|
| 468 |
+
return indices
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
class Emu3VisionVQEncoder(nn.Module):
|
| 472 |
+
|
| 473 |
+
def __init__(self, config: Emu3VisionVQConfig):
|
| 474 |
+
super().__init__()
|
| 475 |
+
self.ch = config.ch
|
| 476 |
+
self.num_resolutions = len(config.ch_mult)
|
| 477 |
+
self.num_res_blocks = config.num_res_blocks
|
| 478 |
+
self.in_channels = config.in_channels
|
| 479 |
+
|
| 480 |
+
# downsampling
|
| 481 |
+
self.conv_in = nn.Conv2d(
|
| 482 |
+
self.in_channels,
|
| 483 |
+
self.ch,
|
| 484 |
+
kernel_size=3,
|
| 485 |
+
stride=1,
|
| 486 |
+
padding=1
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
in_ch_mult = (1,) + tuple(config.ch_mult)
|
| 490 |
+
self.down = nn.ModuleList()
|
| 491 |
+
for i_level in range(self.num_resolutions):
|
| 492 |
+
block = nn.ModuleList()
|
| 493 |
+
attn = nn.ModuleList()
|
| 494 |
+
block_in = config.ch * in_ch_mult[i_level]
|
| 495 |
+
block_out = config.ch * config.ch_mult[i_level]
|
| 496 |
+
for i_block in range(self.num_res_blocks):
|
| 497 |
+
block.append(
|
| 498 |
+
Emu3VisionVQResnetBlock(
|
| 499 |
+
in_channels=block_in,
|
| 500 |
+
out_channels=block_out,
|
| 501 |
+
dropout=config.dropout,
|
| 502 |
+
)
|
| 503 |
+
)
|
| 504 |
+
block_in = block_out
|
| 505 |
+
if i_level in config.attn_resolutions:
|
| 506 |
+
attn.append(Emu3VisionVQAttnBlock(block_in))
|
| 507 |
+
|
| 508 |
+
down = nn.Module()
|
| 509 |
+
down.block = block
|
| 510 |
+
down.attn = attn
|
| 511 |
+
if i_level != self.num_resolutions - 1:
|
| 512 |
+
down.downsample = Emu3VisionVQDownsample(block_in)
|
| 513 |
+
|
| 514 |
+
self.down.append(down)
|
| 515 |
+
|
| 516 |
+
# middle
|
| 517 |
+
self.mid = nn.Module()
|
| 518 |
+
self.mid.block_1 = Emu3VisionVQResnetBlock(
|
| 519 |
+
in_channels=block_in,
|
| 520 |
+
out_channels=block_in,
|
| 521 |
+
dropout=config.dropout,
|
| 522 |
+
)
|
| 523 |
+
self.mid.attn_1 = Emu3VisionVQAttnBlock(block_in)
|
| 524 |
+
self.mid.block_2 = Emu3VisionVQResnetBlock(
|
| 525 |
+
in_channels=block_in,
|
| 526 |
+
out_channels=block_in,
|
| 527 |
+
dropout=config.dropout,
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
+
# end
|
| 531 |
+
self.norm_out = nn.GroupNorm(num_channels=block_in, num_groups=32, eps=1e-6, affine=True)
|
| 532 |
+
|
| 533 |
+
out_z_channels = 2 * config.z_channels if config.double_z else config.z_channels
|
| 534 |
+
self.conv_out = nn.Conv2d(
|
| 535 |
+
block_in,
|
| 536 |
+
out_z_channels,
|
| 537 |
+
kernel_size=3,
|
| 538 |
+
stride=1,
|
| 539 |
+
padding=1,
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
temporal_down_blocks = int(math.log2(config.temporal_downsample_factor))
|
| 543 |
+
self.time_conv = nn.ModuleList()
|
| 544 |
+
|
| 545 |
+
for i in range(temporal_down_blocks):
|
| 546 |
+
conv = Emu3VisionVQTemporalDownsample(out_z_channels, out_z_channels)
|
| 547 |
+
self.time_conv.append(conv)
|
| 548 |
+
|
| 549 |
+
self.time_res_stack = nn.Sequential(*[
|
| 550 |
+
Emu3VisionVQResnetTemporalBlock(
|
| 551 |
+
in_channels=out_z_channels,
|
| 552 |
+
out_channels=out_z_channels,
|
| 553 |
+
dropout=config.dropout,
|
| 554 |
+
) for _ in range(self.num_res_blocks)
|
| 555 |
+
])
|
| 556 |
+
|
| 557 |
+
self.act = Emu3VisionVQActivation()
|
| 558 |
+
|
| 559 |
+
def forward(self, x: torch.Tensor):
|
| 560 |
+
t = x.shape[1]
|
| 561 |
+
x = x.reshape(-1, *x.shape[2:])
|
| 562 |
+
|
| 563 |
+
# downsampling
|
| 564 |
+
h = self.conv_in(x)
|
| 565 |
+
for i_level in range(self.num_resolutions):
|
| 566 |
+
for i_block in range(self.num_res_blocks):
|
| 567 |
+
h = self.down[i_level].block[i_block](h)
|
| 568 |
+
if len(self.down[i_level].attn) > 0:
|
| 569 |
+
h = self.down[i_level].attn[i_block](h)
|
| 570 |
+
|
| 571 |
+
if i_level != self.num_resolutions - 1:
|
| 572 |
+
h = self.down[i_level].downsample(h)
|
| 573 |
+
|
| 574 |
+
h = self.mid.block_1(h)
|
| 575 |
+
h = self.mid.attn_1(h)
|
| 576 |
+
h = self.mid.block_2(h)
|
| 577 |
+
|
| 578 |
+
# end
|
| 579 |
+
h = self.norm_out(h)
|
| 580 |
+
h = self.act(h)
|
| 581 |
+
|
| 582 |
+
h = self.conv_out(h)
|
| 583 |
+
|
| 584 |
+
h = h.reshape(-1, t, *h.shape[1:])
|
| 585 |
+
h = h.permute(0, 2, 1, 3, 4)
|
| 586 |
+
|
| 587 |
+
for conv in self.time_conv:
|
| 588 |
+
h = self.act(conv(h))
|
| 589 |
+
|
| 590 |
+
h = self.time_res_stack(h)
|
| 591 |
+
h = h.permute(0, 2, 1, 3, 4)
|
| 592 |
+
|
| 593 |
+
return h
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
class Emu3VisionVQDecoder(nn.Module):
|
| 597 |
+
|
| 598 |
+
def __init__(self, config: Emu3VisionVQConfig):
|
| 599 |
+
super().__init__()
|
| 600 |
+
self.ch = config.ch
|
| 601 |
+
self.num_resolutions = len(config.ch_mult)
|
| 602 |
+
self.num_res_blocks = config.num_res_blocks
|
| 603 |
+
|
| 604 |
+
in_ch_mult = (1,) + tuple(config.ch_mult)
|
| 605 |
+
zq_ch = config.embed_dim
|
| 606 |
+
|
| 607 |
+
block_in = config.ch * config.ch_mult[-1]
|
| 608 |
+
self.time_res_stack = nn.Sequential(*[
|
| 609 |
+
Emu3VisionVQResnetTemporalBlock(
|
| 610 |
+
in_channels=config.z_channels,
|
| 611 |
+
out_channels=config.z_channels,
|
| 612 |
+
dropout=config.dropout,
|
| 613 |
+
) for _ in range(config.num_res_blocks)
|
| 614 |
+
])
|
| 615 |
+
|
| 616 |
+
tempo_upsample_block_num = int(math.log2(config.temporal_downsample_factor))
|
| 617 |
+
self.time_conv = nn.ModuleList()
|
| 618 |
+
for i in range(tempo_upsample_block_num):
|
| 619 |
+
conv = Emu3VisionVQTemporalUpsample(config.z_channels, config.z_channels)
|
| 620 |
+
self.time_conv.append(conv)
|
| 621 |
+
|
| 622 |
+
self.conv_in = nn.Conv2d(
|
| 623 |
+
config.z_channels,
|
| 624 |
+
block_in,
|
| 625 |
+
kernel_size=3,
|
| 626 |
+
stride=1,
|
| 627 |
+
padding=1,
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
# middle
|
| 631 |
+
self.mid = nn.Module()
|
| 632 |
+
self.mid.block_1 = Emu3VisionVQResnetBlock(
|
| 633 |
+
in_channels=block_in,
|
| 634 |
+
out_channels=block_in,
|
| 635 |
+
dropout=config.dropout,
|
| 636 |
+
zq_ch=zq_ch,
|
| 637 |
+
)
|
| 638 |
+
self.mid.attn_1 = Emu3VisionVQAttnBlock(block_in, zq_ch)
|
| 639 |
+
self.mid.block_2 = Emu3VisionVQResnetBlock(
|
| 640 |
+
in_channels=block_in,
|
| 641 |
+
out_channels=block_in,
|
| 642 |
+
dropout=config.dropout,
|
| 643 |
+
zq_ch=zq_ch,
|
| 644 |
+
)
|
| 645 |
+
|
| 646 |
+
# upsampling
|
| 647 |
+
self.up = nn.ModuleList()
|
| 648 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 649 |
+
block = nn.ModuleList()
|
| 650 |
+
attn = nn.ModuleList()
|
| 651 |
+
block_out = config.ch * config.ch_mult[i_level]
|
| 652 |
+
for i_block in range(self.num_res_blocks + 1):
|
| 653 |
+
block.append(
|
| 654 |
+
Emu3VisionVQResnetBlock(
|
| 655 |
+
in_channels=block_in,
|
| 656 |
+
out_channels=block_out,
|
| 657 |
+
dropout=config.dropout,
|
| 658 |
+
zq_ch=zq_ch,
|
| 659 |
+
)
|
| 660 |
+
)
|
| 661 |
+
block_in = block_out
|
| 662 |
+
if i_level in config.attn_resolutions:
|
| 663 |
+
attn.append(Emu3VisionVQAttnBlock(block_in, zq_ch))
|
| 664 |
+
|
| 665 |
+
up = nn.Module()
|
| 666 |
+
up.block = block
|
| 667 |
+
up.attn = attn
|
| 668 |
+
if i_level != 0:
|
| 669 |
+
up.upsample = Emu3VisionVQUpsample(block_in)
|
| 670 |
+
|
| 671 |
+
self.up.insert(0, up)
|
| 672 |
+
|
| 673 |
+
self.act = Emu3VisionVQActivation()
|
| 674 |
+
|
| 675 |
+
self.norm_out = Emu3VisionVQSpatialNorm(block_in, zq_ch)
|
| 676 |
+
self.conv_out = nn.Conv2d(
|
| 677 |
+
block_in,
|
| 678 |
+
config.out_channels,
|
| 679 |
+
kernel_size=3,
|
| 680 |
+
stride=1,
|
| 681 |
+
padding=1,
|
| 682 |
+
)
|
| 683 |
+
|
| 684 |
+
def forward(self, z: torch.Tensor, zq: torch.Tensor):
|
| 685 |
+
z_zq = torch.cat((z, zq), dim=0)
|
| 686 |
+
z_zq = z_zq.permute(0, 2, 1, 3, 4)
|
| 687 |
+
z_zq = self.time_res_stack(z_zq)
|
| 688 |
+
|
| 689 |
+
for conv in self.time_conv:
|
| 690 |
+
z_zq = self.act(conv(z_zq))
|
| 691 |
+
|
| 692 |
+
z_zq = z_zq.permute(0, 2, 1, 3, 4)
|
| 693 |
+
|
| 694 |
+
h, zq = torch.chunk(z_zq, 2, dim=0)
|
| 695 |
+
|
| 696 |
+
h = h.reshape(-1, *h.shape[2:])
|
| 697 |
+
zq = zq.reshape(-1, *zq.shape[2:])
|
| 698 |
+
|
| 699 |
+
h = self.conv_in(h)
|
| 700 |
+
|
| 701 |
+
# middle
|
| 702 |
+
h = self.mid.block_1(h, zq)
|
| 703 |
+
h = self.mid.attn_1(h, zq)
|
| 704 |
+
h = self.mid.block_2(h, zq)
|
| 705 |
+
|
| 706 |
+
# upsampling
|
| 707 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 708 |
+
for i_block in range(self.num_res_blocks+1):
|
| 709 |
+
h = self.up[i_level].block[i_block](h, zq)
|
| 710 |
+
if len(self.up[i_level].attn) > 0:
|
| 711 |
+
h = self.up[i_level].attn[i_block](h, zq)
|
| 712 |
+
|
| 713 |
+
if i_level != 0:
|
| 714 |
+
h = self.up[i_level].upsample(h)
|
| 715 |
+
|
| 716 |
+
h = self.norm_out(h, zq)
|
| 717 |
+
h = self.act(h)
|
| 718 |
+
h = self.conv_out(h)
|
| 719 |
+
|
| 720 |
+
return h
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
class Emu3VisionVQPretrainedModel(PreTrainedModel):
|
| 724 |
+
"""
|
| 725 |
+
An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained
|
| 726 |
+
models.
|
| 727 |
+
"""
|
| 728 |
+
|
| 729 |
+
config_class = Emu3VisionVQConfig
|
| 730 |
+
base_model_prefix = "emuvideovq"
|
| 731 |
+
main_input_name = "pixel_values"
|
| 732 |
+
_no_split_modules = ["Emu3VisionVQResnetBlock", "Emu3VisionVQAttnBlock", "Emu3VisionVQResnetTemporalBlock"]
|
| 733 |
+
|
| 734 |
+
def _init_weights(self, module):
|
| 735 |
+
if isinstance(module, (nn.Conv2d, nn.Conv3d)):
|
| 736 |
+
nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
|
| 737 |
+
# copied from the `reset_parameters` method of `class Linear(Module)` in `torch`.
|
| 738 |
+
elif isinstance(module, nn.Linear):
|
| 739 |
+
nn.init.kaiming_uniform_(module.weight, a=math.sqrt(5))
|
| 740 |
+
if module.bias is not None:
|
| 741 |
+
fan_in, _ = nn.init._calculate_fan_in_and_fan_out(module.weight)
|
| 742 |
+
bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
|
| 743 |
+
nn.init.uniform_(module.bias, -bound, bound)
|
| 744 |
+
elif isinstance(module, (nn.BatchNorm2d, nn.BatchNorm3d, nn.GroupNorm)):
|
| 745 |
+
nn.init.constant_(module.weight, 1)
|
| 746 |
+
nn.init.constant_(module.bias, 0)
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
class Emu3VisionVQModel(Emu3VisionVQPretrainedModel):
|
| 750 |
+
|
| 751 |
+
def __init__(self, config):
|
| 752 |
+
super().__init__(config)
|
| 753 |
+
self.config = config
|
| 754 |
+
|
| 755 |
+
self.encoder = Emu3VisionVQEncoder(config)
|
| 756 |
+
self.decoder = Emu3VisionVQDecoder(config)
|
| 757 |
+
self.quantize = Emu3VisionVQVectorQuantizer(config)
|
| 758 |
+
|
| 759 |
+
self.quant_conv = Emu3VisionVQCausalConv3d(config.z_channels, config.embed_dim)
|
| 760 |
+
self.post_quant_conv = Emu3VisionVQCausalConv3d(config.embed_dim, config.z_channels)
|
| 761 |
+
|
| 762 |
+
self.spatial_scale_factor = 2 ** (len(config.ch_mult) - 1)
|
| 763 |
+
|
| 764 |
+
self.post_init()
|
| 765 |
+
|
| 766 |
+
def encode(self, x: torch.Tensor):
|
| 767 |
+
ndim = x.ndim
|
| 768 |
+
if ndim == 4:
|
| 769 |
+
t = self.config.temporal_downsample_factor
|
| 770 |
+
b, c, h, w = x.shape
|
| 771 |
+
x = x.unsqueeze(1).repeat(1, t, 1, 1, 1)
|
| 772 |
+
elif ndim == 5:
|
| 773 |
+
b, t, c, h, w = x.shape
|
| 774 |
+
|
| 775 |
+
h = self.encoder(x)
|
| 776 |
+
|
| 777 |
+
# b t c h w -> b c t h w
|
| 778 |
+
h = h.permute(0, 2, 1, 3, 4)
|
| 779 |
+
h = self.quant_conv(h)
|
| 780 |
+
# b c t h w -> b t c h w
|
| 781 |
+
h = h.permute(0, 2, 1, 3, 4)
|
| 782 |
+
|
| 783 |
+
codes = self.quantize(h)
|
| 784 |
+
|
| 785 |
+
if ndim == 4:
|
| 786 |
+
codes = codes.squeeze(1)
|
| 787 |
+
|
| 788 |
+
return codes
|
| 789 |
+
|
| 790 |
+
def decode(self, x: torch.Tensor):
|
| 791 |
+
ndim = x.ndim
|
| 792 |
+
if ndim == 3:
|
| 793 |
+
x = x.unsqueeze(1)
|
| 794 |
+
|
| 795 |
+
b, t, h, w = x.shape
|
| 796 |
+
quant = self.quantize.embedding(x.flatten())
|
| 797 |
+
c = quant.shape[-1]
|
| 798 |
+
quant = quant.view(b, t, h, w, c).permute(0, 4, 1, 2, 3).contiguous()
|
| 799 |
+
quant2 = self.post_quant_conv(quant)
|
| 800 |
+
|
| 801 |
+
quant = quant.permute(0, 2, 1, 3, 4)
|
| 802 |
+
quant2 = quant2.permute(0, 2, 1, 3, 4)
|
| 803 |
+
|
| 804 |
+
video = self.decoder(quant2, quant)
|
| 805 |
+
video = video.reshape(
|
| 806 |
+
b,
|
| 807 |
+
t * self.config.temporal_downsample_factor,
|
| 808 |
+
self.config.out_channels,
|
| 809 |
+
h * self.spatial_scale_factor,
|
| 810 |
+
w * self.spatial_scale_factor,
|
| 811 |
+
)
|
| 812 |
+
if ndim == 3:
|
| 813 |
+
return video[:, 0]
|
| 814 |
+
return video
|
| 815 |
+
|
| 816 |
+
@property
|
| 817 |
+
def device(self):
|
| 818 |
+
return next(self.parameters()).device
|
| 819 |
+
|
| 820 |
+
@property
|
| 821 |
+
def dtype(self):
|
| 822 |
+
return next(self.parameters()).dtype
|
codecs/vision/preprocessor_config.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"auto_map": {
|
| 3 |
+
"AutoImageProcessor": "image_processing_emu3visionvq.Emu3VisionVQImageProcessor"
|
| 4 |
+
},
|
| 5 |
+
"do_convert_rgb": true,
|
| 6 |
+
"do_normalize": true,
|
| 7 |
+
"do_rescale": true,
|
| 8 |
+
"do_resize": true,
|
| 9 |
+
"image_mean": [
|
| 10 |
+
0.5,
|
| 11 |
+
0.5,
|
| 12 |
+
0.5
|
| 13 |
+
],
|
| 14 |
+
"image_processor_type": "Emu3VisionVQImageProcessor",
|
| 15 |
+
"image_std": [
|
| 16 |
+
0.5,
|
| 17 |
+
0.5,
|
| 18 |
+
0.5
|
| 19 |
+
],
|
| 20 |
+
"max_pixels": 1048576,
|
| 21 |
+
"min_pixels": 262144,
|
| 22 |
+
"resample": 3,
|
| 23 |
+
"rescale_factor": 0.00392156862745098,
|
| 24 |
+
"size": {
|
| 25 |
+
"max_pixels": 1048576,
|
| 26 |
+
"min_pixels": 262144
|
| 27 |
+
},
|
| 28 |
+
"spatial_factor": 8
|
| 29 |
+
}
|
config.json
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": {
|
| 3 |
+
"architecture": "axiom1_1",
|
| 4 |
+
"vocab_size": 54144,
|
| 5 |
+
"d_model": 640,
|
| 6 |
+
"n_layers": 12,
|
| 7 |
+
"n_heads": 10,
|
| 8 |
+
"n_kv_heads": 10,
|
| 9 |
+
"ffn_mult": 2.6666666666666665,
|
| 10 |
+
"max_seq_len": 1024,
|
| 11 |
+
"rope_theta": 10000.0,
|
| 12 |
+
"norm_eps": 1e-06,
|
| 13 |
+
"vis_positions": 256,
|
| 14 |
+
"visual_offset": 16640,
|
| 15 |
+
"visual_size": 32768,
|
| 16 |
+
"tie_embeddings": true,
|
| 17 |
+
"init_std": 0.02,
|
| 18 |
+
"grad_checkpoint": true,
|
| 19 |
+
"attention_layers": [
|
| 20 |
+
3,
|
| 21 |
+
7,
|
| 22 |
+
11
|
| 23 |
+
],
|
| 24 |
+
"d_cell": 1280,
|
| 25 |
+
"n_cells": 2,
|
| 26 |
+
"top_k": 2,
|
| 27 |
+
"scan_chunk": 64,
|
| 28 |
+
"balance_weight": 0.01
|
| 29 |
+
},
|
| 30 |
+
"vocab": {
|
| 31 |
+
"text_size": 16384,
|
| 32 |
+
"visual_size": 32768,
|
| 33 |
+
"audio_size": 1024,
|
| 34 |
+
"audio_books": 4,
|
| 35 |
+
"coordinate_bins": 512,
|
| 36 |
+
"max_vertices": 128,
|
| 37 |
+
"max_faces": 128,
|
| 38 |
+
"frame_tokens": 64,
|
| 39 |
+
"max_frames": 8,
|
| 40 |
+
"max_audio_frames": 160,
|
| 41 |
+
"schema_version": 3,
|
| 42 |
+
"special_tokens": [
|
| 43 |
+
"pad",
|
| 44 |
+
"bos",
|
| 45 |
+
"eos",
|
| 46 |
+
"input",
|
| 47 |
+
"output",
|
| 48 |
+
"text",
|
| 49 |
+
"/text",
|
| 50 |
+
"image",
|
| 51 |
+
"/image",
|
| 52 |
+
"video",
|
| 53 |
+
"/video",
|
| 54 |
+
"frame",
|
| 55 |
+
"/frame",
|
| 56 |
+
"audio",
|
| 57 |
+
"/audio",
|
| 58 |
+
"mesh",
|
| 59 |
+
"/mesh",
|
| 60 |
+
"vertices",
|
| 61 |
+
"faces",
|
| 62 |
+
"tools",
|
| 63 |
+
"/tools",
|
| 64 |
+
"calls",
|
| 65 |
+
"/calls",
|
| 66 |
+
"task:text",
|
| 67 |
+
"task:chat_text",
|
| 68 |
+
"task:t2i",
|
| 69 |
+
"task:i2t",
|
| 70 |
+
"task:t2v",
|
| 71 |
+
"task:v2t",
|
| 72 |
+
"task:vcont",
|
| 73 |
+
"task:t2a",
|
| 74 |
+
"task:a2t",
|
| 75 |
+
"task:t2m",
|
| 76 |
+
"task:m2t",
|
| 77 |
+
"task:tools",
|
| 78 |
+
"task:mixed",
|
| 79 |
+
"task:coding",
|
| 80 |
+
"time:0",
|
| 81 |
+
"time:1",
|
| 82 |
+
"time:2",
|
| 83 |
+
"time:3",
|
| 84 |
+
"time:4",
|
| 85 |
+
"time:5",
|
| 86 |
+
"time:6",
|
| 87 |
+
"time:7",
|
| 88 |
+
"resolution:128"
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
"meta": {
|
| 92 |
+
"stage": "joint_repair",
|
| 93 |
+
"step": 512,
|
| 94 |
+
"tokens_seen": 807174144,
|
| 95 |
+
"task_target_tokens": {
|
| 96 |
+
"coding": 373488,
|
| 97 |
+
"chat_text": 24092354,
|
| 98 |
+
"text": 257928814,
|
| 99 |
+
"i2t": 11852250,
|
| 100 |
+
"t2i": 39056667,
|
| 101 |
+
"vcont": 6105359,
|
| 102 |
+
"v2t": 252329,
|
| 103 |
+
"t2v": 19027778,
|
| 104 |
+
"t2m": 5739237,
|
| 105 |
+
"a2t": 207450,
|
| 106 |
+
"tools": 1430210,
|
| 107 |
+
"m2t": 477100,
|
| 108 |
+
"t2a": 10952145,
|
| 109 |
+
"replay/chat_text": 66428,
|
| 110 |
+
"replay/text": 225433,
|
| 111 |
+
"coverage/vcont": 52303,
|
| 112 |
+
"coverage/t2v": 384846
|
| 113 |
+
},
|
| 114 |
+
"stage_task_target_tokens": {
|
| 115 |
+
"coverage/vcont": 52303,
|
| 116 |
+
"t2i": 481000,
|
| 117 |
+
"coverage/t2v": 384846,
|
| 118 |
+
"replay/chat_text": 23241,
|
| 119 |
+
"replay/text": 74112
|
| 120 |
+
},
|
| 121 |
+
"stages_completed": [
|
| 122 |
+
"pilot",
|
| 123 |
+
"text",
|
| 124 |
+
"image_pilot",
|
| 125 |
+
"image",
|
| 126 |
+
"video",
|
| 127 |
+
"audio_mesh_pilot",
|
| 128 |
+
"audio_mesh",
|
| 129 |
+
"unified",
|
| 130 |
+
"pilot",
|
| 131 |
+
"image",
|
| 132 |
+
"video_coverage",
|
| 133 |
+
"joint_repair"
|
| 134 |
+
],
|
| 135 |
+
"num_params": 90247794,
|
| 136 |
+
"precision": "bf16",
|
| 137 |
+
"image_128_trained": true,
|
| 138 |
+
"image_128_target_tokens": 16775415,
|
| 139 |
+
"env": {
|
| 140 |
+
"torch": "2.11.0+cu128",
|
| 141 |
+
"cuda": "12.8",
|
| 142 |
+
"python": "3.13.15",
|
| 143 |
+
"gpu": "NVIDIA GeForce RTX 5070"
|
| 144 |
+
},
|
| 145 |
+
"research_only": true,
|
| 146 |
+
"research_trial": "joint_repair",
|
| 147 |
+
"research_note": "Experimental branch; not a production promotion or validated semantic capability.",
|
| 148 |
+
"data": {
|
| 149 |
+
"preparation": {
|
| 150 |
+
"audio_codec": {
|
| 151 |
+
"path": "codecs/audio",
|
| 152 |
+
"revision": "6ba020b5ba7d9d8076fb90db7e67f27e31980f6e",
|
| 153 |
+
"sha256": {
|
| 154 |
+
"config.json": "3af86549c509e5491367dab3f7093014bd559f441ba31fa7427b5f94232368d7",
|
| 155 |
+
"model.safetensors": "7452e3fc6972991da871ae1a1c9d3e8e219aa247cc0f757018867d6a7fe59aaf",
|
| 156 |
+
"README.md": "6b1db50f4974b1544fb795491b6fd57a20320ff17686565c50a08407f06075b1"
|
| 157 |
+
},
|
| 158 |
+
"codebooks": 4,
|
| 159 |
+
"boundary_guard_samples": 320,
|
| 160 |
+
"alignment_samples": -8,
|
| 161 |
+
"admission_sha256": "7968bc0f22c01b433bd516acc4562ed9ef7543a6e93b8c7c7d7d697a7157bf62",
|
| 162 |
+
"implementation": "quasar.tokenizers.audio_aligned.AlignedAudioTokenizer"
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"release_provenance": {
|
| 167 |
+
"source_weights_sha256": "de4846e5e63c0004cf0b6331127ab421d6843f6efde04243c6d5448ab4545240",
|
| 168 |
+
"original_config_sha256": "1d7254fcaaca82c66d2eab32ba39cabf673c9c63ed16aabede5d6b1092aeabb1",
|
| 169 |
+
"runtime_source_commit": "4afbdccdbd3b0967b161a68658cfd3b92c42636f",
|
| 170 |
+
"metadata_changes": "Portable codec paths; runtime-required metadata retained; workspace paths and preparation-only metadata omitted.",
|
| 171 |
+
"weights_changed": false
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
}
|
files.sha256.json
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"config.json": {
|
| 3 |
+
"bytes": 4547,
|
| 4 |
+
"sha256": "0a5a60a1af2eb406e8195beed8fb8602aade8252dd3c5ef5bb9c8d4c7a20083c"
|
| 5 |
+
},
|
| 6 |
+
"evaluation.json": {
|
| 7 |
+
"bytes": 3456,
|
| 8 |
+
"sha256": "70681a6e629a7731f74d3dfa8c0b5692ba61a7cfb520ba307119d5dd20237bd6"
|
| 9 |
+
},
|
| 10 |
+
"infer.py": {
|
| 11 |
+
"bytes": 2730,
|
| 12 |
+
"sha256": "cdf04813be3d8505cefbaeeee604ff53d022e09fbec7fc321af3544d295b95e6"
|
| 13 |
+
},
|
| 14 |
+
"model.safetensors": {
|
| 15 |
+
"bytes": 361008568,
|
| 16 |
+
"sha256": "de4846e5e63c0004cf0b6331127ab421d6843f6efde04243c6d5448ab4545240"
|
| 17 |
+
},
|
| 18 |
+
"pyproject.toml": {
|
| 19 |
+
"bytes": 784,
|
| 20 |
+
"sha256": "320d43b1c10d23a898b64a6fd66253c78b3bbb7a74fd2c4d0f60b40fe3d9943a"
|
| 21 |
+
},
|
| 22 |
+
"README.md": {
|
| 23 |
+
"bytes": 16894,
|
| 24 |
+
"sha256": "ed5b08fa100594fc8c795a6b8188d99d0563ca736ddcc2cb071c67c909a266c2"
|
| 25 |
+
},
|
| 26 |
+
"release.json": {
|
| 27 |
+
"bytes": 783,
|
| 28 |
+
"sha256": "2734d29b5d10e09c5de8c6f2cbcc27f205ae1bedc8d46638030262b5aae84896"
|
| 29 |
+
},
|
| 30 |
+
"release_validation.json": {
|
| 31 |
+
"bytes": 660,
|
| 32 |
+
"sha256": "ffeb7f7738e459e1c9132d284072d39d367b04c751079f7d67d4746ca34c005e"
|
| 33 |
+
},
|
| 34 |
+
"requirements-inference.txt": {
|
| 35 |
+
"bytes": 256,
|
| 36 |
+
"sha256": "4d38e10bd30904cec18c0025edbe57299ff8034243ed9b670e7ace7657e97b8d"
|
| 37 |
+
},
|
| 38 |
+
"verify_release.py": {
|
| 39 |
+
"bytes": 4126,
|
| 40 |
+
"sha256": "af41074f5c74ff4090be59bb1d656f5238bd1d31d2c1b2d4612990f1eef7285a"
|
| 41 |
+
},
|
| 42 |
+
"assets/attributions.json": {
|
| 43 |
+
"bytes": 2391,
|
| 44 |
+
"sha256": "afd033dfe7cc8822899d9946ebec9bb872314127e4ec864e99a03b3fed5e62e3"
|
| 45 |
+
},
|
| 46 |
+
"assets/image_comparison.png": {
|
| 47 |
+
"bytes": 210993,
|
| 48 |
+
"sha256": "49f61b6696e34dc3589c3cf24aa4da55a5475a369bfbf5ccdefb602a0b41a216"
|
| 49 |
+
},
|
| 50 |
+
"assets/motion_comparison.gif": {
|
| 51 |
+
"bytes": 281900,
|
| 52 |
+
"sha256": "9184db2e2abe62c33c57de75d08d5c8bf6273600dfadc80842c5ecd5fcfce436"
|
| 53 |
+
},
|
| 54 |
+
"quasar/__init__.py": {
|
| 55 |
+
"bytes": 0,
|
| 56 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 57 |
+
},
|
| 58 |
+
"tokenizers/text_tokenizer.json": {
|
| 59 |
+
"bytes": 1136009,
|
| 60 |
+
"sha256": "bf620532636ec536d19191eddde091645856e1d6f262d52fdc9f7e9b43ee843a"
|
| 61 |
+
},
|
| 62 |
+
"variants/v5/config.json": {
|
| 63 |
+
"bytes": 4351,
|
| 64 |
+
"sha256": "5e10caa317356974650e2fa9ecc5bc39db73237ed08a0bcbf7f7a61fedc66c63"
|
| 65 |
+
},
|
| 66 |
+
"variants/v5/model.safetensors": {
|
| 67 |
+
"bytes": 361008568,
|
| 68 |
+
"sha256": "e20242e9971b6a7cef7ca23a65c25faa70a007807a55a2c0cfe35f4f5682a643"
|
| 69 |
+
},
|
| 70 |
+
"variants/v5/tokenizers/text_tokenizer.json": {
|
| 71 |
+
"bytes": 1136009,
|
| 72 |
+
"sha256": "bf620532636ec536d19191eddde091645856e1d6f262d52fdc9f7e9b43ee843a"
|
| 73 |
+
},
|
| 74 |
+
"variants/v5/tokenizers/vq/vq.safetensors": {
|
| 75 |
+
"bytes": 524448,
|
| 76 |
+
"sha256": "0ed59aaface626b762214262572da4d9b0eb841175859fe26c925e606a82069b"
|
| 77 |
+
},
|
| 78 |
+
"variants/v5/tokenizers/vq/vq_config.json": {
|
| 79 |
+
"bytes": 2186,
|
| 80 |
+
"sha256": "4eae4e47af92536dacc30559498eed3c1318a80fbf603ab6a3a5da6e500e8448"
|
| 81 |
+
},
|
| 82 |
+
"tokenizers/vq/vq.safetensors": {
|
| 83 |
+
"bytes": 524448,
|
| 84 |
+
"sha256": "0ed59aaface626b762214262572da4d9b0eb841175859fe26c925e606a82069b"
|
| 85 |
+
},
|
| 86 |
+
"tokenizers/vq/vq_config.json": {
|
| 87 |
+
"bytes": 2180,
|
| 88 |
+
"sha256": "a720078321fd0c30cdedc241ca47c05474d5e91945bb4f1feb100452dffe03de"
|
| 89 |
+
},
|
| 90 |
+
"quasar/data/adapters.py": {
|
| 91 |
+
"bytes": 5338,
|
| 92 |
+
"sha256": "93dcfca754d0d5d58fefef7c23eb49f91538ad822231422ede0ad2aafe36a1ab"
|
| 93 |
+
},
|
| 94 |
+
"quasar/data/collator.py": {
|
| 95 |
+
"bytes": 1444,
|
| 96 |
+
"sha256": "dbc6b411df217460ef6578394d4ca3f1a4f6e8be672cdc71771eaf28b1a4a675"
|
| 97 |
+
},
|
| 98 |
+
"quasar/data/curated.py": {
|
| 99 |
+
"bytes": 11234,
|
| 100 |
+
"sha256": "036bcb4f95a90c3bc338deae2491fc5c416acd3454a862494347712e08c7737e"
|
| 101 |
+
},
|
| 102 |
+
"quasar/data/datasets.py": {
|
| 103 |
+
"bytes": 13870,
|
| 104 |
+
"sha256": "2a0fccbb576c69351e12e9a60f1c74348cce385e6d06a1786ec87f8bb82f3785"
|
| 105 |
+
},
|
| 106 |
+
"quasar/data/download.py": {
|
| 107 |
+
"bytes": 9387,
|
| 108 |
+
"sha256": "e98607b44e557c6b9a86ed1d06d487ac810ca2f7458c29f08abbb6fd5a15f19f"
|
| 109 |
+
},
|
| 110 |
+
"quasar/data/preprocessing.py": {
|
| 111 |
+
"bytes": 11794,
|
| 112 |
+
"sha256": "5a802f8fc5da21699136ffd9941e556474200a3f5038667a1e90293b3af0de25"
|
| 113 |
+
},
|
| 114 |
+
"quasar/data/synthetic.py": {
|
| 115 |
+
"bytes": 18794,
|
| 116 |
+
"sha256": "4f6046fa0fa8fb426820c8970628ff723d84c6b9539cbf1e02f3bc80b8f3bd05"
|
| 117 |
+
},
|
| 118 |
+
"quasar/data/text_preparation.py": {
|
| 119 |
+
"bytes": 29675,
|
| 120 |
+
"sha256": "777fb661fa86ef7602e0fca0a707efbc3e1eaa168de3fbda2177506462051fac"
|
| 121 |
+
},
|
| 122 |
+
"quasar/data/__init__.py": {
|
| 123 |
+
"bytes": 0,
|
| 124 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 125 |
+
},
|
| 126 |
+
"quasar/evaluation/__init__.py": {
|
| 127 |
+
"bytes": 0,
|
| 128 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 129 |
+
},
|
| 130 |
+
"quasar/inference/decoding.py": {
|
| 131 |
+
"bytes": 7170,
|
| 132 |
+
"sha256": "936f834f079dfb2763091897095d4cc65a5e6b6ff16fcbe844e60cdf95bd4152"
|
| 133 |
+
},
|
| 134 |
+
"quasar/inference/extended.py": {
|
| 135 |
+
"bytes": 8322,
|
| 136 |
+
"sha256": "5ce1f4bd1023f3fb77b1853e2f36d5410ae0595349bf5f9d03aae99d52f1ebab"
|
| 137 |
+
},
|
| 138 |
+
"quasar/inference/generate.py": {
|
| 139 |
+
"bytes": 16454,
|
| 140 |
+
"sha256": "00a31f84a6917d58d3ebbc09bb04e796b98aa8fc8aef8e32d278652e5aa24e24"
|
| 141 |
+
},
|
| 142 |
+
"quasar/inference/universal.py": {
|
| 143 |
+
"bytes": 6151,
|
| 144 |
+
"sha256": "50569a27c60c91d5b5a998e018f2e18d63b9483b082faa7225e4ec6a808120eb"
|
| 145 |
+
},
|
| 146 |
+
"quasar/inference/__init__.py": {
|
| 147 |
+
"bytes": 0,
|
| 148 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 149 |
+
},
|
| 150 |
+
"quasar/model/attention.py": {
|
| 151 |
+
"bytes": 2905,
|
| 152 |
+
"sha256": "22bfd305f5dba981438913d0a8418e4ec9d5e62672740e458db1ce2e8c11585c"
|
| 153 |
+
},
|
| 154 |
+
"quasar/model/axiom.py": {
|
| 155 |
+
"bytes": 6040,
|
| 156 |
+
"sha256": "1919f42a57cf3f4f0601cb79846f7fe91af826b802a188c63cf0c1a02a4e539d"
|
| 157 |
+
},
|
| 158 |
+
"quasar/model/config.py": {
|
| 159 |
+
"bytes": 3031,
|
| 160 |
+
"sha256": "be2218d6e051120af3be8ee53aa0ccdd052a316588e69c7d8684540f029d0591"
|
| 161 |
+
},
|
| 162 |
+
"quasar/model/layers.py": {
|
| 163 |
+
"bytes": 1704,
|
| 164 |
+
"sha256": "859edfce4cce98453da2bd6bc3e24b45ad188d908d3351e0074e90cf55ab4b9f"
|
| 165 |
+
},
|
| 166 |
+
"quasar/model/transformer.py": {
|
| 167 |
+
"bytes": 6531,
|
| 168 |
+
"sha256": "43ff8614eeb1e04d0d69e20cf235e1e0b5c1a4b8c022554ec21e2a4033440c51"
|
| 169 |
+
},
|
| 170 |
+
"quasar/model/__init__.py": {
|
| 171 |
+
"bytes": 0,
|
| 172 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 173 |
+
},
|
| 174 |
+
"quasar/tokenizers/audio.py": {
|
| 175 |
+
"bytes": 2726,
|
| 176 |
+
"sha256": "e587422428a3925841754b7a8a928abd0c3bb0fa82bd259b8a17534075f93a55"
|
| 177 |
+
},
|
| 178 |
+
"quasar/tokenizers/audio_aligned.py": {
|
| 179 |
+
"bytes": 1852,
|
| 180 |
+
"sha256": "d32dbd18faa8eb75b40f7ae1cc64d7e5fffa8f634bb2998f399ed1c0f24adfb2"
|
| 181 |
+
},
|
| 182 |
+
"quasar/tokenizers/audio_complete.py": {
|
| 183 |
+
"bytes": 1068,
|
| 184 |
+
"sha256": "d50959abc6130c58d4935b4846bc2916475cd51cd26a35a4bcad498d7f51917d"
|
| 185 |
+
},
|
| 186 |
+
"quasar/tokenizers/mesh.py": {
|
| 187 |
+
"bytes": 2144,
|
| 188 |
+
"sha256": "148cc80bcb900eed289926b3130dcd1d35f1eae2c000511d13a80f3de056ed26"
|
| 189 |
+
},
|
| 190 |
+
"quasar/tokenizers/multimodal.py": {
|
| 191 |
+
"bytes": 9485,
|
| 192 |
+
"sha256": "5c895527a02624489e7c4ced5912eb2cf0d41912945071ff6795770d740ca5a8"
|
| 193 |
+
},
|
| 194 |
+
"quasar/tokenizers/multires_emu.py": {
|
| 195 |
+
"bytes": 2840,
|
| 196 |
+
"sha256": "764a04ff63106146cbc0d910b5ff293baf54335ffa93305531d2b971c77da353"
|
| 197 |
+
},
|
| 198 |
+
"quasar/tokenizers/restricted_emu.py": {
|
| 199 |
+
"bytes": 2511,
|
| 200 |
+
"sha256": "add63d1042dc72dadfd79ec0ad3796e1a6cac2a9023ebf498a54a1d6d3ab8bf8"
|
| 201 |
+
},
|
| 202 |
+
"quasar/tokenizers/text.py": {
|
| 203 |
+
"bytes": 1781,
|
| 204 |
+
"sha256": "d97bc0c634dab8318fc7528def8d9fb1eb17e21da0c92e0812368c02d313135b"
|
| 205 |
+
},
|
| 206 |
+
"quasar/tokenizers/universal.py": {
|
| 207 |
+
"bytes": 12299,
|
| 208 |
+
"sha256": "1607e4c9b7eeefe8ee7eacc70bbf53a2bdf934f7636823c5abed2d0a0151dc8c"
|
| 209 |
+
},
|
| 210 |
+
"quasar/tokenizers/vision.py": {
|
| 211 |
+
"bytes": 7566,
|
| 212 |
+
"sha256": "10f239e83334a06dc49b47fdab7bde9188ade3e5a4612ee1175e0680ba329ca2"
|
| 213 |
+
},
|
| 214 |
+
"quasar/tokenizers/__init__.py": {
|
| 215 |
+
"bytes": 0,
|
| 216 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 217 |
+
},
|
| 218 |
+
"quasar/training/checkpoint.py": {
|
| 219 |
+
"bytes": 3473,
|
| 220 |
+
"sha256": "39c02d1b30da98b8f3861d304f9db5eea0660cec701d8c58cdb4ef14f1beaefd"
|
| 221 |
+
},
|
| 222 |
+
"quasar/training/compilation.py": {
|
| 223 |
+
"bytes": 2062,
|
| 224 |
+
"sha256": "812443fb70939fd06d3f8c96e3d75dc8f9beabcbcaef11623cc571291b437877"
|
| 225 |
+
},
|
| 226 |
+
"quasar/training/hardware.py": {
|
| 227 |
+
"bytes": 2776,
|
| 228 |
+
"sha256": "d9e47b5246c61d09e54b699d9de09d3e42cda29992894b1a58b5b4042e956c2a"
|
| 229 |
+
},
|
| 230 |
+
"quasar/training/metrics.py": {
|
| 231 |
+
"bytes": 2505,
|
| 232 |
+
"sha256": "19189212aa5513def43109142213f9d1ef8c759162395d26d02d1104b5bbc947"
|
| 233 |
+
},
|
| 234 |
+
"quasar/training/migration.py": {
|
| 235 |
+
"bytes": 1381,
|
| 236 |
+
"sha256": "0db3c61f34fe0cc202d6bee9a7d4e4a5f58c6da0f23049d58adcf794239b4f00"
|
| 237 |
+
},
|
| 238 |
+
"quasar/training/trainer.py": {
|
| 239 |
+
"bytes": 21181,
|
| 240 |
+
"sha256": "5a00a53173ca9ee21cce763f9345af1db045b1f0898c073c692aeeb2dbf1c8f2"
|
| 241 |
+
},
|
| 242 |
+
"quasar/training/validation_guard.py": {
|
| 243 |
+
"bytes": 2101,
|
| 244 |
+
"sha256": "81b2916c4879e06025447fba99111cdbacdcdf4065dc033e1e509b434fdacbdd"
|
| 245 |
+
},
|
| 246 |
+
"quasar/training/__init__.py": {
|
| 247 |
+
"bytes": 0,
|
| 248 |
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 249 |
+
},
|
| 250 |
+
"codecs/audio/config.json": {
|
| 251 |
+
"bytes": 542,
|
| 252 |
+
"sha256": "3af86549c509e5491367dab3f7093014bd559f441ba31fa7427b5f94232368d7"
|
| 253 |
+
},
|
| 254 |
+
"codecs/audio/model.safetensors": {
|
| 255 |
+
"bytes": 298652268,
|
| 256 |
+
"sha256": "7452e3fc6972991da871ae1a1c9d3e8e219aa247cc0f757018867d6a7fe59aaf"
|
| 257 |
+
},
|
| 258 |
+
"codecs/audio/OFFICIAL_LICENSE": {
|
| 259 |
+
"bytes": 1095,
|
| 260 |
+
"sha256": "32277f65d28ee0be285c9e2d12970d92bd1331ae9fad490801c3a79243fdb21b"
|
| 261 |
+
},
|
| 262 |
+
"codecs/audio/OFFICIAL_WEIGHTS_TERMS.md": {
|
| 263 |
+
"bytes": 6557,
|
| 264 |
+
"sha256": "3178619f84b8524105b10518d0527600ca74c01386885995f58bc633da2fcbd9"
|
| 265 |
+
},
|
| 266 |
+
"codecs/audio/README.md": {
|
| 267 |
+
"bytes": 5198,
|
| 268 |
+
"sha256": "6b1db50f4974b1544fb795491b6fd57a20320ff17686565c50a08407f06075b1"
|
| 269 |
+
},
|
| 270 |
+
"codecs/vision/config.json": {
|
| 271 |
+
"bytes": 592,
|
| 272 |
+
"sha256": "6717e1bd03d980c6b5e88aa0a47a557d193b0c33afbdb9d24e53a92593ee7473"
|
| 273 |
+
},
|
| 274 |
+
"codecs/vision/configuration_emu3visionvq.py": {
|
| 275 |
+
"bytes": 4176,
|
| 276 |
+
"sha256": "e4a9c0ee78c5196219a5ab89223ba82d2d20844687d19f877b261e5026ef4c11"
|
| 277 |
+
},
|
| 278 |
+
"codecs/vision/image_processing_emu3visionvq.py": {
|
| 279 |
+
"bytes": 22487,
|
| 280 |
+
"sha256": "506392861a6842fa3c9a0bb320b3be0228309c93a1f3b45e4aa2caf501141ea4"
|
| 281 |
+
},
|
| 282 |
+
"codecs/vision/LICENSE": {
|
| 283 |
+
"bytes": 11558,
|
| 284 |
+
"sha256": "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c6"
|
| 285 |
+
},
|
| 286 |
+
"codecs/vision/model.safetensors": {
|
| 287 |
+
"bytes": 1083015124,
|
| 288 |
+
"sha256": "89536431c69b08b10b449ec309f52dcea22f14b7647317f30f5715273392bbf1"
|
| 289 |
+
},
|
| 290 |
+
"codecs/vision/modeling_emu3visionvq.py": {
|
| 291 |
+
"bytes": 24362,
|
| 292 |
+
"sha256": "07558b036f40ec751de174487b15fe5c03823ba2edf29449bfa29be6f7f01afc"
|
| 293 |
+
},
|
| 294 |
+
"codecs/vision/preprocessor_config.json": {
|
| 295 |
+
"bytes": 556,
|
| 296 |
+
"sha256": "98a019cd8820290dbdc53e7366f7e447ee55d085824b63fdb68b2d642dd9edbf"
|
| 297 |
+
},
|
| 298 |
+
"codecs/vision/README.md": {
|
| 299 |
+
"bytes": 3531,
|
| 300 |
+
"sha256": "5763e0d54ed458232a8a4edb94cbf8b3b7fcf7d76372c2c0ea2c4c7b6e6a4e78"
|
| 301 |
+
}
|
| 302 |
+
}
|
infer.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit local Quasar inference. No model/codec download or tool execution."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parent
|
| 8 |
+
sys.path.insert(0, str(ROOT))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
parser = argparse.ArgumentParser()
|
| 13 |
+
parser.add_argument('--checkpoint', type=Path, default=ROOT)
|
| 14 |
+
parser.add_argument('--task', choices=('text', 'image', 'video'), required=True)
|
| 15 |
+
parser.add_argument('--prompt', required=True)
|
| 16 |
+
parser.add_argument('--device', choices=('cpu', 'cuda'), default='cuda')
|
| 17 |
+
parser.add_argument('--output', type=Path)
|
| 18 |
+
parser.add_argument('--resolution', type=int, choices=(64, 128), default=128)
|
| 19 |
+
parser.add_argument('--frames', type=int, default=8)
|
| 20 |
+
parser.add_argument('--seed', type=int, default=20261021)
|
| 21 |
+
parser.add_argument('--max-new-tokens', type=int, default=64)
|
| 22 |
+
args = parser.parse_args()
|
| 23 |
+
if args.device == 'cpu':
|
| 24 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
| 25 |
+
os.environ.setdefault('TORCHINDUCTOR_TRITON_DISABLE_DEVICE_DETECTION', '1')
|
| 26 |
+
# These FP32/BF16 weights do not use the optional GPU quantizer.
|
| 27 |
+
sys.modules['torchao'] = None
|
| 28 |
+
import torch
|
| 29 |
+
torch.set_num_threads(2)
|
| 30 |
+
if args.device == 'cuda':
|
| 31 |
+
from quasar.training.hardware import check_gpu
|
| 32 |
+
check_gpu({'require_cuda': True, 'exclusive_gpu': True, 'min_free_vram_gb': 4})
|
| 33 |
+
from quasar.inference.extended import ExtendedGenerator
|
| 34 |
+
from quasar.inference.decoding import Sampling
|
| 35 |
+
generator = ExtendedGenerator(args.checkpoint.resolve(), args.device)
|
| 36 |
+
sampling = Sampling(temperature=.5, top_k=64, top_p=.9, seed=args.seed, max_new_tokens=args.max_new_tokens)
|
| 37 |
+
if args.task == 'text':
|
| 38 |
+
result = generator.generate_text(args.prompt, sampling)
|
| 39 |
+
if args.output:
|
| 40 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
args.output.write_text(result, encoding='utf-8')
|
| 42 |
+
print(result)
|
| 43 |
+
return
|
| 44 |
+
from PIL import Image
|
| 45 |
+
output = args.output or Path('generated.png' if args.task == 'image' else 'generated.gif')
|
| 46 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 47 |
+
if args.task == 'image':
|
| 48 |
+
pixels = generator.generate_image(args.prompt, sampling, resolution=args.resolution)
|
| 49 |
+
Image.fromarray(pixels).save(output)
|
| 50 |
+
else:
|
| 51 |
+
if output.suffix.lower() != '.gif':
|
| 52 |
+
parser.error('Video preview output must use .gif')
|
| 53 |
+
frames = generator.generate_video(args.prompt, args.frames, sampling)
|
| 54 |
+
images = [Image.fromarray(frame) for frame in frames]
|
| 55 |
+
images[0].save(output, save_all=True, append_images=images[1:], duration=250, loop=0, disposal=2)
|
| 56 |
+
print(output.resolve())
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == '__main__':
|
| 60 |
+
main()
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:de4846e5e63c0004cf0b6331127ab421d6843f6efde04243c6d5448ab4545240
|
| 3 |
+
size 361008568
|
pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "quasar"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Quasar: a unified autoregressive text/image/video foundation-model prototype"
|
| 5 |
+
requires-python = ">=3.10"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"torch>=2.6", "numpy>=1.26", "pillow>=10.0", "pyyaml>=6.0", "safetensors>=0.4",
|
| 8 |
+
"tokenizers>=0.19", "huggingface_hub>=0.24", "pyarrow>=14", "imageio-ffmpeg>=0.5", "requests>=2.31",
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
[project.optional-dependencies]
|
| 12 |
+
app = ["gradio==6.30.0", "psutil>=7.0"]
|
| 13 |
+
dev = ["pytest>=8.0"]
|
| 14 |
+
multimodal = ["jsonschema>=4.20", "transformers==5.5.0", "soundfile>=0.12", "trimesh>=4.0", "scipy>=1.14"]
|
| 15 |
+
|
| 16 |
+
[build-system]
|
| 17 |
+
requires = ["setuptools>=68"]
|
| 18 |
+
build-backend = "setuptools.build_meta"
|
| 19 |
+
|
| 20 |
+
[tool.setuptools.packages.find]
|
| 21 |
+
include = ["quasar*"]
|
| 22 |
+
|
| 23 |
+
[tool.pytest.ini_options]
|
| 24 |
+
testpaths = ["tests"]
|
quasar/__init__.py
ADDED
|
File without changes
|
quasar/data/__init__.py
ADDED
|
File without changes
|
quasar/data/adapters.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Offline adapters for the selected native records.
|
| 2 |
+
|
| 3 |
+
Media bindings are prepared separately with real codecs and audited rights.
|
| 4 |
+
An adapter never downloads an image URL or assumes a dataset card licenses it.
|
| 5 |
+
All reversals of a pair keep the same source group and media assets.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import ast
|
| 11 |
+
from .curated import digest, validate_calls
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _json(x):
|
| 15 |
+
return json.loads(x) if isinstance(x, str) else x
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def normalize(dataset: str, row: dict, provenance: dict, binding: dict | None = None) -> list[dict]:
|
| 19 |
+
"""`provenance` supplies id/revision/license/attribution/source_url/split.
|
| 20 |
+
|
| 21 |
+
binding: {packet: encoded modality packet, assets: [...rights/checksums...],
|
| 22 |
+
group: source object/video/speaker/uploader ID, caption: aligned caption}
|
| 23 |
+
For media, `group` is mandatory: no clip-level or view-level random split.
|
| 24 |
+
"""
|
| 25 |
+
def record(task, ins, outs, group):
|
| 26 |
+
return {**provenance, "dataset": dataset, "task": task, "group": group, "inputs": ins, "outputs": outs,
|
| 27 |
+
**({"assets": binding["assets"], "codec": binding.get("codec", {})} if binding else {})}
|
| 28 |
+
text = lambda s: {"type": "text", "text": str(s).strip()}
|
| 29 |
+
if dataset == "mbpp":
|
| 30 |
+
source_split = provenance.get("source_split")
|
| 31 |
+
expected = {"train": "train", "validation": "val", "test": "test"}
|
| 32 |
+
if source_split not in expected or provenance.get("split") != expected[source_split]:
|
| 33 |
+
raise ValueError("MBPP official train/validation/test provenance must be preserved")
|
| 34 |
+
code = row["code"].strip()
|
| 35 |
+
ast.parse(code) # syntax validation only; dataset code is never executed by this adapter
|
| 36 |
+
examples = "\n".join(row.get("test_list", []))
|
| 37 |
+
prompt = row["text"].strip() + ("\n\nExamples:\n" + examples if examples else "")
|
| 38 |
+
return [record("coding", [text(prompt)], [text("```python\n" + code + "\n```")], str(row["task_id"]))]
|
| 39 |
+
if dataset in ("builtin", "user"):
|
| 40 |
+
if not row.get("group"): raise ValueError("local records require a stable source group")
|
| 41 |
+
return [record(row["task"], row["inputs"], row["outputs"], row["group"])]
|
| 42 |
+
if dataset == "fineweb_edu":
|
| 43 |
+
return [record("text", [], [text(row["text"])], row.get("url") or digest(row["text"]))]
|
| 44 |
+
if dataset == "smoltalk":
|
| 45 |
+
messages = row["messages"]
|
| 46 |
+
if not messages or messages[-1]["role"] != "assistant":
|
| 47 |
+
raise ValueError("SmolTalk row must end with a complete assistant response")
|
| 48 |
+
context = "\n".join(f"{m['role']}: {m['content']}" for m in messages[:-1])
|
| 49 |
+
if not context.strip(): raise ValueError("conversation has no conditioning")
|
| 50 |
+
return [record("chat_text", [text(context)], [text(messages[-1]["content"])], digest(messages))]
|
| 51 |
+
if dataset == "xlam":
|
| 52 |
+
tools = _json(row["tools"])
|
| 53 |
+
calls = _json(row["answers"])
|
| 54 |
+
validate_calls(tools, calls)
|
| 55 |
+
# Whole tool schemas (including function descriptions/parameters) define a heldout family.
|
| 56 |
+
group = digest(sorted(tools, key=lambda x: x.get("function", x)["name"]))
|
| 57 |
+
return [record("tools", [{"type": "tools", "value": tools}, text(row["query"])], [{"type": "calls", "value": calls}], group)]
|
| 58 |
+
if binding is None or not binding.get("group") or not binding.get("assets"):
|
| 59 |
+
raise ValueError("media adapter requires an encoded packet, group ID and audited asset binding")
|
| 60 |
+
packet, group = binding["packet"], binding["group"]
|
| 61 |
+
if dataset == "openimages_narratives":
|
| 62 |
+
if not str(row["dataset_id"]).startswith("open_images"):
|
| 63 |
+
raise ValueError("only the Open Images subset is selected; COCO/Flickr/ADE rights differ")
|
| 64 |
+
cap, a, b, kind = row["caption"], "t2i", "i2t", "image"
|
| 65 |
+
elif dataset == "finevideo":
|
| 66 |
+
# Required exact-window caption is selected from scene/activity metadata, not a whole-video title.
|
| 67 |
+
if not binding.get("caption_aligned") or not binding.get("window_seconds"):
|
| 68 |
+
raise ValueError("FineVideo needs scene-aligned window caption and timestamps")
|
| 69 |
+
cap, a, b, kind = binding["caption"], "t2v", "v2t", "video"
|
| 70 |
+
elif dataset == "libritts_r":
|
| 71 |
+
if not binding.get("complete_utterance"):
|
| 72 |
+
raise ValueError("speech pair must retain the complete aligned utterance")
|
| 73 |
+
cap, a, b, kind = row["text_normalized"], "t2a", "a2t", "audio"
|
| 74 |
+
elif dataset == "fsd50k":
|
| 75 |
+
# Weak class-label conditioning; never report this as free-form human captioning.
|
| 76 |
+
cap, a, b, kind = "sound of " + ", ".join(row["labels"]), "t2a", "a2t", "audio"
|
| 77 |
+
elif dataset == "objaverse_cap3d":
|
| 78 |
+
cap, a, b, kind = row["caption"], "t2m", "m2t", "mesh"
|
| 79 |
+
else:
|
| 80 |
+
raise ValueError(f"no enabled adapter for {dataset}")
|
| 81 |
+
if packet["type"] != kind or not str(cap).strip(): raise ValueError("packet kind or caption mismatch")
|
| 82 |
+
out = [record(a, [text(cap)], [packet], group), record(b, [packet], [text(cap)], group)]
|
| 83 |
+
if dataset == "finevideo" and len(packet["codes"]) >= 4:
|
| 84 |
+
mid = len(packet["codes"]) // 2
|
| 85 |
+
out.append(record("vcont", [{"type":"video", "codes":packet["codes"][:mid]}],
|
| 86 |
+
[{"type":"video", "codes":packet["codes"][mid:]}], group))
|
| 87 |
+
return out
|
quasar/data/collator.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dynamic padding + next-token label construction."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from ..tokenizers.multimodal import Sample
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def collate(samples: list[Sample], pad_id: int, multiple: int = 8, *, target_length: int | None = None,
|
| 11 |
+
batch_size: int | None = None) -> tuple[torch.Tensor, torch.Tensor]:
|
| 12 |
+
"""Right-pad to the batch max (rounded up to `multiple`) and build shifted labels.
|
| 13 |
+
|
| 14 |
+
labels[j] = tokens[j+1] where j+1 >= target_start, else -100 (prompt and padding are not
|
| 15 |
+
trained on). Right padding is safe under causal attention: real tokens never attend to it.
|
| 16 |
+
"""
|
| 17 |
+
L = max(len(s.tokens) for s in samples) - 1
|
| 18 |
+
if target_length is not None:
|
| 19 |
+
if target_length < L:
|
| 20 |
+
raise ValueError("fixed padding cannot truncate a sample")
|
| 21 |
+
L = target_length
|
| 22 |
+
else:
|
| 23 |
+
L = -(-L // multiple) * multiple
|
| 24 |
+
B = len(samples) if batch_size is None else batch_size
|
| 25 |
+
if B < len(samples):
|
| 26 |
+
raise ValueError("fixed batch size cannot discard samples")
|
| 27 |
+
inp = np.full((B, L), pad_id, dtype=np.int64)
|
| 28 |
+
lab = np.full((B, L), -100, dtype=np.int64)
|
| 29 |
+
for i, s in enumerate(samples):
|
| 30 |
+
t = s.tokens.astype(np.int64)
|
| 31 |
+
n = len(t) - 1
|
| 32 |
+
inp[i, :n] = t[:-1]
|
| 33 |
+
lab[i, :n] = t[1:]
|
| 34 |
+
lab[i, : max(0, s.target_start - 1)] = -100
|
| 35 |
+
return torch.from_numpy(inp), torch.from_numpy(lab)
|
quasar/data/curated.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Offline normalized records -> validated, deduplicated, tokenized v2 caches.
|
| 2 |
+
|
| 3 |
+
No network access, model updates, tool execution or implicit license assumptions.
|
| 4 |
+
Records retain dataset, revision, group, split, attribution, asset checksums and
|
| 5 |
+
packet inputs/outputs. Paired generation/understanding rows share one split key.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from collections import Counter, defaultdict
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
from ..tokenizers.multimodal import Sample
|
| 17 |
+
from ..tokenizers.universal import UniversalVocab
|
| 18 |
+
from .preprocessing import sha256_file
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def canonical(value) -> str:
|
| 22 |
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def digest(value) -> str:
|
| 26 |
+
return hashlib.sha256(canonical(value).encode()).hexdigest()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def validate_calls(tools: list[dict], calls: list[dict]) -> None:
|
| 30 |
+
"""Validate tool names and arguments with full JSON Schema; never execute tools."""
|
| 31 |
+
from jsonschema import Draft202012Validator
|
| 32 |
+
names = {}
|
| 33 |
+
for tool in tools:
|
| 34 |
+
f = tool.get("function", tool)
|
| 35 |
+
name = f["name"]
|
| 36 |
+
if name in names:
|
| 37 |
+
raise ValueError(f"duplicate tool name: {name}")
|
| 38 |
+
schema = f["parameters"]
|
| 39 |
+
Draft202012Validator.check_schema(schema)
|
| 40 |
+
names[name] = Draft202012Validator(schema)
|
| 41 |
+
for call in calls:
|
| 42 |
+
if call["name"] not in names:
|
| 43 |
+
raise ValueError(f"unknown tool: {call['name']}")
|
| 44 |
+
if not isinstance(call["arguments"], dict):
|
| 45 |
+
raise ValueError("tool arguments must be a JSON object")
|
| 46 |
+
names[call["name"]].validate(call["arguments"])
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def verify_record(r: dict, catalog: dict):
|
| 50 |
+
for key in ("id", "dataset", "revision", "group", "license", "attribution", "source_url", "task", "inputs", "outputs"):
|
| 51 |
+
if key not in r or r[key] in (None, "", []):
|
| 52 |
+
# Text pretraining legitimately has no input conditioning.
|
| 53 |
+
if key == "inputs" and r.get(key) == []:
|
| 54 |
+
continue
|
| 55 |
+
raise ValueError(f"missing provenance/content field: {key}")
|
| 56 |
+
ds = catalog["datasets"].get(r["dataset"])
|
| 57 |
+
if ds is None or ds["role"] != "core":
|
| 58 |
+
raise ValueError("dataset is unknown or disabled/reserve")
|
| 59 |
+
if r["task"] not in ds["tasks"]:
|
| 60 |
+
raise ValueError("task not registered for dataset")
|
| 61 |
+
if r["dataset"] == "mbpp":
|
| 62 |
+
expected = {"train": "train", "validation": "val", "test": "test"}
|
| 63 |
+
if r.get("source_split") not in expected or r.get("split") != expected[r["source_split"]]:
|
| 64 |
+
raise ValueError("MBPP official splits must be retained; held-out solutions cannot be relabeled train")
|
| 65 |
+
if r["revision"] != ds.get("revision", ds.get("release")):
|
| 66 |
+
raise ValueError("record revision differs from the pinned catalog")
|
| 67 |
+
if ds.get("caption_revision") and r.get("caption_revision") != ds["caption_revision"]:
|
| 68 |
+
raise ValueError("record must pin its caption source revision as well as its asset source")
|
| 69 |
+
allowed = catalog["policy"]["allowed_asset_licenses"]
|
| 70 |
+
licenses = r["license"] if isinstance(r["license"], list) else [r["license"]]
|
| 71 |
+
for lic in licenses:
|
| 72 |
+
if lic not in allowed:
|
| 73 |
+
raise ValueError(f"unapproved or ambiguous license: {lic}")
|
| 74 |
+
declared = ds.get("license")
|
| 75 |
+
if declared and declared not in licenses and not (declared == "CC-BY" and any(x.startswith("CC-BY-") for x in licenses)):
|
| 76 |
+
raise ValueError("record license differs from pinned dataset terms")
|
| 77 |
+
media = [s for s in r["inputs"] + r["outputs"] if s["type"] in ("image", "video", "audio", "mesh")]
|
| 78 |
+
if media:
|
| 79 |
+
for asset in r.get("assets", []):
|
| 80 |
+
for key in ("sha256", "license", "attribution", "source_url", "rights_evidence"):
|
| 81 |
+
if not asset.get(key):
|
| 82 |
+
raise ValueError(f"media asset missing {key}")
|
| 83 |
+
if asset["license"] not in allowed:
|
| 84 |
+
raise ValueError("unapproved media asset license")
|
| 85 |
+
if len(asset["sha256"]) != 64 or any(c not in "0123456789abcdef" for c in asset["sha256"]):
|
| 86 |
+
raise ValueError("invalid asset checksum")
|
| 87 |
+
if not r.get("assets"):
|
| 88 |
+
raise ValueError("media record needs asset rights evidence and checksums")
|
| 89 |
+
if r["task"] == "tools":
|
| 90 |
+
tools = next(s["value"] for s in r["inputs"] if s["type"] == "tools")
|
| 91 |
+
calls = next(s["value"] for s in r["outputs"] if s["type"] == "calls")
|
| 92 |
+
validate_calls(tools, calls)
|
| 93 |
+
inputs = {s["type"] for s in r["inputs"]}
|
| 94 |
+
outputs = {s["type"] for s in r["outputs"]}
|
| 95 |
+
expected = {"t2i": ("text", "image"), "i2t": ("image", "text"), "t2v": ("text", "video"),
|
| 96 |
+
"v2t": ("video", "text"), "vcont": ("video", "video"), "t2a": ("text", "audio"),
|
| 97 |
+
"a2t": ("audio", "text"), "t2m": ("text", "mesh"), "m2t": ("mesh", "text"),
|
| 98 |
+
"chat_text": ("text", "text"), "coding": ("text", "text"), "tools": ("tools", "calls")}
|
| 99 |
+
if r["task"] in expected:
|
| 100 |
+
a, b = expected[r["task"]]
|
| 101 |
+
if a not in inputs or b not in outputs: raise ValueError("packet types do not match the registered task")
|
| 102 |
+
if r["task"] == "text" and outputs != {"text"}: raise ValueError("text task needs text targets")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def compile_manifest(manifest: str | Path, out: str | Path, vocab: UniversalVocab, tok, catalog: dict,
|
| 106 |
+
context: int = 1024, max_disk_bytes: int = 20_000_000_000) -> dict:
|
| 107 |
+
"""Stream into uint16 token files and per-row indices; reject overlong samples without truncation.
|
| 108 |
+
|
| 109 |
+
Existing output directories are refused. Failure preserves an INCOMPLETE marker;
|
| 110 |
+
a loader refuses that directory. Provenance is saved separately from compact indices.
|
| 111 |
+
"""
|
| 112 |
+
manifest, out = Path(manifest), Path(out)
|
| 113 |
+
if out.exists():
|
| 114 |
+
raise FileExistsError(f"refusing to overwrite cache {out}; use a new versioned directory")
|
| 115 |
+
if tok.vocab_size != vocab.text_size or vocab.size > 65536:
|
| 116 |
+
raise ValueError("tokenizer size/vocabulary mismatch for uint16 cache")
|
| 117 |
+
out.mkdir(parents=True)
|
| 118 |
+
(out / "INCOMPLETE").write_text("preparation in progress")
|
| 119 |
+
counts, offsets = Counter(), defaultdict(int)
|
| 120 |
+
groups, assets, seen, seen_split = {}, {}, set(), {}
|
| 121 |
+
report = {"training_started": False, "records": 0, "target_tokens": 0}
|
| 122 |
+
handles = {}
|
| 123 |
+
try:
|
| 124 |
+
for split in ("train", "val", "test"):
|
| 125 |
+
handles[split] = ((out / f"{split}.bin").open("wb"), (out / f"{split}.index.jsonl").open("w", encoding="utf-8"))
|
| 126 |
+
with manifest.open(encoding="utf-8") as src, (out / "provenance.jsonl").open("w", encoding="utf-8") as prov:
|
| 127 |
+
for lineno, line in enumerate(src, 1):
|
| 128 |
+
if not line.strip(): continue
|
| 129 |
+
try:
|
| 130 |
+
r = json.loads(line)
|
| 131 |
+
verify_record(r, catalog)
|
| 132 |
+
group = r["dataset"] + ":" + str(r["group"])
|
| 133 |
+
# Explicit official splits win. Otherwise hash source groups, never windows/captions.
|
| 134 |
+
split = r.get("split") or ("val" if int(digest(group)[:8], 16) % 100 < 2 else "train")
|
| 135 |
+
if split not in handles: raise ValueError("split must be train, val or test")
|
| 136 |
+
if group in groups and groups[group] != split: raise ValueError("source group leaks across splits")
|
| 137 |
+
groups[group] = split
|
| 138 |
+
for a in r.get("assets", []):
|
| 139 |
+
h = a["sha256"]
|
| 140 |
+
if h in assets and assets[h] != split: raise ValueError("asset content leaks across splits")
|
| 141 |
+
assets[h] = split
|
| 142 |
+
key = digest([r["task"], r["inputs"], r["outputs"]])
|
| 143 |
+
if key in seen:
|
| 144 |
+
if seen_split[key] != split: raise ValueError("duplicate example leaks across splits")
|
| 145 |
+
counts["duplicates_skipped"] += 1
|
| 146 |
+
continue
|
| 147 |
+
seen.add(key)
|
| 148 |
+
seen_split[key] = split
|
| 149 |
+
sample = vocab.build(r["task"], r["inputs"], r["outputs"], tok.encode, context)
|
| 150 |
+
if (sum(offsets.values()) + len(sample.tokens)) * 2 > max_disk_bytes:
|
| 151 |
+
raise ValueError("prepared token disk budget exceeded")
|
| 152 |
+
sample.tokens.astype("<u2").tofile(handles[split][0])
|
| 153 |
+
idx = {"offset": offsets[split], "length": len(sample.tokens), "target_start": sample.target_start,
|
| 154 |
+
"task": r["task"], "id": r["id"], "dataset": r["dataset"], "group": r["group"]}
|
| 155 |
+
handles[split][1].write(canonical(idx) + "\n")
|
| 156 |
+
prov.write(canonical({k: r[k] for k in r if k not in ("inputs", "outputs")}) + "\n")
|
| 157 |
+
offsets[split] += len(sample.tokens)
|
| 158 |
+
counts[split + "/" + r["task"]] += 1
|
| 159 |
+
report["records"] += 1
|
| 160 |
+
report["target_tokens"] += len(sample.tokens) - sample.target_start
|
| 161 |
+
except Exception as e:
|
| 162 |
+
raise ValueError(f"{manifest}:{lineno}: {e}") from e
|
| 163 |
+
finally:
|
| 164 |
+
for pair in handles.values():
|
| 165 |
+
for f in pair: f.close()
|
| 166 |
+
tok.save(out / "text_tokenizer.json")
|
| 167 |
+
(out / "vocab_v2.json").write_text(json.dumps(vocab.to_dict(), indent=2))
|
| 168 |
+
(out / "catalog.json").write_text(json.dumps(catalog, indent=2))
|
| 169 |
+
report |= {"counts": dict(counts), "manifest_sha256": sha256_file(manifest), "catalog_sha256": digest(catalog),
|
| 170 |
+
"token_bytes": sum(offsets.values()) * 2, "context": context, "tokenizer_sha256": sha256_file(out / "text_tokenizer.json")}
|
| 171 |
+
(out / "stats.json").write_text(json.dumps(report, indent=2))
|
| 172 |
+
(out / "INCOMPLETE").unlink()
|
| 173 |
+
return report
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
class UniversalSources:
|
| 177 |
+
"""Same sample/available interface as TaskSources; consumed by existing MixtureLoader."""
|
| 178 |
+
def __init__(self, cache: str | Path, split: str, vocab: UniversalVocab, seq_len: int = 1024):
|
| 179 |
+
cache = Path(cache)
|
| 180 |
+
if (cache / "INCOMPLETE").exists(): raise ValueError("incomplete v2 cache")
|
| 181 |
+
if json.loads((cache / "vocab_v2.json").read_text()) != vocab.to_dict(): raise ValueError("v2 vocabulary mismatch")
|
| 182 |
+
self.vocab, self.seq_len, self.rows = vocab, seq_len, defaultdict(list)
|
| 183 |
+
path = cache / f"{split}.bin"
|
| 184 |
+
self.tokens = np.memmap(path, dtype="<u2", mode="r") if path.stat().st_size else np.array([], dtype=np.uint16)
|
| 185 |
+
for line in (cache / f"{split}.index.jsonl").read_text().splitlines():
|
| 186 |
+
r = json.loads(line)
|
| 187 |
+
if r["length"] - 1 > seq_len: raise ValueError("cached sample exceeds requested context")
|
| 188 |
+
self.rows[r["task"]].append(r)
|
| 189 |
+
|
| 190 |
+
def available(self): return list(self.rows)
|
| 191 |
+
|
| 192 |
+
def sample(self, task: str, rng: np.random.Generator) -> Sample:
|
| 193 |
+
rows = self.rows[task]
|
| 194 |
+
r = rows[int(rng.integers(len(rows)))]
|
| 195 |
+
return Sample(np.array(self.tokens[r["offset"]:r["offset"] + r["length"]], dtype=np.int32), r["target_start"])
|
quasar/data/datasets.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task sources over the pretokenized caches and a seeded mixture loader.
|
| 2 |
+
|
| 3 |
+
Every micro-batch holds a single task (so lengths are similar and padding is small) and is
|
| 4 |
+
drawn from an RNG seeded by (seed, step, micro_index): sampling is a pure function of the step,
|
| 5 |
+
which makes runs reproducible and resumable without storing data-loader state.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import queue
|
| 11 |
+
import threading
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ..tokenizers.multimodal import Sample, UnifiedVocab
|
| 18 |
+
from ..tokenizers.text import TextTokenizer
|
| 19 |
+
from .collator import collate
|
| 20 |
+
|
| 21 |
+
BASE_TASKS = ("text", "t2i", "i2t", "i2i", "t2v", "v2t", "vcont")
|
| 22 |
+
CHAT_TASKS = ("chat_text", "chat_mm")
|
| 23 |
+
TASK_MODALITY = {"text": "text", "chat_text": "text", "t2i": "image", "i2t": "image", "i2i": "image",
|
| 24 |
+
"t2v": "video", "v2t": "video", "vcont": "video", "chat_mm": "multimodal"}
|
| 25 |
+
|
| 26 |
+
CHAT_PROMPTS = {
|
| 27 |
+
"t2i": ["Generate an image of {c}.", "Draw {c}.", "Create a picture of {c}.", "Show me {c}.", "Can you make an image of {c}?"],
|
| 28 |
+
"t2v": ["Make a video of {c}.", "Generate a video of {c}.", "Create a short clip of {c}.", "Animate {c}."],
|
| 29 |
+
"i2t": ["Describe this image.", "What do you see in this picture?", "What is in this image?"],
|
| 30 |
+
"v2t": ["Describe this video.", "What is happening in this video?", "What happens in this clip?"],
|
| 31 |
+
"vcont": ["Continue the movement in this video.", "Continue this video.", "What happens next? Show me."],
|
| 32 |
+
"i2v": ["Animate this image: {c}.", "Bring this picture to life: {c}."],
|
| 33 |
+
}
|
| 34 |
+
CHAT_KINDS = ("t2i", "t2v", "i2t", "v2t", "vcont", "i2i", "i2v")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _load_media(cache_dir: Path, name: str, split: str, vocab: UnifiedVocab, keys: tuple[str, ...]) -> tuple[dict, dict] | None:
|
| 38 |
+
"""Load <name>_<split>.npz/.json, dropping rows whose codes are out of range (corruption guard)."""
|
| 39 |
+
npz, js = cache_dir / f"{name}_{split}.npz", cache_dir / f"{name}_{split}.json"
|
| 40 |
+
if not npz.exists():
|
| 41 |
+
return None
|
| 42 |
+
try:
|
| 43 |
+
with np.load(npz) as z:
|
| 44 |
+
arrs = {k: z[k] for k in keys}
|
| 45 |
+
meta = json.loads(js.read_text())
|
| 46 |
+
except (OSError, ValueError, KeyError) as e:
|
| 47 |
+
raise RuntimeError(f"cache {npz} is unreadable ({e}); regenerate with scripts/generate_dataset.py") from e
|
| 48 |
+
ok = np.ones(len(arrs[keys[0]]), dtype=bool)
|
| 49 |
+
for a in arrs.values():
|
| 50 |
+
ok &= (a.reshape(len(a), -1) < vocab.visual_size).all(1)
|
| 51 |
+
if not ok.all():
|
| 52 |
+
print(f"[data] {npz.name}: dropping {int((~ok).sum())} corrupted rows")
|
| 53 |
+
keep = np.nonzero(ok)[0]
|
| 54 |
+
return {k: a[keep] for k, a in arrs.items()}, {k: [v[i] for i in keep] for k, v in meta.items()}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class TaskSources:
|
| 58 |
+
"""Builds one training Sample for any task from the caches of a given split."""
|
| 59 |
+
|
| 60 |
+
def __init__(self, cache_dir: str | Path, split: str, vocab: UnifiedVocab, tok: TextTokenizer, seq_len: int, caption_dropout: float = 0.1):
|
| 61 |
+
cache_dir = Path(cache_dir)
|
| 62 |
+
self.vocab, self.tok, self.seq_len = vocab, tok, seq_len
|
| 63 |
+
# Blank the t2i/t2v caption this often so the model also learns p(media) -> classifier-free guidance.
|
| 64 |
+
self.caption_dropout = caption_dropout if split == "train" else 0.0
|
| 65 |
+
tp = cache_dir / f"text_{split}.bin"
|
| 66 |
+
self.text = np.memmap(tp, dtype=np.uint16, mode="r") if tp.exists() else None
|
| 67 |
+
self.images = _load_media(cache_dir, "images", split, vocab, ("codes",))
|
| 68 |
+
self.edits = _load_media(cache_dir, "edits", split, vocab, ("src", "tgt"))
|
| 69 |
+
self.videos = _load_media(cache_dir, "videos", split, vocab, ("codes",))
|
| 70 |
+
cp = cache_dir / f"chat_{split}.json"
|
| 71 |
+
self.chat = json.loads(cp.read_text()) if cp.exists() else None
|
| 72 |
+
# indices with a caption (user videos may be uncaptioned: continuation-only)
|
| 73 |
+
cap_idx = lambda m: np.array([i for i, c in enumerate(m[1]["captions"]) if c], dtype=np.int64) if m else np.zeros(0, np.int64)
|
| 74 |
+
self.img_cap, self.vid_cap = cap_idx(self.images), cap_idx(self.videos)
|
| 75 |
+
need = {"t2i": len(self.img_cap), "i2t": len(self.img_cap), "t2v": len(self.vid_cap), "v2t": len(self.vid_cap),
|
| 76 |
+
"vcont": self.videos is not None, "i2i": self.edits is not None, "i2v": len(self.vid_cap)}
|
| 77 |
+
self.chat_kinds = [k for k in CHAT_KINDS if need[k]]
|
| 78 |
+
|
| 79 |
+
def available(self) -> list[str]:
|
| 80 |
+
have = {"text": self.text is not None and len(self.text) > self.seq_len + 1, "chat_text": bool(self.chat),
|
| 81 |
+
"t2i": len(self.img_cap) > 0, "i2t": len(self.img_cap) > 0, "i2i": self.edits is not None,
|
| 82 |
+
"t2v": len(self.vid_cap) > 0, "v2t": len(self.vid_cap) > 0, "vcont": self.videos is not None,
|
| 83 |
+
"chat_mm": bool(self.chat_kinds)}
|
| 84 |
+
return [t for t, ok in have.items() if ok]
|
| 85 |
+
|
| 86 |
+
def enc(self, s: str) -> list[int]:
|
| 87 |
+
return self.tok.encode(s)
|
| 88 |
+
|
| 89 |
+
def _prompt(self, caps: list[str], rng: np.random.Generator) -> list[int]:
|
| 90 |
+
return [] if rng.random() < self.caption_dropout else self.enc(str(rng.choice(caps)))
|
| 91 |
+
|
| 92 |
+
def sample(self, task: str, rng: np.random.Generator) -> Sample:
|
| 93 |
+
v = self.vocab
|
| 94 |
+
if task == "text":
|
| 95 |
+
off = int(rng.integers(0, len(self.text) - self.seq_len - 1))
|
| 96 |
+
return Sample(np.asarray(self.text[off : off + self.seq_len + 1], dtype=np.int32), 0)
|
| 97 |
+
if task in ("t2i", "i2t"):
|
| 98 |
+
(arr, meta), i = self.images, int(self.img_cap[rng.integers(len(self.img_cap))])
|
| 99 |
+
caps = meta["captions"][i]
|
| 100 |
+
if task == "t2i":
|
| 101 |
+
return v.build("t2i", self._prompt(caps, rng), image=arr["codes"][i])
|
| 102 |
+
return v.build("i2t", self.enc(caps[0]), image=arr["codes"][i])
|
| 103 |
+
if task == "i2i":
|
| 104 |
+
(arr, meta), i = self.edits, int(rng.integers(len(self.edits[0]["src"])))
|
| 105 |
+
return v.build("i2i", self.enc(meta["instructions"][i]), image=arr["src"][i], image2=arr["tgt"][i])
|
| 106 |
+
if task in ("t2v", "v2t", "vcont"):
|
| 107 |
+
pool = self.vid_cap if task != "vcont" else np.arange(len(self.videos[0]["codes"]))
|
| 108 |
+
(arr, meta), i = self.videos, int(pool[rng.integers(len(pool))])
|
| 109 |
+
caps = meta["captions"][i]
|
| 110 |
+
if task == "t2v":
|
| 111 |
+
return v.build("t2v", self._prompt(caps, rng), video=arr["codes"][i])
|
| 112 |
+
if task == "v2t":
|
| 113 |
+
return v.build("v2t", self.enc(caps[0]), video=arr["codes"][i])
|
| 114 |
+
return v.build("vcont", video=arr["codes"][i])
|
| 115 |
+
if task == "chat_text":
|
| 116 |
+
r = self.chat[int(rng.integers(len(self.chat)))]
|
| 117 |
+
s = v.chat(self.enc(r["user"]), self.enc(r["assistant"]))
|
| 118 |
+
return Sample(s.tokens[: self.seq_len + 1], s.target_start)
|
| 119 |
+
if task == "chat_mm":
|
| 120 |
+
return self.chat_mm(str(rng.choice(self.chat_kinds)), rng)
|
| 121 |
+
raise KeyError(task)
|
| 122 |
+
|
| 123 |
+
def chat_mm(self, kind: str, rng: np.random.Generator) -> Sample:
|
| 124 |
+
"""Instruction-formatted multimodal conversation built from the media caches."""
|
| 125 |
+
v, enc = self.vocab, self.enc
|
| 126 |
+
pick = lambda xs: str(rng.choice(xs))
|
| 127 |
+
if kind in ("t2i", "i2t"):
|
| 128 |
+
(arr, meta), i = self.images, int(self.img_cap[rng.integers(len(self.img_cap))])
|
| 129 |
+
cap, img = meta["captions"][i][0], arr["codes"][i]
|
| 130 |
+
if kind == "t2i":
|
| 131 |
+
return v.chat(enc(pick(CHAT_PROMPTS["t2i"]).format(c=pick(meta["captions"][i]))), enc(f"Here is an image of {cap}.") + v.image_tokens(img))
|
| 132 |
+
return v.chat(v.image_tokens(img) + enc(" " + pick(CHAT_PROMPTS["i2t"])), enc(f"The image shows {cap}."))
|
| 133 |
+
if kind == "i2i":
|
| 134 |
+
(arr, meta), i = self.edits, int(rng.integers(len(self.edits[0]["src"])))
|
| 135 |
+
instr = meta["instructions"][i]
|
| 136 |
+
return v.chat(v.image_tokens(arr["src"][i]) + enc(" " + instr[0].upper() + instr[1:] + "."), enc("Here is the edited image.") + v.image_tokens(arr["tgt"][i]))
|
| 137 |
+
pool = self.vid_cap if kind != "vcont" else np.arange(len(self.videos[0]["codes"]))
|
| 138 |
+
(arr, meta), i = self.videos, int(pool[rng.integers(len(pool))])
|
| 139 |
+
cap, vid = (meta["captions"][i] or [""])[0], arr["codes"][i]
|
| 140 |
+
if kind == "t2v":
|
| 141 |
+
return v.chat(enc(pick(CHAT_PROMPTS["t2v"]).format(c=pick(meta["captions"][i]))), enc(f"Here is a video of {cap}.") + v.video_tokens(vid))
|
| 142 |
+
if kind == "v2t":
|
| 143 |
+
return v.chat(v.video_tokens(vid) + enc(" " + pick(CHAT_PROMPTS["v2t"])), enc(f"The video shows {cap}."))
|
| 144 |
+
if kind == "vcont":
|
| 145 |
+
k = len(vid) // 2
|
| 146 |
+
return v.chat(v.video_tokens(vid[:k]) + enc(" " + pick(CHAT_PROMPTS["vcont"])), enc("Here is the continuation.") + v.video_tokens(vid[k:], first_index=k))
|
| 147 |
+
# i2v: the image is frame 0; the answer is the rest of the clip
|
| 148 |
+
return v.chat(v.image_tokens(vid[0]) + enc(" " + pick(CHAT_PROMPTS["i2v"]).format(c=cap)), enc("Here is the animation.") + v.video_tokens(vid[1:], first_index=1))
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def split_task(name: str) -> tuple[str, str]:
|
| 152 |
+
"""'user/t2i' -> ('user', 't2i'); 't2i' -> ('', 't2i') (source prefix selects the cache)."""
|
| 153 |
+
src, _, task = name.rpartition("/")
|
| 154 |
+
return src, task
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def available_tasks(sources: dict[str, TaskSources]) -> list[str]:
|
| 158 |
+
return [f"{k}/{t}" if k else t for k, src in sources.items() for t in src.available()]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def compile_bucket_length(length: int, context: int) -> int:
|
| 162 |
+
"""At most four fixed sequence sizes at context 1024; no content truncation."""
|
| 163 |
+
if not 0 < length <= context:
|
| 164 |
+
raise ValueError("sample length is outside the model context")
|
| 165 |
+
return min(context, max(128, 1 << (length - 1).bit_length()))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
class MixtureLoader:
|
| 169 |
+
"""Yields (task, input_ids, labels) micro-batches; one task per micro-batch, ~`micro_tokens` tokens each.
|
| 170 |
+
|
| 171 |
+
`sources` maps a source name to its TaskSources ('' = built-in cache, 'user' = imported data);
|
| 172 |
+
mixture keys address them as 't2i' or 'user/t2i', so every source/task share is an explicit weight."""
|
| 173 |
+
|
| 174 |
+
def __init__(self, sources: dict[str, TaskSources] | TaskSources, weights: dict[str, float], micro_tokens: int, seed: int, prefetch: int = 6,
|
| 175 |
+
compile_buckets: bool = False):
|
| 176 |
+
sources = sources if isinstance(sources, dict) else {"": sources}
|
| 177 |
+
missing = [t for t, w in weights.items() if w > 0 and t not in available_tasks(sources)]
|
| 178 |
+
if missing:
|
| 179 |
+
raise ValueError(f"mixture uses tasks with no cache: {missing}; available: {available_tasks(sources)}")
|
| 180 |
+
self.sources, self.seed, self.micro_tokens = sources, seed, micro_tokens
|
| 181 |
+
self.vocab = next(iter(sources.values())).vocab
|
| 182 |
+
self.tasks = [t for t, w in weights.items() if w > 0]
|
| 183 |
+
p = np.array([weights[t] for t in self.tasks], dtype=np.float64)
|
| 184 |
+
self.p = p / p.sum()
|
| 185 |
+
self.prefetch = prefetch
|
| 186 |
+
self.compile_buckets = compile_buckets
|
| 187 |
+
|
| 188 |
+
def batch(self, step: int, micro: int) -> tuple[str, torch.Tensor, torch.Tensor]:
|
| 189 |
+
rng = np.random.default_rng([self.seed, step, micro])
|
| 190 |
+
task = self.tasks[int(rng.choice(len(self.tasks), p=self.p))]
|
| 191 |
+
samples: list[Sample] = []
|
| 192 |
+
longest = 0
|
| 193 |
+
src, base = split_task(task)
|
| 194 |
+
while True:
|
| 195 |
+
s = self.sources[src].sample(base, rng)
|
| 196 |
+
proposed = max(longest, len(s.tokens) - 1 if self.compile_buckets else len(s.tokens))
|
| 197 |
+
padded = compile_bucket_length(proposed, self.sources[src].seq_len) if self.compile_buckets else proposed
|
| 198 |
+
if self.compile_buckets and padded > self.micro_tokens:
|
| 199 |
+
raise ValueError("compiled batch token budget is smaller than its padded context")
|
| 200 |
+
if samples and (len(samples) + 1) * padded > self.micro_tokens:
|
| 201 |
+
break
|
| 202 |
+
longest = proposed
|
| 203 |
+
samples.append(s)
|
| 204 |
+
if self.compile_buckets:
|
| 205 |
+
length = compile_bucket_length(longest, self.sources[src].seq_len)
|
| 206 |
+
# Empty rows have no target labels and do not change the supervised objective.
|
| 207 |
+
inp, lab = collate(samples, self.vocab.pad, target_length=length,
|
| 208 |
+
batch_size=self.micro_tokens // length)
|
| 209 |
+
else:
|
| 210 |
+
inp, lab = collate(samples, self.vocab.pad)
|
| 211 |
+
return task, inp, lab
|
| 212 |
+
|
| 213 |
+
def iterate(self, start_step: int, grad_accum: int):
|
| 214 |
+
"""Background-thread prefetching iterator of (step, micro, task, inp, lab)."""
|
| 215 |
+
q: queue.Queue = queue.Queue(maxsize=self.prefetch)
|
| 216 |
+
stop = threading.Event()
|
| 217 |
+
|
| 218 |
+
def work():
|
| 219 |
+
step = start_step
|
| 220 |
+
while not stop.is_set():
|
| 221 |
+
for m in range(grad_accum):
|
| 222 |
+
item = (step, m, *self.batch(step, m))
|
| 223 |
+
while not stop.is_set():
|
| 224 |
+
try:
|
| 225 |
+
q.put(item, timeout=0.5)
|
| 226 |
+
break
|
| 227 |
+
except queue.Full:
|
| 228 |
+
continue
|
| 229 |
+
step += 1
|
| 230 |
+
|
| 231 |
+
th = threading.Thread(target=work, daemon=True)
|
| 232 |
+
th.start()
|
| 233 |
+
try:
|
| 234 |
+
while True:
|
| 235 |
+
yield q.get()
|
| 236 |
+
finally:
|
| 237 |
+
stop.set()
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def fixed_eval_batches(sources: dict[str, TaskSources] | TaskSources, tasks: list[str], micro_tokens: int, n_batches: int, seed: int = 999) -> dict[str, list]:
|
| 241 |
+
"""Deterministic held-out batches per task (same every evaluation)."""
|
| 242 |
+
sources = sources if isinstance(sources, dict) else {"": sources}
|
| 243 |
+
out = {}
|
| 244 |
+
for ti, task in enumerate(tasks):
|
| 245 |
+
if task not in available_tasks(sources):
|
| 246 |
+
continue
|
| 247 |
+
ml = MixtureLoader(sources, {task: 1.0}, micro_tokens, seed)
|
| 248 |
+
out[task] = [ml.batch(10_000 + ti, i)[1:] for i in range(n_batches)]
|
| 249 |
+
return out
|
quasar/data/download.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Resumable HTTP acquisition with disk reserve, hashes and atomic progress snapshots.
|
| 2 |
+
|
| 3 |
+
This module never extracts archives, evaluates remote code or initializes CUDA.
|
| 4 |
+
Dataset payloads remain separate from licensed/validated training caches.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 9 |
+
import hashlib
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import shutil
|
| 14 |
+
import threading
|
| 15 |
+
import time
|
| 16 |
+
|
| 17 |
+
import requests
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AccessBlocked(RuntimeError): pass
|
| 21 |
+
class DiskBudget(RuntimeError): pass
|
| 22 |
+
class FileTooLarge(ValueError): pass
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _comparable_path(path: Path) -> Path:
|
| 26 |
+
"""Normalize Windows namespace aliases after resolving symlinks/junctions.
|
| 27 |
+
|
| 28 |
+
Non-strict realpath can retain the extended prefix when a parent directory
|
| 29 |
+
appears concurrently. Compare both spellings consistently; do not skip the
|
| 30 |
+
resolved-path containment check.
|
| 31 |
+
"""
|
| 32 |
+
if os.name == "nt":
|
| 33 |
+
value = str(path)
|
| 34 |
+
if value.startswith("\\\\?\\UNC\\"):
|
| 35 |
+
return Path("\\\\" + value[8:])
|
| 36 |
+
if value.startswith("\\\\?\\") and len(value) >= 7 and value[5:7] == ":\\":
|
| 37 |
+
return Path(value[4:])
|
| 38 |
+
return path
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def exception_summary(exc: Exception, limit: int = 180) -> str:
|
| 42 |
+
lines = str(exc).splitlines()
|
| 43 |
+
return type(exc).__name__ + ": " + (lines[0][:limit] if lines else "no additional detail")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class Downloads:
|
| 47 |
+
def __init__(self, root: str | Path, max_gib: float = 70, reserve_gib: float = 25):
|
| 48 |
+
self.root = Path(root).resolve()
|
| 49 |
+
self.root.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
self.maximum, self.reserve = int(max_gib * 2**30), int(reserve_gib * 2**30)
|
| 51 |
+
self.lock = threading.RLock()
|
| 52 |
+
self.last_save = 0.0
|
| 53 |
+
self.bytes = sum(p.stat().st_size for p in self.root.rglob("*") if p.is_file())
|
| 54 |
+
self.state = {"pid": os.getpid(), "started": time.time(), "training_started": False,
|
| 55 |
+
"max_gib": max_gib, "reserve_gib": reserve_gib, "sources": {}}
|
| 56 |
+
self.current: dict = {}
|
| 57 |
+
|
| 58 |
+
def safe_path(self, rel: str) -> Path:
|
| 59 |
+
p = (self.root / rel).resolve()
|
| 60 |
+
if not _comparable_path(p).is_relative_to(_comparable_path(self.root)):
|
| 61 |
+
raise ValueError("download path escapes acquisition directory")
|
| 62 |
+
return p
|
| 63 |
+
|
| 64 |
+
def save(self, force=False):
|
| 65 |
+
with self.lock:
|
| 66 |
+
if not force and time.time() - self.last_save < 3: return
|
| 67 |
+
self.last_save = time.time()
|
| 68 |
+
self.state |= {"updated": time.time(), "stored_bytes": self.bytes,
|
| 69 |
+
"free_bytes": shutil.disk_usage(self.root).free, "current_files": dict(self.current)}
|
| 70 |
+
tmp = self.root / "download_status.json.tmp"
|
| 71 |
+
tmp.write_text(json.dumps(self.state, indent=2), encoding="utf-8")
|
| 72 |
+
# Windows readers briefly deny replacement while their file handle
|
| 73 |
+
# is open (for example, the live dashboard). Keep the last complete
|
| 74 |
+
# snapshot visible and retry this atomic swap for a bounded period.
|
| 75 |
+
for attempt in range(8):
|
| 76 |
+
try:
|
| 77 |
+
os.replace(tmp, self.root / "download_status.json")
|
| 78 |
+
break
|
| 79 |
+
except PermissionError:
|
| 80 |
+
if attempt == 7:
|
| 81 |
+
raise
|
| 82 |
+
time.sleep(0.01 * 2**attempt)
|
| 83 |
+
|
| 84 |
+
def event(self, source, status, **kwargs):
|
| 85 |
+
with self.lock:
|
| 86 |
+
old = self.state["sources"].setdefault(source, {})
|
| 87 |
+
old.update({"status": status, "updated": time.time(), **kwargs})
|
| 88 |
+
self.save(force=True)
|
| 89 |
+
print(json.dumps({"source": source, "status": status, **kwargs}), flush=True)
|
| 90 |
+
|
| 91 |
+
def _claim(self, amount):
|
| 92 |
+
with self.lock:
|
| 93 |
+
if self.bytes + amount > self.maximum: raise DiskBudget(f"{self.maximum / 2**30:g} GiB acquisition cap reached")
|
| 94 |
+
if shutil.disk_usage(self.root).free - amount < self.reserve: raise DiskBudget("25 GiB free-space reserve reached")
|
| 95 |
+
self.bytes += amount
|
| 96 |
+
|
| 97 |
+
def _record(self, record):
|
| 98 |
+
with self.lock:
|
| 99 |
+
with (self.root / "files.jsonl").open("a", encoding="utf-8") as f:
|
| 100 |
+
f.write(json.dumps(record) + "\n")
|
| 101 |
+
|
| 102 |
+
def get(self, url: str, rel: str, expected_size: int | None = None, checksum: str | None = None,
|
| 103 |
+
headers: dict | None = None, max_file_bytes: int = 12 * 2**30, retries: int = 3) -> Path:
|
| 104 |
+
"""Resume .part with validated Content-Range. HTTP credentials are never logged."""
|
| 105 |
+
path = self.safe_path(rel)
|
| 106 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 107 |
+
if path.exists():
|
| 108 |
+
if expected_size is not None and path.stat().st_size != expected_size: raise ValueError(f"existing size mismatch: {rel}")
|
| 109 |
+
if checksum:
|
| 110 |
+
alg, value = checksum.split(":", 1)
|
| 111 |
+
if self.hash(path, alg) != value: raise ValueError(f"existing checksum mismatch: {rel}")
|
| 112 |
+
return path
|
| 113 |
+
part = path.with_name(path.name + ".part")
|
| 114 |
+
for attempt in range(retries):
|
| 115 |
+
try:
|
| 116 |
+
offset = part.stat().st_size if part.exists() else 0
|
| 117 |
+
h = {**(headers or {}), "Accept-Encoding": "identity"}
|
| 118 |
+
if offset: h["Range"] = f"bytes={offset}-"
|
| 119 |
+
with requests.get(url, headers=h, stream=True, timeout=(25, 90)) as response:
|
| 120 |
+
if response.status_code in (401, 403): raise AccessBlocked(f"access approval required for {rel} (HTTP {response.status_code})")
|
| 121 |
+
response.raise_for_status()
|
| 122 |
+
if offset and response.status_code != 206:
|
| 123 |
+
# Server ignored range. Restart this owned partial file, accounting for the old bytes.
|
| 124 |
+
with self.lock: self.bytes -= offset
|
| 125 |
+
offset = 0
|
| 126 |
+
if response.status_code == 206 and not response.headers.get("Content-Range", "").startswith(f"bytes {offset}-"):
|
| 127 |
+
raise ValueError("server returned a mismatched byte range")
|
| 128 |
+
length = int(response.headers.get("Content-Length", "0"))
|
| 129 |
+
if offset + length > max_file_bytes or (expected_size is not None and expected_size > max_file_bytes):
|
| 130 |
+
raise FileTooLarge(f"file exceeds per-file limit: {rel}")
|
| 131 |
+
if expected_size is not None and offset + length and length and expected_size != offset + length:
|
| 132 |
+
raise ValueError(f"unexpected remote size for {rel}")
|
| 133 |
+
with part.open("ab" if offset else "wb") as f:
|
| 134 |
+
for chunk in response.iter_content(1024 * 1024):
|
| 135 |
+
if not chunk: continue
|
| 136 |
+
if f.tell() + len(chunk) > max_file_bytes: raise FileTooLarge("stream exceeded per-file limit")
|
| 137 |
+
self._claim(len(chunk))
|
| 138 |
+
try: f.write(chunk)
|
| 139 |
+
except Exception:
|
| 140 |
+
with self.lock: self.bytes -= len(chunk)
|
| 141 |
+
raise
|
| 142 |
+
with self.lock: self.current[rel] = {"bytes": f.tell(), "expected": expected_size or offset + length or None}
|
| 143 |
+
self.save()
|
| 144 |
+
if expected_size is not None and part.stat().st_size != expected_size: raise ValueError(f"truncated file: {rel}")
|
| 145 |
+
if length and part.stat().st_size != offset + length: raise ValueError(f"truncated HTTP body: {rel}")
|
| 146 |
+
sha = self.hash(part)
|
| 147 |
+
if checksum:
|
| 148 |
+
alg, value = checksum.split(":", 1)
|
| 149 |
+
actual = sha if alg == "sha256" else self.hash(part, alg)
|
| 150 |
+
if actual != value: raise ValueError(f"checksum mismatch: {rel}; partial retained for inspection")
|
| 151 |
+
os.replace(part, path)
|
| 152 |
+
self._record({"path": rel, "bytes": path.stat().st_size, "sha256": sha, "source_url": url,
|
| 153 |
+
"publisher_checksum": checksum, "completed": time.time()})
|
| 154 |
+
with self.lock: self.current.pop(rel, None)
|
| 155 |
+
self.save(force=True)
|
| 156 |
+
return path
|
| 157 |
+
except (AccessBlocked, DiskBudget, FileTooLarge): raise
|
| 158 |
+
except Exception:
|
| 159 |
+
if attempt + 1 == retries: raise
|
| 160 |
+
time.sleep(min(2**attempt, 8))
|
| 161 |
+
raise AssertionError("unreachable")
|
| 162 |
+
|
| 163 |
+
@staticmethod
|
| 164 |
+
def hash(path, alg="sha256"):
|
| 165 |
+
h = hashlib.new(alg)
|
| 166 |
+
with Path(path).open("rb") as f:
|
| 167 |
+
for chunk in iter(lambda: f.read(4 * 1024 * 1024), b""): h.update(chunk)
|
| 168 |
+
return h.hexdigest()
|
| 169 |
+
|
| 170 |
+
def batch(self, jobs: list[dict], workers=3):
|
| 171 |
+
errors = []
|
| 172 |
+
with ThreadPoolExecutor(workers) as pool:
|
| 173 |
+
futures = {pool.submit(self.get, **job): job["rel"] for job in jobs}
|
| 174 |
+
for future in as_completed(futures):
|
| 175 |
+
try: future.result()
|
| 176 |
+
except Exception as e:
|
| 177 |
+
errors.append({"path": futures[future], "error": exception_summary(e)})
|
| 178 |
+
if isinstance(e, DiskBudget):
|
| 179 |
+
for f in futures: f.cancel()
|
| 180 |
+
return errors
|
quasar/data/preprocessing.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Corpus download (pinned + licensed), text pretokenization, dataset validation and statistics.
|
| 2 |
+
|
| 3 |
+
Every external corpus is listed in SOURCES with a pinned revision and its license, and the
|
| 4 |
+
provenance record is written next to the downloaded files so caches can be audited later.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import hashlib
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
SOURCES: dict[str, dict] = {
|
| 16 |
+
"tinystories": {
|
| 17 |
+
"repo_id": "roneneldan/TinyStories",
|
| 18 |
+
"repo_type": "dataset",
|
| 19 |
+
"revision": "f54c09fd23315a6f9c86f9dc80f725de7d8f9c64",
|
| 20 |
+
"files": [
|
| 21 |
+
"data/train-00000-of-00004-2d5a1467fff1081b.parquet",
|
| 22 |
+
"data/validation-00000-of-00001-869c898b519ad725.parquet",
|
| 23 |
+
],
|
| 24 |
+
"license": "CDLA-Sharing-1.0",
|
| 25 |
+
"url": "https://huggingface.co/datasets/roneneldan/TinyStories",
|
| 26 |
+
"notes": "Synthetic children's stories (GPT-3.5/4 generated). Only 1 of 4 train shards is used.",
|
| 27 |
+
},
|
| 28 |
+
"dolly": {
|
| 29 |
+
"repo_id": "databricks/databricks-dolly-15k",
|
| 30 |
+
"repo_type": "dataset",
|
| 31 |
+
"revision": "bdd27f4d94b9c1f951818a7da7fd7aeea5dbff1a",
|
| 32 |
+
"files": ["databricks-dolly-15k.jsonl"],
|
| 33 |
+
"license": "CC-BY-SA-3.0",
|
| 34 |
+
"url": "https://huggingface.co/datasets/databricks/databricks-dolly-15k",
|
| 35 |
+
"notes": "Human-written instruction/response pairs; used for instruction tuning only.",
|
| 36 |
+
},
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def sha256_file(path: str | Path) -> str:
|
| 41 |
+
h = hashlib.sha256()
|
| 42 |
+
with open(path, "rb") as f:
|
| 43 |
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
| 44 |
+
h.update(chunk)
|
| 45 |
+
return h.hexdigest()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def download_sources(raw_dir: str | Path, names: list[str] | None = None) -> dict[str, list[Path]]:
|
| 49 |
+
"""Download the pinned corpus files and write a provenance.json record."""
|
| 50 |
+
from huggingface_hub import hf_hub_download
|
| 51 |
+
|
| 52 |
+
raw_dir = Path(raw_dir)
|
| 53 |
+
out: dict[str, list[Path]] = {}
|
| 54 |
+
for name in names or list(SOURCES):
|
| 55 |
+
src = SOURCES[name]
|
| 56 |
+
dest = raw_dir / name
|
| 57 |
+
paths = [
|
| 58 |
+
Path(hf_hub_download(src["repo_id"], f, repo_type=src["repo_type"], revision=src["revision"], local_dir=dest))
|
| 59 |
+
for f in src["files"]
|
| 60 |
+
]
|
| 61 |
+
record = {k: v for k, v in src.items()} | {"sha256": {p.name: sha256_file(p) for p in paths}}
|
| 62 |
+
(dest / "provenance.json").write_text(json.dumps(record, indent=2))
|
| 63 |
+
out[name] = paths
|
| 64 |
+
return out
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_tinystories(raw_dir: str | Path, split: str) -> list[str]:
|
| 68 |
+
import pyarrow.parquet as pq
|
| 69 |
+
|
| 70 |
+
files = sorted((Path(raw_dir) / "tinystories" / "data").glob(f"{split}-*.parquet"))
|
| 71 |
+
if not files:
|
| 72 |
+
raise FileNotFoundError(f"No TinyStories {split} parquet under {raw_dir}; run scripts/generate_dataset.py download")
|
| 73 |
+
texts: list[str] = []
|
| 74 |
+
for f in files:
|
| 75 |
+
texts += [t.strip() for t in pq.read_table(f, columns=["text"]).column("text").to_pylist() if t and t.strip()]
|
| 76 |
+
return texts
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_dolly(raw_dir: str | Path) -> list[dict]:
|
| 80 |
+
rows = []
|
| 81 |
+
with open(Path(raw_dir) / "dolly" / "databricks-dolly-15k.jsonl", encoding="utf-8") as f:
|
| 82 |
+
for line in f:
|
| 83 |
+
r = json.loads(line)
|
| 84 |
+
if r["instruction"].strip() and r["response"].strip():
|
| 85 |
+
rows.append(r)
|
| 86 |
+
return rows
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def tokenize_stream(texts: list[str], encode_batch, prefix: list[int], eos_id: int, out_path: str | Path) -> int:
|
| 90 |
+
"""Concatenate documents as <prefix> doc [EOS] ... into one uint16 token stream (for packing)."""
|
| 91 |
+
out_path = Path(out_path)
|
| 92 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 93 |
+
n = 0
|
| 94 |
+
with open(out_path, "wb") as f:
|
| 95 |
+
for i in range(0, len(texts), 4096):
|
| 96 |
+
arrs = [np.array([*prefix, *ids, eos_id], dtype=np.uint16) for ids in encode_batch(texts[i : i + 4096])]
|
| 97 |
+
buf = np.concatenate(arrs)
|
| 98 |
+
buf.tofile(f)
|
| 99 |
+
n += len(buf)
|
| 100 |
+
return n
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def validate_sequence(tokens: np.ndarray, vocab) -> None:
|
| 104 |
+
"""Raise ValueError if a token sequence violates the unified-vocabulary structure rules."""
|
| 105 |
+
vocab.check_structure(tokens)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def dataset_stats(cache_dir: str | Path) -> dict:
|
| 109 |
+
"""Summarise every cache file (token counts / sample counts) for docs and the dashboard."""
|
| 110 |
+
cache_dir = Path(cache_dir)
|
| 111 |
+
stats: dict[str, dict] = {}
|
| 112 |
+
for p in sorted(cache_dir.glob("*.bin")):
|
| 113 |
+
stats[p.name] = {"tokens": p.stat().st_size // 2}
|
| 114 |
+
for p in sorted(cache_dir.glob("*.npz")):
|
| 115 |
+
with np.load(p) as z:
|
| 116 |
+
stats[p.name] = {k: list(z[k].shape) for k in z.files}
|
| 117 |
+
return stats
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ----------------------------------------------------------------------------- media loading
|
| 121 |
+
def prepare_image(img, res: int = 64) -> np.ndarray:
|
| 122 |
+
"""PIL / array / path -> uint8 [res,res,3] (centre square crop, area resize)."""
|
| 123 |
+
if isinstance(img, (str, Path)):
|
| 124 |
+
img = Image.open(img)
|
| 125 |
+
if isinstance(img, np.ndarray):
|
| 126 |
+
img = Image.fromarray(img.astype(np.uint8))
|
| 127 |
+
img = img.convert("RGB")
|
| 128 |
+
w, h = img.size
|
| 129 |
+
s = min(w, h)
|
| 130 |
+
img = img.crop(((w - s) // 2, (h - s) // 2, (w - s) // 2 + s, (h - s) // 2 + s))
|
| 131 |
+
return np.asarray(img.resize((res, res), Image.BOX if s >= res else Image.BICUBIC))
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def load_video(src, max_frames: int = 8, fps: float = 4.0, res: int = 64) -> np.ndarray:
|
| 135 |
+
"""Video file (mp4/gif/webm...) or frame list -> uint8 [T,res,res,3], resampled to ~`fps`."""
|
| 136 |
+
if not isinstance(src, (str, Path)):
|
| 137 |
+
return np.stack([prepare_image(f, res) for f in list(src)[:max_frames]])
|
| 138 |
+
import imageio_ffmpeg
|
| 139 |
+
|
| 140 |
+
reader = imageio_ffmpeg.read_frames(str(src))
|
| 141 |
+
meta = next(reader)
|
| 142 |
+
w, h = meta["size"]
|
| 143 |
+
stride = max(1, round(float(meta.get("fps") or fps) / fps))
|
| 144 |
+
frames = []
|
| 145 |
+
for i, raw in enumerate(reader):
|
| 146 |
+
if i % stride == 0:
|
| 147 |
+
frames.append(prepare_image(np.frombuffer(raw, np.uint8).reshape(h, w, 3), res))
|
| 148 |
+
if len(frames) == max_frames:
|
| 149 |
+
break
|
| 150 |
+
reader.close()
|
| 151 |
+
if not frames:
|
| 152 |
+
raise ValueError(f"could not decode any frames from {src}")
|
| 153 |
+
return np.stack(frames)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def video_windows(path: str | Path, n_frames: int = 8, fps: float = 4.0, max_windows: int = 4, res: int = 64) -> list[np.ndarray]:
|
| 157 |
+
"""Cut a video file into up to `max_windows` consecutive `n_frames`-frame clips at ~`fps`.
|
| 158 |
+
Clips too short for one window are sampled evenly at native frame rate; < n_frames frames -> []."""
|
| 159 |
+
clip = load_video(path, max_frames=n_frames * max_windows, fps=fps, res=res)
|
| 160 |
+
if len(clip) >= n_frames:
|
| 161 |
+
return [clip[i : i + n_frames] for i in range(0, len(clip) - n_frames + 1, n_frames)]
|
| 162 |
+
native = load_video(path, max_frames=10_000, fps=1e9, res=res) # every frame
|
| 163 |
+
if len(native) < n_frames:
|
| 164 |
+
return []
|
| 165 |
+
return [native[np.linspace(0, len(native) - 1, n_frames).round().astype(int)]]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# ----------------------------------------------------------------------------- user datasets
|
| 169 |
+
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
|
| 170 |
+
VIDEO_EXT = {".mp4", ".mov", ".webm", ".mkv", ".avi", ".gif"}
|
| 171 |
+
USER_KEYS = ("user", "instruction", "prompt", "question", "input")
|
| 172 |
+
ASSISTANT_KEYS = ("assistant", "response", "output", "answer", "completion")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def scan_user_dataset(root: str | Path) -> dict[str, list]:
|
| 176 |
+
"""Collect samples from a user-provided folder or .jsonl file, routing records by their keys.
|
| 177 |
+
|
| 178 |
+
JSONL records (paths relative to the .jsonl file):
|
| 179 |
+
{"text": ...} -> text corpus
|
| 180 |
+
{"image": p, "caption": ...} -> image-caption (t2i + i2t)
|
| 181 |
+
{"image": p, "target_image": p, "instruction": ...} -> image editing (i2i)
|
| 182 |
+
{"video": p, "caption": ...} (caption optional) -> video-caption (t2v, v2t) / continuation
|
| 183 |
+
{"instruction"|"prompt"|"user": ..., "response"|"output"|"assistant": ...} -> instruction tuning
|
| 184 |
+
{"messages": [{"role": "user", ...}, {"role": "assistant", ...}, ...]} -> instruction tuning
|
| 185 |
+
Folders: *.txt -> text documents; image/video files with a same-name .txt sidecar -> captioned
|
| 186 |
+
media (images without a caption are skipped; videos without one are kept for continuation).
|
| 187 |
+
"""
|
| 188 |
+
root = Path(root)
|
| 189 |
+
out: dict[str, list] = {"texts": [], "images": [], "edits": [], "videos": [], "chats": [], "skipped": []}
|
| 190 |
+
files = [root] if root.is_file() else sorted(p for p in root.rglob("*") if p.is_file())
|
| 191 |
+
sidecar = {p.with_suffix(".txt") for p in files if p.suffix.lower() in IMAGE_EXT | VIDEO_EXT}
|
| 192 |
+
for p in files:
|
| 193 |
+
if p.suffix.lower() == ".jsonl":
|
| 194 |
+
for ln, line in enumerate(p.read_text(encoding="utf-8", errors="replace").splitlines()):
|
| 195 |
+
if not line.strip():
|
| 196 |
+
continue
|
| 197 |
+
try:
|
| 198 |
+
_route(json.loads(line), p.parent, out)
|
| 199 |
+
except (ValueError, KeyError, TypeError) as e:
|
| 200 |
+
out["skipped"].append(f"{p.name}:{ln + 1}: {e}")
|
| 201 |
+
# media referenced by a manifest record is not also scanned as a loose file
|
| 202 |
+
referenced = {Path(x).resolve() for k in ("images", "videos") for x, _ in out[k]} | {Path(x).resolve() for e in out["edits"] for x in e[:2]}
|
| 203 |
+
for p in files:
|
| 204 |
+
ext = p.suffix.lower()
|
| 205 |
+
if ext == ".jsonl" or p.resolve() in referenced:
|
| 206 |
+
continue
|
| 207 |
+
if ext == ".txt" and p not in sidecar:
|
| 208 |
+
t = p.read_text(encoding="utf-8", errors="replace").strip()
|
| 209 |
+
if t:
|
| 210 |
+
out["texts"].append(t)
|
| 211 |
+
elif ext in IMAGE_EXT | VIDEO_EXT:
|
| 212 |
+
cap_p = p.with_suffix(".txt")
|
| 213 |
+
cap = cap_p.read_text(encoding="utf-8", errors="replace").strip() if cap_p.exists() else ""
|
| 214 |
+
if ext in VIDEO_EXT:
|
| 215 |
+
out["videos"].append((p, cap))
|
| 216 |
+
elif cap:
|
| 217 |
+
out["images"].append((p, cap))
|
| 218 |
+
else:
|
| 219 |
+
out["skipped"].append(f"{p.name}: image without caption")
|
| 220 |
+
return out
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _route(r: dict, base: Path, out: dict) -> None:
|
| 224 |
+
get = lambda keys: next((str(r[k]).strip() for k in keys if r.get(k)), "")
|
| 225 |
+
if "messages" in r:
|
| 226 |
+
msgs = r["messages"]
|
| 227 |
+
for a, b in zip(msgs, msgs[1:]):
|
| 228 |
+
if a.get("role") == "user" and b.get("role") == "assistant" and a.get("content") and b.get("content"):
|
| 229 |
+
out["chats"].append((str(a["content"]).strip(), str(b["content"]).strip()))
|
| 230 |
+
elif "video" in r:
|
| 231 |
+
out["videos"].append((base / r["video"], get(("caption", "text", "prompt"))))
|
| 232 |
+
elif "image" in r and ("target_image" in r or "edited_image" in r):
|
| 233 |
+
out["edits"].append((base / r["image"], base / (r.get("target_image") or r["edited_image"]), get(("instruction", "edit", "prompt"))))
|
| 234 |
+
elif "image" in r:
|
| 235 |
+
cap = get(("caption", "text", "prompt"))
|
| 236 |
+
if not cap:
|
| 237 |
+
raise ValueError("image record without caption")
|
| 238 |
+
out["images"].append((base / r["image"], cap))
|
| 239 |
+
elif get(USER_KEYS) and get(ASSISTANT_KEYS):
|
| 240 |
+
user = get(USER_KEYS) + (("\n\n" + str(r["context"]).strip()) if r.get("context") else "")
|
| 241 |
+
out["chats"].append((user, get(ASSISTANT_KEYS)))
|
| 242 |
+
elif get(("text", "content", "story")):
|
| 243 |
+
out["texts"].append(get(("text", "content", "story")))
|
| 244 |
+
else:
|
| 245 |
+
raise ValueError(f"unrecognised record keys {sorted(r)}")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def split_of(key: str, val_pct: float) -> str:
|
| 249 |
+
"""Deterministic train/val assignment from a content hash (stable across runs and machines)."""
|
| 250 |
+
return "val" if int(hashlib.sha1(key.encode("utf-8", "replace")).hexdigest()[:8], 16) % 10_000 < val_pct * 100 else "train"
|
quasar/data/synthetic.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Procedural images and videos with exact ground truth, plus an analyzer that measures them.
|
| 2 |
+
|
| 3 |
+
Everything here is generated locally (no external data, no license questions). The same
|
| 4 |
+
attribute vocabulary is used for rendering, captioning and for *measuring* model outputs, so
|
| 5 |
+
prompt-following and motion can be scored quantitatively instead of by eyeballing samples.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import math
|
| 10 |
+
from dataclasses import dataclass, field, replace
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image, ImageDraw
|
| 14 |
+
|
| 15 |
+
RES = 64
|
| 16 |
+
SS = 4 # supersampling factor for anti-aliased edges
|
| 17 |
+
|
| 18 |
+
COLORS: dict[str, tuple[int, int, int]] = {
|
| 19 |
+
"red": (220, 40, 40), "green": (40, 180, 60), "blue": (50, 90, 235), "yellow": (240, 220, 50),
|
| 20 |
+
"purple": (150, 60, 200), "orange": (245, 140, 30), "white": (240, 240, 240), "cyan": (40, 210, 220),
|
| 21 |
+
}
|
| 22 |
+
BACKGROUNDS: dict[str, tuple[int, int, int]] = {
|
| 23 |
+
"black": (15, 15, 15), "gray": (105, 105, 105), "navy": (20, 30, 85), "brown": (85, 55, 30),
|
| 24 |
+
}
|
| 25 |
+
SHAPES = ("circle", "square", "triangle")
|
| 26 |
+
SIZES = {"small": 5.0, "medium": 9.0, "large": 13.0} # half-extent in pixels
|
| 27 |
+
GRID = { # name -> (col, row) in a 3x3 layout
|
| 28 |
+
"top left": (0, 0), "top": (1, 0), "top right": (2, 0),
|
| 29 |
+
"left": (0, 1), "center": (1, 1), "right": (2, 1),
|
| 30 |
+
"bottom left": (0, 2), "bottom": (1, 2), "bottom right": (2, 2),
|
| 31 |
+
}
|
| 32 |
+
CELL_CENTERS = (16.0, 32.0, 48.0)
|
| 33 |
+
POS_PHRASE = {
|
| 34 |
+
"top left": "in the top left", "top": "at the top", "top right": "in the top right",
|
| 35 |
+
"left": "on the left", "center": "in the center", "right": "on the right",
|
| 36 |
+
"bottom left": "in the bottom left", "bottom": "at the bottom", "bottom right": "in the bottom right",
|
| 37 |
+
}
|
| 38 |
+
# (shape, color) pairs never shown during training: compositional-generalisation probe.
|
| 39 |
+
HOLDOUT_COMBOS = {("triangle", "purple"), ("circle", "orange"), ("square", "cyan")}
|
| 40 |
+
|
| 41 |
+
MOTIONS = ("left", "right", "up", "down", "bounce", "cw", "ccw", "grow", "shrink", "still", "collide")
|
| 42 |
+
MOTION_PHRASE = {
|
| 43 |
+
"left": "moving left", "right": "moving right", "up": "moving up", "down": "moving down",
|
| 44 |
+
"cw": "rotating clockwise", "ccw": "rotating counterclockwise", "grow": "growing",
|
| 45 |
+
"shrink": "shrinking", "still": "standing still",
|
| 46 |
+
}
|
| 47 |
+
DIR_VEC = {"left": (-1, 0), "right": (1, 0), "up": (0, -1), "down": (0, 1)}
|
| 48 |
+
SHAPE_WORDS = {"circle": ["circle", "ball", "dot"], "square": ["square", "box"], "triangle": ["triangle"]}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class Obj:
|
| 53 |
+
shape: str
|
| 54 |
+
color: str
|
| 55 |
+
half: float
|
| 56 |
+
cx: float
|
| 57 |
+
cy: float
|
| 58 |
+
angle: float = 0.0 # degrees, clockwise on screen
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def size_name(self) -> str:
|
| 62 |
+
return min(SIZES, key=lambda k: abs(SIZES[k] - self.half))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclass
|
| 66 |
+
class ImageSpec:
|
| 67 |
+
obj: Obj
|
| 68 |
+
bg: str
|
| 69 |
+
pos: str
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclass
|
| 73 |
+
class VideoSpec:
|
| 74 |
+
frames: list[list[Obj]] # per frame, list of objects
|
| 75 |
+
bg: str
|
| 76 |
+
motion: str
|
| 77 |
+
hdir: str = "" # horizontal drift for "bounce"
|
| 78 |
+
meta: dict = field(default_factory=dict)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ----------------------------------------------------------------------------- rendering
|
| 82 |
+
def _polygon(o: Obj) -> list[tuple[float, float]]:
|
| 83 |
+
h = o.half
|
| 84 |
+
if o.shape == "square":
|
| 85 |
+
pts = [(-h, -h), (h, -h), (h, h), (-h, h)]
|
| 86 |
+
else: # triangle: apex up, base at bottom, fills half of its bounding square
|
| 87 |
+
pts = [(0.0, -h), (h, h), (-h, h)]
|
| 88 |
+
a = math.radians(o.angle)
|
| 89 |
+
c, s = math.cos(a), math.sin(a)
|
| 90 |
+
# y points down, so this standard rotation appears clockwise on screen for positive angles
|
| 91 |
+
return [((x * c - y * s + o.cx) * SS, (x * s + y * c + o.cy) * SS) for x, y in pts]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def render(objs: list[Obj], bg: str) -> np.ndarray:
|
| 95 |
+
"""Render objects onto a background; returns uint8 [64, 64, 3]."""
|
| 96 |
+
im = Image.new("RGB", (RES * SS, RES * SS), BACKGROUNDS[bg])
|
| 97 |
+
d = ImageDraw.Draw(im)
|
| 98 |
+
for o in objs:
|
| 99 |
+
if o.shape == "circle":
|
| 100 |
+
d.ellipse([(o.cx - o.half) * SS, (o.cy - o.half) * SS, (o.cx + o.half) * SS, (o.cy + o.half) * SS], fill=COLORS[o.color])
|
| 101 |
+
else:
|
| 102 |
+
d.polygon(_polygon(o), fill=COLORS[o.color])
|
| 103 |
+
return np.asarray(im.resize((RES, RES), Image.BOX))
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def render_video(spec: VideoSpec) -> np.ndarray:
|
| 107 |
+
return np.stack([render(objs, spec.bg) for objs in spec.frames])
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ----------------------------------------------------------------------------- sampling
|
| 111 |
+
def _combo_ok(shape: str, color: str, holdout: str) -> bool:
|
| 112 |
+
"""holdout: 'exclude' (training), 'only' (generalisation probe) or 'any'."""
|
| 113 |
+
held = (shape, color) in HOLDOUT_COMBOS
|
| 114 |
+
return holdout == "any" or (held if holdout == "only" else not held)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _pick_shape_color(rng: np.random.Generator, holdout: str, shapes=SHAPES) -> tuple[str, str]:
|
| 118 |
+
while True:
|
| 119 |
+
shape, color = str(rng.choice(shapes)), str(rng.choice(list(COLORS)))
|
| 120 |
+
if _combo_ok(shape, color, holdout):
|
| 121 |
+
return shape, color
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def sample_image_spec(rng: np.random.Generator, holdout: str = "exclude") -> ImageSpec:
|
| 125 |
+
shape, color = _pick_shape_color(rng, holdout)
|
| 126 |
+
size = str(rng.choice(list(SIZES)))
|
| 127 |
+
pos = str(rng.choice(list(GRID)))
|
| 128 |
+
return _place(shape, color, size, pos, str(rng.choice(list(BACKGROUNDS))), rng)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _place(shape: str, color: str, size: str, pos: str, bg: str, rng: np.random.Generator) -> ImageSpec:
|
| 132 |
+
half = SIZES[size]
|
| 133 |
+
col, row = GRID[pos]
|
| 134 |
+
j = lambda: float(rng.uniform(-3, 3))
|
| 135 |
+
cx = float(np.clip(CELL_CENTERS[col] + j(), half + 1, RES - half - 1))
|
| 136 |
+
cy = float(np.clip(CELL_CENTERS[row] + j(), half + 1, RES - half - 1))
|
| 137 |
+
return ImageSpec(Obj(shape, color, half, cx, cy), bg, pos)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def make_image_spec(shape: str, color: str, size: str, pos: str, bg: str, seed: int = 0) -> ImageSpec:
|
| 141 |
+
return _place(shape, color, size, pos, bg, np.random.default_rng(seed))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
EDITS = ("color", "move", "size", "background", "shape")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def sample_edit(rng: np.random.Generator, src: ImageSpec) -> tuple[str, ImageSpec, str]:
|
| 148 |
+
"""Pick an edit; returns (edit_type, target_spec, canonical instruction)."""
|
| 149 |
+
o = src.obj
|
| 150 |
+
while True:
|
| 151 |
+
kind = str(rng.choice(EDITS))
|
| 152 |
+
if kind == "color":
|
| 153 |
+
c = str(rng.choice([c for c in COLORS if c != o.color]))
|
| 154 |
+
if _combo_ok(o.shape, c, "exclude"):
|
| 155 |
+
return kind, replace(src, obj=replace(o, color=c)), f"change the color to {c}"
|
| 156 |
+
elif kind == "shape":
|
| 157 |
+
s = str(rng.choice([s for s in SHAPES if s != o.shape]))
|
| 158 |
+
if _combo_ok(s, o.color, "exclude"):
|
| 159 |
+
return kind, replace(src, obj=replace(o, shape=s)), f"turn it into a {s}"
|
| 160 |
+
elif kind == "size":
|
| 161 |
+
z = str(rng.choice([z for z in SIZES if z != o.size_name]))
|
| 162 |
+
t = _place(o.shape, o.color, z, src.pos, src.bg, rng)
|
| 163 |
+
return kind, replace(t, obj=replace(t.obj, cx=float(np.clip(o.cx, SIZES[z] + 1, RES - SIZES[z] - 1)),
|
| 164 |
+
cy=float(np.clip(o.cy, SIZES[z] + 1, RES - SIZES[z] - 1)))), f"make it {z}"
|
| 165 |
+
elif kind == "move":
|
| 166 |
+
p = str(rng.choice([p for p in GRID if p != src.pos]))
|
| 167 |
+
return kind, _place(o.shape, o.color, o.size_name, p, src.bg, rng), f"move it to the {p}"
|
| 168 |
+
else:
|
| 169 |
+
b = str(rng.choice([b for b in BACKGROUNDS if b != src.bg]))
|
| 170 |
+
return kind, replace(src, bg=b), f"change the background to {b}"
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def sample_video_spec(rng: np.random.Generator, n_frames: int = 8, holdout: str = "exclude", motion: str | None = None) -> VideoSpec:
|
| 174 |
+
motion = motion or str(rng.choice(MOTIONS))
|
| 175 |
+
bg = str(rng.choice(list(BACKGROUNDS)))
|
| 176 |
+
T = n_frames
|
| 177 |
+
if motion == "collide":
|
| 178 |
+
# exclude: neither ball is a held-out combo; only: at least one is (only orange circles are held out)
|
| 179 |
+
ok = lambda a, b: holdout == "any" or (("circle", a) in HOLDOUT_COMBOS or ("circle", b) in HOLDOUT_COMBOS) == (holdout == "only")
|
| 180 |
+
c1, c2 = rng.choice([c for c in COLORS], size=2, replace=False)
|
| 181 |
+
while not ok(str(c1), str(c2)):
|
| 182 |
+
c1, c2 = rng.choice([c for c in COLORS], size=2, replace=False)
|
| 183 |
+
half, v = 6.0, float(rng.uniform(3, 4.5))
|
| 184 |
+
y = float(rng.uniform(16, 48))
|
| 185 |
+
x1, x2 = float(rng.uniform(8, 14)), float(rng.uniform(50, 56))
|
| 186 |
+
frames = []
|
| 187 |
+
v1, v2 = v, -v
|
| 188 |
+
for _ in range(T):
|
| 189 |
+
frames.append([Obj("circle", str(c1), half, x1, y), Obj("circle", str(c2), half, x2, y)])
|
| 190 |
+
x1, x2 = x1 + v1, x2 + v2
|
| 191 |
+
if x2 - x1 <= 2 * half: # elastic collision: swap velocities
|
| 192 |
+
v1, v2 = v2, v1
|
| 193 |
+
x1, x2 = x1 + 2 * v1, x2 + 2 * v2
|
| 194 |
+
return VideoSpec(frames, bg, motion, meta={"colors": [str(c1), str(c2)]})
|
| 195 |
+
|
| 196 |
+
shapes = ("circle",) if motion == "bounce" else ("square", "triangle") if motion in ("cw", "ccw") else SHAPES
|
| 197 |
+
shape, color = _pick_shape_color(rng, holdout, shapes)
|
| 198 |
+
size = str(rng.choice(["small", "medium"] if motion in ("left", "right", "up", "down", "bounce") else list(SIZES)))
|
| 199 |
+
half = SIZES[size]
|
| 200 |
+
lo, hi = half + 1, RES - half - 1
|
| 201 |
+
frames: list[list[Obj]] = []
|
| 202 |
+
hdir = ""
|
| 203 |
+
if motion in DIR_VEC:
|
| 204 |
+
dx, dy = DIR_VEC[motion]
|
| 205 |
+
v = float(rng.uniform(3, 5))
|
| 206 |
+
travel = v * (T - 1)
|
| 207 |
+
start = lambda d: float(rng.uniform(lo, hi - travel)) if d > 0 else float(rng.uniform(lo + travel, hi)) if d < 0 else float(rng.uniform(lo, hi))
|
| 208 |
+
x, y = start(dx), start(dy)
|
| 209 |
+
frames = [[Obj(shape, color, half, x + dx * v * t, y + dy * v * t)] for t in range(T)]
|
| 210 |
+
elif motion == "bounce":
|
| 211 |
+
vx = float(rng.uniform(1.5, 3)) * (1 if rng.random() < 0.5 else -1)
|
| 212 |
+
hdir = "right" if vx > 0 else "left"
|
| 213 |
+
x = float(rng.uniform(lo, hi - 7 * abs(vx))) if vx > 0 else float(rng.uniform(lo + 7 * abs(vx), hi))
|
| 214 |
+
y, vy, g = float(rng.uniform(lo, 26)), 0.0, float(rng.uniform(2.5, 4.0))
|
| 215 |
+
for _ in range(T):
|
| 216 |
+
frames.append([Obj(shape, color, half, x, y)])
|
| 217 |
+
for _ in range(4): # substeps for a clean bounce
|
| 218 |
+
vy += g / 4
|
| 219 |
+
y += vy / 4
|
| 220 |
+
if y > hi:
|
| 221 |
+
y, vy = 2 * hi - y, -0.9 * vy
|
| 222 |
+
x += vx
|
| 223 |
+
elif motion in ("cw", "ccw"):
|
| 224 |
+
step = float(rng.uniform(12, 22)) * (1 if motion == "cw" else -1)
|
| 225 |
+
x, y, a0 = float(rng.uniform(lo, hi)), float(rng.uniform(lo, hi)), float(rng.uniform(0, 90))
|
| 226 |
+
frames = [[Obj(shape, color, half, x, y, a0 + step * t)] for t in range(T)]
|
| 227 |
+
elif motion in ("grow", "shrink"):
|
| 228 |
+
a, b = (4.0, 14.0) if motion == "grow" else (14.0, 4.0)
|
| 229 |
+
x, y = float(rng.uniform(15, 49)), float(rng.uniform(15, 49))
|
| 230 |
+
frames = [[Obj(shape, color, a + (b - a) * t / (T - 1), x, y)] for t in range(T)]
|
| 231 |
+
else: # still
|
| 232 |
+
x, y = float(rng.uniform(lo, hi)), float(rng.uniform(lo, hi))
|
| 233 |
+
frames = [[Obj(shape, color, half, x, y)] for _ in range(T)]
|
| 234 |
+
return VideoSpec(frames, bg, motion, hdir)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def render_clutter(rng: np.random.Generator) -> np.ndarray:
|
| 238 |
+
"""Tokenizer-only augmentation: 1-6 shapes in arbitrary RGB colours on a random gradient.
|
| 239 |
+
|
| 240 |
+
Broadens what the visual codec can represent beyond the 8-colour palette (e.g. user uploads).
|
| 241 |
+
"""
|
| 242 |
+
c0, c1 = rng.integers(0, 256, 3), rng.integers(0, 256, 3)
|
| 243 |
+
t = np.linspace(0, 1, RES * SS)[:, None, None] if rng.random() < 0.5 else np.linspace(0, 1, RES * SS)[None, :, None]
|
| 244 |
+
base = (c0 * (1 - t) + c1 * t).astype(np.uint8)
|
| 245 |
+
im = Image.fromarray(np.broadcast_to(base, (RES * SS, RES * SS, 3)).copy())
|
| 246 |
+
d = ImageDraw.Draw(im)
|
| 247 |
+
for _ in range(int(rng.integers(1, 7))):
|
| 248 |
+
o = Obj(str(rng.choice(SHAPES)), "red", float(rng.uniform(3, 20)), float(rng.uniform(0, RES)), float(rng.uniform(0, RES)), float(rng.uniform(0, 360)))
|
| 249 |
+
fill = tuple(int(v) for v in rng.integers(0, 256, 3))
|
| 250 |
+
if o.shape == "circle":
|
| 251 |
+
d.ellipse([(o.cx - o.half) * SS, (o.cy - o.half) * SS, (o.cx + o.half) * SS, (o.cy + o.half) * SS], fill=fill)
|
| 252 |
+
else:
|
| 253 |
+
d.polygon(_polygon(o), fill=fill)
|
| 254 |
+
return np.asarray(im.resize((RES, RES), Image.BOX))
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ----------------------------------------------------------------------------- captions
|
| 258 |
+
def caption_image(spec: ImageSpec, rng: np.random.Generator | None = None) -> str:
|
| 259 |
+
"""Canonical caption when rng is None (used as a target); otherwise a random paraphrase (prompt)."""
|
| 260 |
+
o = spec.obj
|
| 261 |
+
if rng is None:
|
| 262 |
+
return f"a {o.size_name} {o.color} {o.shape} {POS_PHRASE[spec.pos]} on a {spec.bg} background"
|
| 263 |
+
w = str(rng.choice(SHAPE_WORDS[o.shape]))
|
| 264 |
+
templates = [
|
| 265 |
+
f"a {o.size_name} {o.color} {w} {POS_PHRASE[spec.pos]} on a {spec.bg} background",
|
| 266 |
+
f"a {o.color} {w} {POS_PHRASE[spec.pos]} on a {spec.bg} background, {o.size_name}",
|
| 267 |
+
f"{spec.bg} background with a {o.size_name} {o.color} {w} {POS_PHRASE[spec.pos]}",
|
| 268 |
+
f"an image of a {o.size_name} {o.color} {w} {POS_PHRASE[spec.pos]}, {spec.bg} background",
|
| 269 |
+
f"a {o.size_name} {o.color} {w} {POS_PHRASE[spec.pos]} on a {spec.bg} background",
|
| 270 |
+
]
|
| 271 |
+
return str(rng.choice(templates))
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def caption_video(spec: VideoSpec, rng: np.random.Generator | None = None) -> str:
|
| 275 |
+
if spec.motion == "collide":
|
| 276 |
+
c1, c2 = spec.meta["colors"]
|
| 277 |
+
return f"a {c1} circle and a {c2} circle colliding on a {spec.bg} background"
|
| 278 |
+
o = spec.frames[0][0]
|
| 279 |
+
size = "medium" if spec.motion in ("grow", "shrink") else o.size_name
|
| 280 |
+
if spec.motion == "bounce":
|
| 281 |
+
name = "ball" if rng is None else str(rng.choice(["ball", "circle"]))
|
| 282 |
+
what = f"a {size} {o.color} {name} bouncing to the {spec.hdir}"
|
| 283 |
+
else:
|
| 284 |
+
name = o.shape if rng is None else str(rng.choice(SHAPE_WORDS[o.shape]))
|
| 285 |
+
what = f"a {size} {o.color} {name} {MOTION_PHRASE[spec.motion]}"
|
| 286 |
+
if spec.motion in ("grow", "shrink"):
|
| 287 |
+
what = f"a {o.color} {name} {MOTION_PHRASE[spec.motion]}"
|
| 288 |
+
if rng is None or rng.random() < 0.6:
|
| 289 |
+
return f"{what} on a {spec.bg} background"
|
| 290 |
+
return str(rng.choice([f"{what}", f"video of {what} on a {spec.bg} background", f"{spec.bg} background, {what}"]))
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def parse_caption(text: str) -> dict:
|
| 294 |
+
"""Extract attribute words from (generated) caption text; missing attributes map to None."""
|
| 295 |
+
t = " " + text.lower().replace(",", " ") + " "
|
| 296 |
+
find = lambda opts: next((o for o in sorted(opts, key=len, reverse=True) if f" {o} " in t), None)
|
| 297 |
+
shape = find(["circle", "square", "triangle", "ball", "box", "dot"])
|
| 298 |
+
shape = {"ball": "circle", "dot": "circle", "box": "square"}.get(shape, shape)
|
| 299 |
+
pos = next((p for p, ph in sorted(POS_PHRASE.items(), key=lambda kv: -len(kv[1])) if ph in t), None)
|
| 300 |
+
bg = next((b for b in BACKGROUNDS if f"{b} background" in t), None)
|
| 301 |
+
motion = next((m for m, ph in MOTION_PHRASE.items() if ph in t), None)
|
| 302 |
+
if " bouncing" in t:
|
| 303 |
+
motion = "bounce"
|
| 304 |
+
if " colliding" in t:
|
| 305 |
+
motion = "collide"
|
| 306 |
+
color = next((w for w in t.split() if w in COLORS), None) # fg and bg colour names never overlap
|
| 307 |
+
return {"shape": shape, "color": color, "size": find(list(SIZES)), "pos": pos, "bg": bg, "motion": motion}
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
# ----------------------------------------------------------------------------- analysis
|
| 311 |
+
def _nearest(rgb: np.ndarray, table: dict[str, tuple[int, int, int]]) -> str:
|
| 312 |
+
names = list(table)
|
| 313 |
+
d = ((np.array([table[n] for n in names], dtype=np.float32) - rgb.astype(np.float32)) ** 2).sum(-1)
|
| 314 |
+
return names[int(d.argmin())]
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def analyze_image(img: np.ndarray, min_area: int = 12) -> dict:
|
| 318 |
+
"""Measure background, dominant object colour/shape/size/position of a uint8 [H,W,3] image."""
|
| 319 |
+
img = np.asarray(img)
|
| 320 |
+
border = np.concatenate([img[0], img[-1], img[:, 0], img[:, -1]]).astype(np.float32)
|
| 321 |
+
bg_rgb = np.median(border, axis=0)
|
| 322 |
+
bg = _nearest(bg_rgb, BACKGROUNDS)
|
| 323 |
+
dist = np.sqrt(((img.astype(np.float32) - bg_rgb) ** 2).sum(-1))
|
| 324 |
+
mask = dist > 70
|
| 325 |
+
area = int(mask.sum())
|
| 326 |
+
out = {"bg": bg, "area": area, "color": None, "shape": None, "size": None, "pos": None, "cx": None, "cy": None}
|
| 327 |
+
if area < min_area:
|
| 328 |
+
return out
|
| 329 |
+
ys, xs = np.nonzero(mask)
|
| 330 |
+
w, h = xs.max() - xs.min() + 1, ys.max() - ys.min() + 1
|
| 331 |
+
cx, cy = (xs.min() + xs.max()) / 2.0, (ys.min() + ys.max()) / 2.0 # bbox centre (triangle mass centroid sits low)
|
| 332 |
+
fill = area / float(w * h)
|
| 333 |
+
out.update(
|
| 334 |
+
color=_nearest(np.median(img[mask], axis=0), COLORS),
|
| 335 |
+
shape="square" if fill > 0.88 else "circle" if fill > 0.69 else "triangle",
|
| 336 |
+
size=min(SIZES, key=lambda k: abs(SIZES[k] - max(w, h) / 2)),
|
| 337 |
+
cx=cx, cy=cy,
|
| 338 |
+
pos=next(p for p, (c, r) in GRID.items() if c == min(2, int(cx // (RES / 3))) and r == min(2, int(cy // (RES / 3)))),
|
| 339 |
+
)
|
| 340 |
+
return out
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def analyze_video(frames: np.ndarray) -> dict:
|
| 344 |
+
"""Per-frame analysis + trajectory summary for single-object clips."""
|
| 345 |
+
per = [analyze_image(f) for f in frames]
|
| 346 |
+
pts = [(p["cx"], p["cy"]) for p in per if p["cx"] is not None]
|
| 347 |
+
diffs = [float(np.abs(frames[i + 1].astype(np.float32) - frames[i].astype(np.float32)).mean()) for i in range(len(frames) - 1)]
|
| 348 |
+
out = {"frames": per, "detected": len(pts), "frame_diffs": diffs}
|
| 349 |
+
if len(pts) >= 2:
|
| 350 |
+
out["dx"], out["dy"] = pts[-1][0] - pts[0][0], pts[-1][1] - pts[0][1]
|
| 351 |
+
areas = [p["area"] for p in per if p["cx"] is not None]
|
| 352 |
+
out["area_ratio"] = areas[-1] / max(1, areas[0])
|
| 353 |
+
return out
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def motion_direction(dx: float, dy: float, min_travel: float = 6.0) -> str:
|
| 357 |
+
"""Classify a displacement as left/right/up/down/still."""
|
| 358 |
+
if max(abs(dx), abs(dy)) < min_travel:
|
| 359 |
+
return "still"
|
| 360 |
+
if abs(dx) >= abs(dy):
|
| 361 |
+
return "right" if dx > 0 else "left"
|
| 362 |
+
return "down" if dy > 0 else "up"
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def _self_check() -> None:
|
| 366 |
+
rng = np.random.default_rng(0)
|
| 367 |
+
ok = 0
|
| 368 |
+
for _ in range(200):
|
| 369 |
+
s = sample_image_spec(rng)
|
| 370 |
+
a = analyze_image(render([s.obj], s.bg))
|
| 371 |
+
ok += (a["color"], a["shape"], a["pos"], a["bg"], a["size"]) == (s.obj.color, s.obj.shape, s.pos, s.bg, s.obj.size_name)
|
| 372 |
+
print(f"analyzer exact-match on ground truth renders: {ok}/200")
|
| 373 |
+
p = parse_caption(caption_image(s))
|
| 374 |
+
assert (p["shape"], p["color"], p["pos"], p["bg"], p["size"]) == (s.obj.shape, s.obj.color, s.pos, s.bg, s.obj.size_name), p
|
| 375 |
+
for m in ("left", "right", "up", "down"):
|
| 376 |
+
v = sample_video_spec(rng, motion=m)
|
| 377 |
+
a = analyze_video(render_video(v))
|
| 378 |
+
assert motion_direction(a["dx"], a["dy"]) == m, (m, a["dx"], a["dy"])
|
| 379 |
+
assert parse_caption(caption_video(v))["motion"] == m
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
if __name__ == "__main__":
|
| 383 |
+
_self_check()
|
quasar/data/text_preparation.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bounded offline preparation of the initial real text/chat/Python corpus.
|
| 2 |
+
|
| 3 |
+
SQLite spools documents and deduplication keys instead of retaining a corpus in
|
| 4 |
+
RAM. No downloads, archive extraction, GPU calls, or optimizer steps occur here.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from collections import Counter
|
| 9 |
+
from copy import deepcopy
|
| 10 |
+
import hashlib
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import shutil
|
| 15 |
+
import sqlite3
|
| 16 |
+
import time
|
| 17 |
+
from urllib.parse import urlsplit, urlunsplit
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import pyarrow.parquet as pq
|
| 21 |
+
import yaml
|
| 22 |
+
|
| 23 |
+
from .adapters import normalize
|
| 24 |
+
from .curated import canonical, digest, verify_record
|
| 25 |
+
from .preprocessing import sha256_file
|
| 26 |
+
from ..tokenizers.text import TextTokenizer
|
| 27 |
+
from ..tokenizers.universal import UniversalVocab
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def atomic_json(path: Path, value: dict) -> None:
|
| 31 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 32 |
+
tmp = path.with_name(path.name + ".tmp")
|
| 33 |
+
tmp.write_text(json.dumps(value, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 34 |
+
for attempt in range(8):
|
| 35 |
+
try:
|
| 36 |
+
os.replace(tmp, path)
|
| 37 |
+
return
|
| 38 |
+
except PermissionError:
|
| 39 |
+
if attempt == 7:
|
| 40 |
+
raise
|
| 41 |
+
time.sleep(0.01 * 2**attempt)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def normalized_hash(text: str) -> str:
|
| 45 |
+
return hashlib.sha256(" ".join(text.split()).encode("utf-8")).hexdigest()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def document_group(url: str, text: str) -> str:
|
| 49 |
+
"""Group HTTP/HTTPS aliases and fragments of the same document together."""
|
| 50 |
+
if not url:
|
| 51 |
+
return normalized_hash(text)
|
| 52 |
+
p = urlsplit(url)
|
| 53 |
+
return urlunsplit(("https", p.netloc.lower(), p.path.rstrip("/"), p.query, ""))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def parquet_rows(path: Path):
|
| 57 |
+
for batch in pq.ParquetFile(path).iter_batches(batch_size=128):
|
| 58 |
+
yield from batch.to_pylist()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def pinned_source_sha(path: Path, root: Path, dataset: str, revision: str, ledger: dict) -> str:
|
| 62 |
+
"""Starter downloads retain pinned LFS hashes in HF's local metadata file."""
|
| 63 |
+
rel = path.relative_to(root).as_posix()
|
| 64 |
+
if rel in ledger:
|
| 65 |
+
expected = ledger[rel]["sha256"]
|
| 66 |
+
else:
|
| 67 |
+
relative = path.relative_to(root / dataset)
|
| 68 |
+
metadata = root / dataset / ".cache/huggingface/download" / (str(relative) + ".metadata")
|
| 69 |
+
lines = metadata.read_text(encoding="utf-8").splitlines()
|
| 70 |
+
if len(lines) < 2 or lines[0] != revision:
|
| 71 |
+
raise ValueError(f"missing or mismatched pinned acquisition metadata: {rel}")
|
| 72 |
+
expected = lines[1]
|
| 73 |
+
if len(expected) != 64 or any(c not in "0123456789abcdef" for c in expected):
|
| 74 |
+
raise ValueError(f"source has no pinned SHA256: {rel}")
|
| 75 |
+
return expected
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def text_windows(text: str, tok: TextTokenizer, limit: int):
|
| 79 |
+
"""Segment at Unicode character/whitespace boundaries; never cut byte tokens."""
|
| 80 |
+
remaining = text.strip()
|
| 81 |
+
while remaining:
|
| 82 |
+
encoded = tok.tok.encode(remaining)
|
| 83 |
+
if len(encoded.ids) <= limit:
|
| 84 |
+
yield remaining
|
| 85 |
+
return
|
| 86 |
+
cut = encoded.offsets[limit - 1][1]
|
| 87 |
+
# Prefer a nearby complete word while retaining at least ~75% capacity.
|
| 88 |
+
lower = encoded.offsets[max(0, limit * 3 // 4 - 1)][1]
|
| 89 |
+
boundary = max(remaining.rfind(" ", lower, cut), remaining.rfind("\n", lower, cut))
|
| 90 |
+
if boundary > lower:
|
| 91 |
+
cut = boundary
|
| 92 |
+
while cut > 0 and len(tok.encode(remaining[:cut])) > limit:
|
| 93 |
+
cut -= 1
|
| 94 |
+
if cut <= 0:
|
| 95 |
+
raise ValueError("cannot segment text within context")
|
| 96 |
+
yield remaining[:cut].strip()
|
| 97 |
+
remaining = remaining[cut:].strip()
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def text_sample(record: dict, tok: TextTokenizer, vocab: UniversalVocab, context: int):
|
| 101 |
+
"""Exact UniversalVocab text packet layout, validated in bulk with NumPy."""
|
| 102 |
+
s = vocab.special
|
| 103 |
+
ids = [s("bos"), s("task:" + record["task"]), s("input")]
|
| 104 |
+
for segment in record["inputs"]:
|
| 105 |
+
ids.extend([s("text"), *tok.encode(segment["text"]), s("/text")])
|
| 106 |
+
ids.append(s("output"))
|
| 107 |
+
target = len(ids)
|
| 108 |
+
for segment in record["outputs"]:
|
| 109 |
+
ids.extend([s("text"), *tok.encode(segment["text"]), s("/text")])
|
| 110 |
+
ids.append(s("eos"))
|
| 111 |
+
tokens = np.array(ids, dtype="<u2")
|
| 112 |
+
validate_text_sample(tokens, target, record["task"], vocab, context)
|
| 113 |
+
return tokens, target
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def validate_text_sample(tokens: np.ndarray, target: int, task: str, v: UniversalVocab, context: int) -> None:
|
| 117 |
+
"""Strict text-only subset of the universal grammar; reserved IDs are rejected."""
|
| 118 |
+
s = v.special
|
| 119 |
+
if task not in ("text", "chat_text", "coding") or not 7 <= len(tokens) <= context + 1:
|
| 120 |
+
raise ValueError("invalid task or context length")
|
| 121 |
+
if tokens[:3].tolist() != [s("bos"), s("task:" + task), s("input")] or tokens[-1] != s("eos"):
|
| 122 |
+
raise ValueError("invalid sequence boundary")
|
| 123 |
+
if not 4 <= target < len(tokens) or tokens[target - 1] != s("output"):
|
| 124 |
+
raise ValueError("invalid supervision boundary")
|
| 125 |
+
for start, end, required in ((3, target - 1, task != "text"), (target, len(tokens) - 1, True)):
|
| 126 |
+
count = 0
|
| 127 |
+
while start < end:
|
| 128 |
+
if tokens[start] != s("text"):
|
| 129 |
+
raise ValueError("invalid text packet start")
|
| 130 |
+
closes = np.flatnonzero(tokens[start + 1:end] >= v.text_size)
|
| 131 |
+
if not len(closes):
|
| 132 |
+
raise ValueError("unterminated text packet")
|
| 133 |
+
close = start + 1 + int(closes[0])
|
| 134 |
+
if tokens[close] != s("/text"):
|
| 135 |
+
raise ValueError("non-text token in text packet")
|
| 136 |
+
start, count = close + 1, count + 1
|
| 137 |
+
if required and count == 0:
|
| 138 |
+
raise ValueError("empty packet side")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class TextPreparation:
|
| 142 |
+
def __init__(self, config: dict, status_path: Path | None = None):
|
| 143 |
+
self.cfg, self.status_path = config, status_path
|
| 144 |
+
self.work, self.out = Path(config["work"]), Path(config["output"])
|
| 145 |
+
if self.work.exists() or self.out.exists():
|
| 146 |
+
raise FileExistsError("preserve existing preparation/cache; inspect it before selecting a new work directory")
|
| 147 |
+
self.guard()
|
| 148 |
+
self.work.mkdir(parents=True)
|
| 149 |
+
self.catalog = yaml.safe_load(Path(config["catalog"]).read_text(encoding="utf-8"))
|
| 150 |
+
self.catalog = deepcopy(self.catalog)
|
| 151 |
+
self.counts, self.source_files = Counter(), []
|
| 152 |
+
self.started = time.time()
|
| 153 |
+
self.db = sqlite3.connect(self.work / "records.sqlite")
|
| 154 |
+
self.db.executescript("""
|
| 155 |
+
PRAGMA journal_mode=WAL;
|
| 156 |
+
PRAGMA cache_size=-32768;
|
| 157 |
+
CREATE TABLE records (id INTEGER PRIMARY KEY, dataset TEXT, split TEXT, payload TEXT);
|
| 158 |
+
CREATE TABLE groups (dataset TEXT, name TEXT, split TEXT, PRIMARY KEY(dataset,name));
|
| 159 |
+
CREATE TABLE contents (hash TEXT PRIMARY KEY, split TEXT);
|
| 160 |
+
""")
|
| 161 |
+
|
| 162 |
+
def guard(self):
|
| 163 |
+
anchor = self.work if self.work.exists() else self.work.parent
|
| 164 |
+
if shutil.disk_usage(anchor).free < self.cfg["reserve_gib"] * 2**30:
|
| 165 |
+
raise RuntimeError("25 GiB free-space reserve reached")
|
| 166 |
+
if self.work.exists():
|
| 167 |
+
used = sum(p.stat().st_size for p in self.work.rglob("*") if p.is_file())
|
| 168 |
+
if self.out.exists():
|
| 169 |
+
used += sum(p.stat().st_size for p in self.out.rglob("*") if p.is_file())
|
| 170 |
+
if used > self.cfg["max_work_gib"] * 2**30:
|
| 171 |
+
raise RuntimeError("bounded preparation disk budget reached")
|
| 172 |
+
|
| 173 |
+
def progress(self, phase: str, **fields):
|
| 174 |
+
self.guard()
|
| 175 |
+
record = {"phase": phase, "pid": os.getpid(), "updated": time.time(), "elapsed_s": time.time() - self.started,
|
| 176 |
+
"training_started": False, "counts": dict(self.counts), **fields}
|
| 177 |
+
atomic_json(self.work / "progress.json", record)
|
| 178 |
+
if self.status_path:
|
| 179 |
+
state = json.loads(self.status_path.read_text(encoding="utf-8")) if self.status_path.exists() else {}
|
| 180 |
+
state.update(state="preparing_data", updated=time.time(),
|
| 181 |
+
message="CPU data preparation: " + phase, preprocessing_elapsed_s=record["elapsed_s"])
|
| 182 |
+
atomic_json(self.status_path, state)
|
| 183 |
+
print(json.dumps({k: v for k, v in record.items() if k != "counts"}), flush=True)
|
| 184 |
+
|
| 185 |
+
def source(self, path: Path, expected: str | None = None) -> str:
|
| 186 |
+
sha = sha256_file(path)
|
| 187 |
+
if expected and sha != expected:
|
| 188 |
+
raise ValueError(f"source checksum mismatch: {path}")
|
| 189 |
+
self.source_files.append({"path": str(path), "bytes": path.stat().st_size, "sha256": sha})
|
| 190 |
+
return sha
|
| 191 |
+
|
| 192 |
+
def provenance(self, ds: str, ident: str, split: str, path: Path, sha: str, source_split: str, **extra):
|
| 193 |
+
d = self.catalog["datasets"][ds]
|
| 194 |
+
return {"id": ds + ":" + ident, "revision": d.get("revision", d.get("release")), "license": d["license"],
|
| 195 |
+
"attribution": d.get("attribution", d.get("repo_id", ds)) + "; see pinned dataset card and retained source metadata",
|
| 196 |
+
"source_url": d["source"], "source_file": str(path), "source_sha256": sha,
|
| 197 |
+
"source_split": source_split, "split": split, **extra}
|
| 198 |
+
|
| 199 |
+
def add(self, record: dict) -> bool:
|
| 200 |
+
verify_record(record, self.catalog)
|
| 201 |
+
if any(not segment.get("text", "").strip() for segment in record["inputs"] + record["outputs"]):
|
| 202 |
+
self.counts["empty_text_excluded"] += 1
|
| 203 |
+
return False
|
| 204 |
+
key = normalized_hash("\n".join(x["text"] for x in record["inputs"] + record["outputs"]))
|
| 205 |
+
old = self.db.execute("SELECT split FROM contents WHERE hash=?", (key,)).fetchone()
|
| 206 |
+
group = (record["dataset"], str(record["group"]))
|
| 207 |
+
prior = self.db.execute("SELECT split FROM groups WHERE dataset=? AND name=?", group).fetchone()
|
| 208 |
+
if old or (prior and prior[0] != record["split"]):
|
| 209 |
+
self.counts["cross_split_duplicates_excluded" if (old and old[0] != record["split"]) or
|
| 210 |
+
(prior and prior[0] != record["split"]) else "duplicates_excluded"] += 1
|
| 211 |
+
return False
|
| 212 |
+
self.db.execute("INSERT INTO contents VALUES (?,?)", (key, record["split"]))
|
| 213 |
+
self.db.execute("INSERT OR IGNORE INTO groups VALUES (?,?,?)", (*group, record["split"]))
|
| 214 |
+
record["document_content_sha256"] = key
|
| 215 |
+
self.db.execute("INSERT INTO records(dataset,split,payload) VALUES (?,?,?)",
|
| 216 |
+
(record["dataset"], record["split"], canonical(record)))
|
| 217 |
+
self.counts[f"documents/{record['split']}/{record['dataset']}"] += 1
|
| 218 |
+
return True
|
| 219 |
+
|
| 220 |
+
def acquired_sources(self):
|
| 221 |
+
root = Path(self.cfg["acquired"])
|
| 222 |
+
ledger = {}
|
| 223 |
+
for line in (root / "files.jsonl").open(encoding="utf-8"):
|
| 224 |
+
r = json.loads(line)
|
| 225 |
+
ledger[r["path"]] = r
|
| 226 |
+
for ds in ("mbpp", "smoltalk", "fineweb_edu"):
|
| 227 |
+
for path in sorted((root / ds).rglob("*.parquet")):
|
| 228 |
+
expected = pinned_source_sha(path, root, ds, self.catalog["datasets"][ds]["revision"], ledger)
|
| 229 |
+
self.source(path, expected)
|
| 230 |
+
hashes = {r["path"]: r["sha256"] for r in self.source_files}
|
| 231 |
+
# Held-out MBPP precedes train so duplicate solutions cannot contaminate it.
|
| 232 |
+
for split, source_split in (("test", "test"), ("val", "validation"), ("train", "train")):
|
| 233 |
+
p = next((root / "mbpp/full").glob(source_split + "-*.parquet"))
|
| 234 |
+
for row in parquet_rows(p):
|
| 235 |
+
prov = self.provenance("mbpp", str(row["task_id"]), split, p, hashes[str(p)], source_split)
|
| 236 |
+
try:
|
| 237 |
+
self.add(normalize("mbpp", row, prov)[0])
|
| 238 |
+
except SyntaxError:
|
| 239 |
+
self.counts["mbpp_syntax_rejected/" + split] += 1
|
| 240 |
+
self.db.commit()
|
| 241 |
+
for source_split, split, capkey in (("test", "val", "smoltalk_val_per_subset"),
|
| 242 |
+
("train", "train", "smoltalk_train_per_subset")):
|
| 243 |
+
for subset in self.catalog["datasets"]["smoltalk"]["subsets"]:
|
| 244 |
+
accepted = 0
|
| 245 |
+
for p in sorted((root / "smoltalk/data" / subset).glob(source_split + "-*.parquet")):
|
| 246 |
+
for i, row in enumerate(parquet_rows(p)):
|
| 247 |
+
prov = self.provenance("smoltalk", f"{subset}/{p.name}/{i}", split, p, hashes[str(p)], source_split,
|
| 248 |
+
component=subset)
|
| 249 |
+
try:
|
| 250 |
+
accepted += self.add(normalize("smoltalk", row, prov)[0])
|
| 251 |
+
except (ValueError, KeyError, TypeError):
|
| 252 |
+
self.counts["smoltalk_invalid"] += 1
|
| 253 |
+
if accepted >= self.cfg[capkey]:
|
| 254 |
+
break
|
| 255 |
+
if accepted >= self.cfg[capkey]:
|
| 256 |
+
break
|
| 257 |
+
self.db.commit()
|
| 258 |
+
self.progress("source_selection", component=subset, split=split, accepted=accepted)
|
| 259 |
+
# FineWeb source budget is publisher-token based; actual BPE budget is applied later.
|
| 260 |
+
source_tokens = 0
|
| 261 |
+
for p in sorted((root / "fineweb_edu").rglob("*.parquet")):
|
| 262 |
+
for i, row in enumerate(parquet_rows(p)):
|
| 263 |
+
if row.get("language") != "en" or float(row["score"]) < self.cfg["fineweb_min_score"]:
|
| 264 |
+
self.counts["fineweb_quality_rejected"] += 1
|
| 265 |
+
continue
|
| 266 |
+
text = row["text"].strip()
|
| 267 |
+
if not text or len(text) > 500000:
|
| 268 |
+
self.counts["fineweb_size_rejected"] += 1
|
| 269 |
+
continue
|
| 270 |
+
group = document_group(row.get("url", ""), text)
|
| 271 |
+
split = "val" if int(digest(group)[:8], 16) % 100 < 2 else "train"
|
| 272 |
+
prov = self.provenance("fineweb_edu", str(row["id"]), split, p, hashes[str(p)], "sample-10BT",
|
| 273 |
+
original_url=row.get("url"), crawl_dump=row.get("dump"), warc_file=row.get("file_path"),
|
| 274 |
+
educational_score=row["score"])
|
| 275 |
+
record = normalize("fineweb_edu", row, prov)[0]
|
| 276 |
+
record["group"] = group
|
| 277 |
+
if self.add(record):
|
| 278 |
+
source_tokens += int(row["token_count"])
|
| 279 |
+
if i % 2000 == 0:
|
| 280 |
+
self.db.commit()
|
| 281 |
+
self.progress("source_selection", component="fineweb_edu", publisher_tokens=source_tokens)
|
| 282 |
+
if source_tokens >= self.cfg["fineweb_source_tokens"]:
|
| 283 |
+
break
|
| 284 |
+
if source_tokens >= self.cfg["fineweb_source_tokens"]:
|
| 285 |
+
break
|
| 286 |
+
self.db.commit()
|
| 287 |
+
|
| 288 |
+
def legacy_sources(self):
|
| 289 |
+
raw, cache = Path(self.cfg["legacy_raw"]), Path(self.cfg["legacy_cache"])
|
| 290 |
+
stats = json.loads((cache / "stats.json").read_text())
|
| 291 |
+
for ds in ("tinystories", "dolly"):
|
| 292 |
+
p = raw / ds / "provenance.json"
|
| 293 |
+
src = json.loads(p.read_text())
|
| 294 |
+
self.source(p)
|
| 295 |
+
self.catalog["datasets"][ds] = {"role": "core", "repo_id": src["repo_id"], "revision": src["revision"],
|
| 296 |
+
"license": src["license"], "source": src["url"], "tasks": ["text" if ds == "tinystories" else "chat_text"],
|
| 297 |
+
"attribution": src["repo_id"], "notes": src["notes"], "retained_builtin": True}
|
| 298 |
+
if src["license"] not in self.catalog["policy"]["allowed_asset_licenses"]:
|
| 299 |
+
self.catalog["policy"]["allowed_asset_licenses"].append(src["license"])
|
| 300 |
+
for filename in src["files"]:
|
| 301 |
+
self.source(raw / ds / filename, src["sha256"][Path(filename).name])
|
| 302 |
+
for split, source_split in (("val", "validation"), ("train", "train")):
|
| 303 |
+
p = next((raw / "tinystories/data").glob(source_split + "-*.parquet"))
|
| 304 |
+
sha = next(r["sha256"] for r in self.source_files if r["path"] == str(p))
|
| 305 |
+
for i, row in enumerate(parquet_rows(p)):
|
| 306 |
+
if i >= stats["text"][split]["documents"]:
|
| 307 |
+
break
|
| 308 |
+
text = row["text"].strip()
|
| 309 |
+
self.add({**self.provenance("tinystories", f"{source_split}/{i}", split, p, sha, source_split),
|
| 310 |
+
"dataset": "tinystories", "task": "text", "group": normalized_hash(text),
|
| 311 |
+
"inputs": [], "outputs": [{"type": "text", "text": text}]})
|
| 312 |
+
# Preserve the actual historical Dolly split and pairs, not a new random split.
|
| 313 |
+
for split in ("val", "train"):
|
| 314 |
+
p = cache / f"chat_{split}.json"
|
| 315 |
+
sha = self.source(p)
|
| 316 |
+
for i, row in enumerate(json.loads(p.read_text())):
|
| 317 |
+
self.add({**self.provenance("dolly", f"legacy-{split}/{i}", split, p, sha, "legacy-" + split),
|
| 318 |
+
"dataset": "dolly", "task": "chat_text", "group": normalized_hash(row["user"]),
|
| 319 |
+
"inputs": [{"type": "text", "text": row["user"]}],
|
| 320 |
+
"outputs": [{"type": "text", "text": row["assistant"]}]})
|
| 321 |
+
self.db.commit()
|
| 322 |
+
|
| 323 |
+
def fit_tokenizer(self):
|
| 324 |
+
fitted = Counter()
|
| 325 |
+
def corpus():
|
| 326 |
+
for ds, cap in self.cfg["tokenizer_characters"].items():
|
| 327 |
+
for (payload,) in self.db.execute("SELECT payload FROM records WHERE dataset=? AND split='train' ORDER BY id", (ds,)):
|
| 328 |
+
r = json.loads(payload)
|
| 329 |
+
for segment in r["inputs"] + r["outputs"]:
|
| 330 |
+
text = segment["text"][:cap - fitted[ds]]
|
| 331 |
+
if text:
|
| 332 |
+
fitted[ds] += len(text)
|
| 333 |
+
yield text
|
| 334 |
+
if fitted[ds] >= cap:
|
| 335 |
+
break
|
| 336 |
+
self.progress("fit_training_only_bpe")
|
| 337 |
+
tok = TextTokenizer.train(corpus(), self.cfg["text_vocab_size"])
|
| 338 |
+
if tok.vocab_size != self.cfg["text_vocab_size"]:
|
| 339 |
+
raise ValueError("BPE did not reach the exact shared vocabulary size")
|
| 340 |
+
tok.save(self.work / "text_tokenizer.json")
|
| 341 |
+
atomic_json(self.work / "tokenizer_fit.json", {"split": "train", "characters_by_dataset": dict(fitted),
|
| 342 |
+
"sha256": sha256_file(self.work / "text_tokenizer.json")})
|
| 343 |
+
return tok
|
| 344 |
+
|
| 345 |
+
def compile(self, tok: TextTokenizer):
|
| 346 |
+
v, context = UniversalVocab(), self.cfg["context"]
|
| 347 |
+
if tok.vocab_size != v.text_size:
|
| 348 |
+
raise ValueError("text vocabulary mismatch")
|
| 349 |
+
self.out.mkdir()
|
| 350 |
+
(self.out / "INCOMPLETE").write_text("awaiting complete validation")
|
| 351 |
+
handles, offsets, seen = {}, Counter(), {}
|
| 352 |
+
manifest_hash = hashlib.sha256()
|
| 353 |
+
targets, lengths, sources = Counter(), Counter(), Counter()
|
| 354 |
+
fineweb_train_tokens, total = 0, 0
|
| 355 |
+
for split in ("train", "val", "test"):
|
| 356 |
+
handles[split] = ((self.out / f"{split}.bin").open("wb"), (self.out / f"{split}.index.jsonl").open("w", encoding="utf-8"))
|
| 357 |
+
try:
|
| 358 |
+
with (self.out / "provenance.jsonl").open("w", encoding="utf-8") as provenance:
|
| 359 |
+
for row_id, payload in self.db.execute("SELECT id,payload FROM records ORDER BY id"):
|
| 360 |
+
manifest_hash.update((payload + "\n").encode("utf-8"))
|
| 361 |
+
r = json.loads(payload)
|
| 362 |
+
chunks = text_windows(r["outputs"][0]["text"], tok, context - 6) if r["task"] == "text" else [r["outputs"][0]["text"]]
|
| 363 |
+
for chunk_id, chunk in enumerate(chunks):
|
| 364 |
+
candidate = r | {"outputs": [{"type": "text", "text": chunk}]}
|
| 365 |
+
try:
|
| 366 |
+
tokens, target = text_sample(candidate, tok, v, context)
|
| 367 |
+
except ValueError as exc:
|
| 368 |
+
if r["task"] == "text" or "context length" not in str(exc):
|
| 369 |
+
raise
|
| 370 |
+
self.counts[f"over_context_rejected/{r['split']}/{r['dataset']}"] += 1
|
| 371 |
+
continue
|
| 372 |
+
if r["dataset"] == "fineweb_edu" and r["split"] == "train":
|
| 373 |
+
if fineweb_train_tokens + len(tokens) - target > self.cfg["fineweb_train_tokens"]:
|
| 374 |
+
self.counts["fineweb_bpe_budget_excluded"] += 1
|
| 375 |
+
continue
|
| 376 |
+
content_key = normalized_hash("\n".join(x["text"] for x in candidate["inputs"] + candidate["outputs"]))
|
| 377 |
+
if content_key in seen:
|
| 378 |
+
self.counts["window_duplicates_excluded"] += 1
|
| 379 |
+
continue
|
| 380 |
+
seen[content_key] = r["split"]
|
| 381 |
+
split, task = r["split"], r["task"]
|
| 382 |
+
ident = r["id"] + f"/window-{chunk_id}"
|
| 383 |
+
idx = {"offset": offsets[split], "length": len(tokens), "target_start": target,
|
| 384 |
+
"task": task, "id": ident, "dataset": r["dataset"], "group": r["group"], "split": split,
|
| 385 |
+
"document_content_sha256": r["document_content_sha256"], "content_sha256": content_key,
|
| 386 |
+
"tokens_sha256": hashlib.sha256(tokens.tobytes()).hexdigest()}
|
| 387 |
+
tokens.tofile(handles[split][0]); handles[split][1].write(canonical(idx) + "\n")
|
| 388 |
+
prov = {k: val for k, val in r.items() if k not in ("inputs", "outputs")}
|
| 389 |
+
prov.update(id=ident, window=chunk_id, token_offset=offsets[split], token_length=len(tokens),
|
| 390 |
+
tokens_sha256=idx["tokens_sha256"], content_sha256=content_key)
|
| 391 |
+
provenance.write(canonical(prov) + "\n")
|
| 392 |
+
offsets[split] += len(tokens)
|
| 393 |
+
self.counts[f"samples/{split}/{task}"] += 1
|
| 394 |
+
targets[f"{split}/{task}"] += len(tokens) - target
|
| 395 |
+
sources[f"{split}/{r['dataset']}"] += 1
|
| 396 |
+
lengths[str(min(context, max(128, 1 << (len(tokens) - 2).bit_length())))] += 1
|
| 397 |
+
if r["dataset"] == "fineweb_edu" and split == "train":
|
| 398 |
+
fineweb_train_tokens += len(tokens) - target
|
| 399 |
+
total += 1
|
| 400 |
+
if row_id % 1000 == 0:
|
| 401 |
+
self.progress("encode_cache", documents=row_id, samples=total, fineweb_train_targets=fineweb_train_tokens)
|
| 402 |
+
finally:
|
| 403 |
+
for pair in handles.values():
|
| 404 |
+
for f in pair:
|
| 405 |
+
f.close()
|
| 406 |
+
tok.save(self.out / "text_tokenizer.json")
|
| 407 |
+
atomic_json(self.out / "vocab_v2.json", v.to_dict())
|
| 408 |
+
atomic_json(self.out / "catalog.json", self.catalog)
|
| 409 |
+
report = {"training_started": False, "records": total, "context": context, "counts": dict(self.counts),
|
| 410 |
+
"target_tokens_by_task": dict(targets), "samples_by_dataset": dict(sources), "bucket_lengths": dict(lengths),
|
| 411 |
+
"input_tokens_by_split": dict(offsets), "token_bytes": sum(offsets.values()) * 2,
|
| 412 |
+
"sources": self.catalog["datasets"], "source_files": self.source_files,
|
| 413 |
+
"tokenizer_fit": json.loads((self.work / "tokenizer_fit.json").read_text()),
|
| 414 |
+
"user_data_included": False, "media_included": False,
|
| 415 |
+
"limitations": ["exact whitespace-normalized dedup only; near-duplicate/template overlap is unmeasured",
|
| 416 |
+
"over-context chat/coding examples are rejected, never truncated",
|
| 417 |
+
"FineWeb page rights remain governed by underlying sources; crawl URLs retained"],
|
| 418 |
+
"elapsed_preparation_s": time.time() - self.started}
|
| 419 |
+
report["normalized_manifest_sha256"] = manifest_hash.hexdigest()
|
| 420 |
+
atomic_json(self.out / "stats.json", report)
|
| 421 |
+
self.progress("validate_cache", samples=total)
|
| 422 |
+
validation = validate_cache(self.out)
|
| 423 |
+
atomic_json(self.out / "validation.json", validation)
|
| 424 |
+
checksums = {p.name: sha256_file(p) for p in self.out.iterdir() if p.is_file() and p.name != "INCOMPLETE"}
|
| 425 |
+
atomic_json(self.out / "checksums.json", checksums)
|
| 426 |
+
(self.out / "INCOMPLETE").unlink()
|
| 427 |
+
self.progress("complete", samples=total, validation_passed=True)
|
| 428 |
+
return report
|
| 429 |
+
|
| 430 |
+
def run(self):
|
| 431 |
+
try:
|
| 432 |
+
self.progress("verify_sources")
|
| 433 |
+
self.legacy_sources()
|
| 434 |
+
self.acquired_sources()
|
| 435 |
+
return self.compile(self.fit_tokenizer())
|
| 436 |
+
finally:
|
| 437 |
+
self.db.close()
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def validate_cache(cache: Path, require_tasks: bool = True, expected_vocab: UniversalVocab | None = None) -> dict:
|
| 441 |
+
cache = Path(cache)
|
| 442 |
+
v = UniversalVocab(**json.loads((cache / "vocab_v2.json").read_text()))
|
| 443 |
+
if v.to_dict() != (expected_vocab or UniversalVocab()).to_dict():
|
| 444 |
+
raise ValueError("not the exact expected UniversalVocab")
|
| 445 |
+
tok = TextTokenizer.load(cache / "text_tokenizer.json")
|
| 446 |
+
if tok.vocab_size != v.text_size:
|
| 447 |
+
raise ValueError("tokenizer vocabulary mismatch")
|
| 448 |
+
stats = json.loads((cache / "stats.json").read_text())
|
| 449 |
+
groups, contents, documents, identities = {}, {}, {}, {}
|
| 450 |
+
counts, total = Counter(), 0
|
| 451 |
+
for split in ("train", "val", "test"):
|
| 452 |
+
p = cache / f"{split}.bin"
|
| 453 |
+
if p.stat().st_size % 2:
|
| 454 |
+
raise ValueError("misaligned uint16 stream")
|
| 455 |
+
tokens = np.memmap(p, dtype="<u2", mode="r") if p.stat().st_size else np.array([], dtype="<u2")
|
| 456 |
+
offset = 0
|
| 457 |
+
with (cache / f"{split}.index.jsonl").open(encoding="utf-8") as index:
|
| 458 |
+
for line in index:
|
| 459 |
+
r = json.loads(line)
|
| 460 |
+
if r["offset"] != offset or offset + r["length"] > len(tokens) or r["split"] != split:
|
| 461 |
+
raise ValueError("invalid index offset/length/split")
|
| 462 |
+
sample = tokens[offset:offset + r["length"]]
|
| 463 |
+
if r["task"] in ("text", "chat_text", "coding"):
|
| 464 |
+
validate_text_sample(sample, r["target_start"], r["task"], v, stats["context"])
|
| 465 |
+
else:
|
| 466 |
+
if len(sample)-1 > stats["context"] or not 0 < r["target_start"] < len(sample):
|
| 467 |
+
raise ValueError("invalid multimodal context or target boundary")
|
| 468 |
+
if int(sample[r["target_start"]-1]) != v.special("output"):
|
| 469 |
+
raise ValueError("multimodal targets must start after the output marker")
|
| 470 |
+
v.validate(sample.tolist())
|
| 471 |
+
if hashlib.sha256(sample.tobytes()).hexdigest() != r["tokens_sha256"]:
|
| 472 |
+
raise ValueError("sample token checksum mismatch")
|
| 473 |
+
for key, store in ((r["dataset"] + ":" + r["group"], groups),
|
| 474 |
+
(r["content_sha256"], contents), (r["document_content_sha256"], documents)):
|
| 475 |
+
if key in store and store[key] != split:
|
| 476 |
+
raise ValueError("train/heldout content or source-group leakage")
|
| 477 |
+
store[key] = split
|
| 478 |
+
if r["id"] in identities:
|
| 479 |
+
raise ValueError("duplicate sample identity")
|
| 480 |
+
identities[r["id"]] = (split, r["tokens_sha256"], r["dataset"], r["group"])
|
| 481 |
+
offset += r["length"]; counts[split + "/" + r["task"]] += 1; total += 1
|
| 482 |
+
if offset != len(tokens):
|
| 483 |
+
raise ValueError("unindexed trailing tokens")
|
| 484 |
+
catalog = json.loads((cache / "catalog.json").read_text())
|
| 485 |
+
checked = set()
|
| 486 |
+
with (cache / "provenance.jsonl").open(encoding="utf-8") as f:
|
| 487 |
+
for line in f:
|
| 488 |
+
r = json.loads(line)
|
| 489 |
+
key = r["id"]
|
| 490 |
+
if key in checked or identities.get(key) != (r["split"], r["tokens_sha256"], r["dataset"], r["group"]):
|
| 491 |
+
raise ValueError("provenance/index mismatch")
|
| 492 |
+
ds = catalog["datasets"][r["dataset"]]
|
| 493 |
+
if r["revision"] != ds.get("revision", ds.get("release")) or r["license"] != ds["license"]:
|
| 494 |
+
raise ValueError("provenance identity/license mismatch")
|
| 495 |
+
if not all(r.get(k) for k in ("attribution", "source_url", "source_file", "source_sha256")):
|
| 496 |
+
raise ValueError("missing source provenance")
|
| 497 |
+
if r["dataset"] == "mbpp" and {"train":"train","validation":"val","test":"test"}.get(r["source_split"]) != r["split"]:
|
| 498 |
+
raise ValueError("MBPP heldout partition changed")
|
| 499 |
+
checked.add(key)
|
| 500 |
+
if len(checked) != total or total != stats["records"]:
|
| 501 |
+
raise ValueError("provenance/statistics count mismatch")
|
| 502 |
+
if require_tasks:
|
| 503 |
+
for split in ("train", "val"):
|
| 504 |
+
for task in ("text", "chat_text", "coding"):
|
| 505 |
+
if not counts[split + "/" + task]:
|
| 506 |
+
raise ValueError("missing required task coverage")
|
| 507 |
+
checksums = cache / "checksums.json"
|
| 508 |
+
if checksums.exists():
|
| 509 |
+
for name, sha in json.loads(checksums.read_text()).items():
|
| 510 |
+
if Path(name).name != name or sha256_file(cache / name) != sha:
|
| 511 |
+
raise ValueError("cache file checksum mismatch: " + name)
|
| 512 |
+
return {"passed": True, "records": total, "counts": dict(counts), "source_groups": len(groups),
|
| 513 |
+
"content_leakage": 0, "context": stats["context"], "vocab_size": v.size,
|
| 514 |
+
"validated_at": time.time(), "training_started": False}
|
quasar/evaluation/__init__.py
ADDED
|
File without changes
|
quasar/inference/__init__.py
ADDED
|
File without changes
|
quasar/inference/decoding.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Constrained decoding: a per-sequence structure state machine + token sampling.
|
| 2 |
+
|
| 3 |
+
The state machine guarantees every generated media block is well formed: inside <image> only
|
| 4 |
+
visual codes are allowed and exactly G of them; inside a video, frames are
|
| 5 |
+
<frame><t_k> G codes </frame> with consecutive timestamps; text spans can only contain text
|
| 6 |
+
tokens. Purely structural tokens are *forced* (no sampling); content tokens are sampled from
|
| 7 |
+
the model's distribution restricted to the legal set.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from ..tokenizers.multimodal import MAX_FRAMES, UnifiedVocab
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class Sampling:
|
| 20 |
+
temperature: float = 1.0
|
| 21 |
+
top_k: int = 0
|
| 22 |
+
top_p: float = 1.0
|
| 23 |
+
repetition_penalty: float = 1.0 # applied to previously generated *text* tokens only
|
| 24 |
+
max_new_tokens: int = 256
|
| 25 |
+
seed: int = 0
|
| 26 |
+
cfg_scale: float = 1.0 # classifier-free guidance on visual tokens (1.0 = off)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Masks:
|
| 30 |
+
"""Boolean legal-token sets, precomputed once per vocabulary/device."""
|
| 31 |
+
|
| 32 |
+
def __init__(self, v: UnifiedVocab, device):
|
| 33 |
+
V = v.size
|
| 34 |
+
|
| 35 |
+
def mk(ids=(), ranges=()):
|
| 36 |
+
m = torch.zeros(V, dtype=torch.bool, device=device)
|
| 37 |
+
for lo, hi in ranges:
|
| 38 |
+
m[lo:hi] = True
|
| 39 |
+
if ids:
|
| 40 |
+
m[list(ids)] = True
|
| 41 |
+
return m
|
| 42 |
+
|
| 43 |
+
self.text = mk([v.eos], [(0, v.text_size)])
|
| 44 |
+
self.text_media = mk([v.eos, v.img_start, v.vid_start], [(0, v.text_size)])
|
| 45 |
+
self.visual = mk(ranges=[(v.visual_offset, v.visual_offset + v.visual_size)])
|
| 46 |
+
self.timestamp = mk(ranges=[(v.timestamp(0), v.timestamp(0) + MAX_FRAMES)])
|
| 47 |
+
self.frame_or_end = mk([v.frm_start, v.vid_end])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class StructureFSM:
|
| 51 |
+
"""Tracks where one sequence is in the media grammar.
|
| 52 |
+
|
| 53 |
+
mode:
|
| 54 |
+
"text" free text until <eos> (media tags disallowed unless allow_media)
|
| 55 |
+
"image" we are right after <image>: G codes, </image>, then <eos> if stop_after_media
|
| 56 |
+
"video" we are inside <video> right before the first <frame>
|
| 57 |
+
n_frames: exact total frame count for videos (None = model decides, bounded by max_timestamp)
|
| 58 |
+
first_ts: timestamp of the next frame to generate (None = sampled by the model)
|
| 59 |
+
frames_done: frames already present in the prefix (prefix then ends right after a </frame>)
|
| 60 |
+
codes_done: image codes already present after <image> (image completion)
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self, v: UnifiedVocab, mode: str = "text", allow_media: bool = False, stop_after_media: bool = True,
|
| 64 |
+
n_frames: int | None = None, first_ts: int | None = None, max_timestamp: int = 7, frames_done: int = 0, codes_done: int = 0):
|
| 65 |
+
self.v, self.allow_media, self.stop_after_media = v, allow_media, stop_after_media
|
| 66 |
+
self.n_frames, self.next_ts, self.max_ts = n_frames, first_ts, max_timestamp
|
| 67 |
+
self.done = False
|
| 68 |
+
self.frames = frames_done
|
| 69 |
+
self.state = {"text": "text", "image": "img_codes", "video": "after_frame" if frames_done else "frame_start"}[mode]
|
| 70 |
+
self.count = codes_done if mode == "image" else 0
|
| 71 |
+
|
| 72 |
+
def constraint(self, masks: Masks) -> tuple[int | None, torch.Tensor | None]:
|
| 73 |
+
"""(forced_token, None) or (None, legal_mask)."""
|
| 74 |
+
v, s = self.v, self.state
|
| 75 |
+
if self.done:
|
| 76 |
+
return v.pad, None
|
| 77 |
+
if s == "text":
|
| 78 |
+
return None, masks.text_media if self.allow_media else masks.text
|
| 79 |
+
if s in ("img_codes", "frm_codes"):
|
| 80 |
+
return None, masks.visual
|
| 81 |
+
if s == "img_end":
|
| 82 |
+
return v.img_end, None
|
| 83 |
+
if s == "frm_end":
|
| 84 |
+
return v.frm_end, None
|
| 85 |
+
if s == "frame_start":
|
| 86 |
+
return v.frm_start, None
|
| 87 |
+
if s == "timestamp":
|
| 88 |
+
return (v.timestamp(self.next_ts), None) if self.next_ts is not None else (None, masks.timestamp)
|
| 89 |
+
if s == "after_frame":
|
| 90 |
+
last = self.next_ts - 1
|
| 91 |
+
if (self.n_frames is not None and self.frames >= self.n_frames) or last >= self.max_ts:
|
| 92 |
+
return v.vid_end, None
|
| 93 |
+
return (v.frm_start, None) if self.n_frames is not None else (None, masks.frame_or_end)
|
| 94 |
+
if s == "eos":
|
| 95 |
+
return v.eos, None
|
| 96 |
+
raise AssertionError(s)
|
| 97 |
+
|
| 98 |
+
def advance(self, tok: int) -> None:
|
| 99 |
+
v, s = self.v, self.state
|
| 100 |
+
if self.done:
|
| 101 |
+
return
|
| 102 |
+
G = v.tokens_per_frame
|
| 103 |
+
if s == "text":
|
| 104 |
+
if tok == v.eos:
|
| 105 |
+
self.done = True
|
| 106 |
+
elif tok == v.img_start:
|
| 107 |
+
self.state, self.count = "img_codes", 0
|
| 108 |
+
elif tok == v.vid_start:
|
| 109 |
+
self.state = "frame_start"
|
| 110 |
+
elif s == "img_codes":
|
| 111 |
+
self.count += 1
|
| 112 |
+
if self.count == G:
|
| 113 |
+
self.state = "img_end"
|
| 114 |
+
elif s == "img_end":
|
| 115 |
+
self.state = "eos" if self.stop_after_media else "text"
|
| 116 |
+
elif s == "frame_start":
|
| 117 |
+
self.state = "timestamp"
|
| 118 |
+
elif s == "timestamp":
|
| 119 |
+
self.next_ts = tok - v.timestamp(0) + 1
|
| 120 |
+
self.state, self.count = "frm_codes", 0
|
| 121 |
+
elif s == "frm_codes":
|
| 122 |
+
self.count += 1
|
| 123 |
+
if self.count == G:
|
| 124 |
+
self.state = "frm_end"
|
| 125 |
+
elif s == "frm_end":
|
| 126 |
+
self.frames += 1
|
| 127 |
+
self.state = "after_frame"
|
| 128 |
+
elif s == "after_frame":
|
| 129 |
+
if tok == v.frm_start:
|
| 130 |
+
self.state = "timestamp"
|
| 131 |
+
else: # </video>
|
| 132 |
+
self.state = "eos" if self.stop_after_media else "text"
|
| 133 |
+
elif s == "eos":
|
| 134 |
+
self.done = True
|
| 135 |
+
|
| 136 |
+
@property
|
| 137 |
+
def in_visual(self) -> bool:
|
| 138 |
+
return self.state in ("img_codes", "frm_codes")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def sample_next(logits: torch.Tensor, legal: torch.Tensor, sp: Sampling, gen: torch.Generator,
|
| 142 |
+
history: torch.Tensor | None = None, text_size: int = 0) -> torch.Tensor:
|
| 143 |
+
"""logits [B,V] (float32), legal [B,V] bool -> sampled ids [B]."""
|
| 144 |
+
logits = logits.float().clone()
|
| 145 |
+
if sp.repetition_penalty != 1.0 and history is not None and history.numel():
|
| 146 |
+
prev = history.clamp(min=0)
|
| 147 |
+
is_text = prev < text_size
|
| 148 |
+
vals = logits.gather(1, prev)
|
| 149 |
+
pen = torch.where(vals > 0, vals / sp.repetition_penalty, vals * sp.repetition_penalty)
|
| 150 |
+
logits.scatter_(1, prev, torch.where(is_text, pen, vals))
|
| 151 |
+
logits = logits.masked_fill(~legal, float("-inf"))
|
| 152 |
+
if sp.temperature <= 0:
|
| 153 |
+
return logits.argmax(-1)
|
| 154 |
+
logits = logits / sp.temperature
|
| 155 |
+
if sp.top_k > 0:
|
| 156 |
+
kth = logits.topk(min(sp.top_k, logits.size(-1)), dim=-1).values[:, -1:]
|
| 157 |
+
logits = logits.masked_fill(logits < kth, float("-inf"))
|
| 158 |
+
if sp.top_p < 1.0:
|
| 159 |
+
srt, idx = logits.sort(dim=-1, descending=True)
|
| 160 |
+
probs = srt.softmax(-1)
|
| 161 |
+
drop = probs.cumsum(-1) - probs > sp.top_p # keep the smallest prefix with mass >= top_p
|
| 162 |
+
logits = logits.masked_fill(drop.scatter(1, idx, drop), float("-inf"))
|
| 163 |
+
return torch.multinomial(logits.softmax(-1), 1, generator=gen).squeeze(1)
|
quasar/inference/extended.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio, geometry and schema-checked calls from the same shared generator.
|
| 2 |
+
|
| 3 |
+
Recorded training exposure is required. Structural masks constrain packets;
|
| 4 |
+
semantic usefulness must be established separately on trained checkpoints.
|
| 5 |
+
"""
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import json
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from .universal import UniversalGenerator
|
| 11 |
+
from .decoding import Sampling, sample_next
|
| 12 |
+
from ..data.curated import validate_calls
|
| 13 |
+
from ..data.text_preparation import sha256_file
|
| 14 |
+
from ..tokenizers.universal import TokenGrammar
|
| 15 |
+
from ..tokenizers.audio_aligned import AlignedAudioTokenizer
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ExtendedGenerator(UniversalGenerator):
|
| 19 |
+
def require_task(self, task):
|
| 20 |
+
if not self.meta.get('task_target_tokens', {}).get(task):
|
| 21 |
+
raise RuntimeError('This checkpoint has no recorded training exposure for ' + task)
|
| 22 |
+
|
| 23 |
+
def audio(self):
|
| 24 |
+
if not hasattr(self, '_audio'):
|
| 25 |
+
identity = self.meta.get('data', {}).get('preparation', {}).get('audio_codec')
|
| 26 |
+
if not identity or identity.get('implementation') != 'quasar.tokenizers.audio_aligned.AlignedAudioTokenizer':
|
| 27 |
+
raise RuntimeError('Checkpoint has no compatible frozen audio codec identity')
|
| 28 |
+
path = Path(identity['path'])
|
| 29 |
+
if not path.is_absolute():
|
| 30 |
+
path = (self.path / path).resolve()
|
| 31 |
+
for name in ('config.json', 'model.safetensors'):
|
| 32 |
+
if sha256_file(path / name) != identity['sha256'][name]:
|
| 33 |
+
raise ValueError('Audio codec differs from training')
|
| 34 |
+
self._audio = AlignedAudioTokenizer(path, identity['alignment_samples'], identity['codebooks'], device=self.device)
|
| 35 |
+
return self._audio
|
| 36 |
+
|
| 37 |
+
def _candidates(self, grammar, kind, remaining):
|
| 38 |
+
v, s, state = self.vocab, self.vocab.special, grammar.state
|
| 39 |
+
if state == 'audio':
|
| 40 |
+
book = grammar.count % v.audio_books
|
| 41 |
+
lo = v.ranges['audio'][0] + book * v.audio_size
|
| 42 |
+
# Keep two boundary guards within the two-second source budget.
|
| 43 |
+
if grammar.count >= 152 * v.audio_books or (book == 0 and grammar.count >= 64 and remaining <= 5):
|
| 44 |
+
return [s('/audio')]
|
| 45 |
+
tokens = list(range(lo, lo + v.audio_size))
|
| 46 |
+
if grammar.count >= 64 and book == 0:
|
| 47 |
+
tokens.append(s('/audio'))
|
| 48 |
+
return tokens
|
| 49 |
+
if state == 'mesh':
|
| 50 |
+
return [s('vertices')]
|
| 51 |
+
if state == 'vertices':
|
| 52 |
+
if grammar.count >= 9 and grammar.count % 3 == 0 and remaining <= 8:
|
| 53 |
+
return [s('faces')]
|
| 54 |
+
return list(range(*v.ranges['coordinate'])) + [s('faces')]
|
| 55 |
+
if state == 'faces':
|
| 56 |
+
if grammar.count >= 3 and grammar.count % 3 == 0 and remaining <= 5:
|
| 57 |
+
return [s('/mesh')]
|
| 58 |
+
lo = v.ranges['index'][0]
|
| 59 |
+
return list(range(lo, lo + grammar.vertices)) + [s('/mesh')]
|
| 60 |
+
if state == 'calls':
|
| 61 |
+
return [s('/calls')] if remaining <= 2 else list(range(v.text_size)) + [s('/calls')]
|
| 62 |
+
if state == 'packets':
|
| 63 |
+
return [s('eos')]
|
| 64 |
+
raise ValueError('Unsupported generated packet state: ' + state)
|
| 65 |
+
|
| 66 |
+
@torch.inference_mode()
|
| 67 |
+
def packet(self, task, inputs, kind, sp):
|
| 68 |
+
self.require_task(task)
|
| 69 |
+
v, s = self.vocab, self.vocab.special
|
| 70 |
+
prefix = [s('bos'), s('task:' + task), s('input')]
|
| 71 |
+
for segment in inputs:
|
| 72 |
+
prefix += v.packet(segment, self.tok.encode)
|
| 73 |
+
prefix += [s('output'), s(kind)]
|
| 74 |
+
budget = min(sp.max_new_tokens, self.model.cfg.max_seq_len - len(prefix))
|
| 75 |
+
minimum = {'audio': 66, 'mesh': 16, 'calls': 3}[kind]
|
| 76 |
+
if budget < minimum:
|
| 77 |
+
raise ValueError('Complete packet and conditioning exceed the requested context/output budget')
|
| 78 |
+
grammar = TokenGrammar(v)
|
| 79 |
+
for token in prefix:
|
| 80 |
+
grammar.advance(token)
|
| 81 |
+
device = next(self.model.parameters()).device
|
| 82 |
+
amp = device.type == 'cuda'
|
| 83 |
+
cache = self.model.new_cache(1, dtype=torch.bfloat16 if amp else torch.float32)
|
| 84 |
+
rng = torch.Generator(device=device).manual_seed(sp.seed)
|
| 85 |
+
emitted = []
|
| 86 |
+
with torch.autocast(device.type, dtype=torch.bfloat16, enabled=amp):
|
| 87 |
+
logits = self.model(torch.tensor([prefix], device=device), cache=cache)[:, -1]
|
| 88 |
+
while grammar.state != 'done' and len(emitted) < budget:
|
| 89 |
+
candidates = [t for t in self._candidates(grammar, kind, budget - len(emitted)) if grammar.accepts(t)]
|
| 90 |
+
if not candidates:
|
| 91 |
+
raise RuntimeError('No structurally valid completion fits the packet budget')
|
| 92 |
+
if len(candidates) == 1:
|
| 93 |
+
token = candidates[0]
|
| 94 |
+
else:
|
| 95 |
+
legal = torch.zeros((1, v.size), dtype=torch.bool, device=device)
|
| 96 |
+
legal[:, candidates] = True
|
| 97 |
+
history = torch.tensor([emitted], dtype=torch.long, device=device)
|
| 98 |
+
token = int(sample_next(logits, legal, sp, rng, history, v.text_size)[0])
|
| 99 |
+
grammar.advance(token); emitted.append(token)
|
| 100 |
+
if grammar.state != 'done':
|
| 101 |
+
logits = self.model(torch.tensor([[token]], device=device), cache=cache, start=cache.next_pos)[:, -1]
|
| 102 |
+
if grammar.state != 'done':
|
| 103 |
+
raise RuntimeError('Model did not finish a valid packet within the output budget')
|
| 104 |
+
v.validate(prefix + emitted)
|
| 105 |
+
return emitted
|
| 106 |
+
|
| 107 |
+
def generate_audio(self, prompt, sp=None):
|
| 108 |
+
tokens = self.packet('t2a', [{'type': 'text', 'text': prompt}], 'audio',
|
| 109 |
+
sp or Sampling(temperature=.8, top_k=64, top_p=.9, max_new_tokens=642))
|
| 110 |
+
raw = np.asarray(tokens[:-2], dtype=np.int64).reshape(-1, self.vocab.audio_books)
|
| 111 |
+
offsets = self.vocab.ranges['audio'][0] + np.arange(self.vocab.audio_books) * self.vocab.audio_size
|
| 112 |
+
packet = {'type': 'audio', 'codes': (raw - offsets).tolist()}
|
| 113 |
+
codec = self.audio()
|
| 114 |
+
return codec.decode(packet), codec.sr
|
| 115 |
+
|
| 116 |
+
def describe_audio(self, waveform, sample_rate, sp=None):
|
| 117 |
+
self.require_task('a2t')
|
| 118 |
+
packet = self.audio().encode(waveform, sample_rate)
|
| 119 |
+
return self._predict('a2t', [packet], 'text', sp or Sampling(temperature=.5, max_new_tokens=128))[0]
|
| 120 |
+
|
| 121 |
+
def generate_mesh(self, prompt, sp=None):
|
| 122 |
+
t = self.packet('t2m', [{'type': 'text', 'text': prompt}], 'mesh',
|
| 123 |
+
sp or Sampling(temperature=.8, top_k=64, top_p=.9, max_new_tokens=780))
|
| 124 |
+
s = self.vocab.special
|
| 125 |
+
vertices = np.asarray(t[t.index(s('vertices')) + 1:t.index(s('faces'))]).reshape(-1, 3)
|
| 126 |
+
faces = np.asarray(t[t.index(s('faces')) + 1:t.index(s('/mesh'))]).reshape(-1, 3)
|
| 127 |
+
vertices -= self.vocab.ranges['coordinate'][0]; faces -= self.vocab.ranges['index'][0]
|
| 128 |
+
if len(np.unique(vertices, axis=0)) != len(vertices) or np.any(np.linalg.norm(
|
| 129 |
+
np.cross(vertices[faces[:, 1]] - vertices[faces[:, 0]], vertices[faces[:, 2]] - vertices[faces[:, 0]]), axis=1) == 0):
|
| 130 |
+
raise RuntimeError('Generated mesh has collapsed or degenerate geometry; semantic generation is unverified')
|
| 131 |
+
return {'type': 'mesh', 'vertices': vertices.tolist(), 'faces': faces.tolist()}
|
| 132 |
+
|
| 133 |
+
def describe_mesh(self, packet, sp=None):
|
| 134 |
+
return self._predict('m2t', [packet], 'text', sp or Sampling(temperature=.5, max_new_tokens=128))[0]
|
| 135 |
+
|
| 136 |
+
def propose_calls(self, prompt, tools, sp=None):
|
| 137 |
+
self.require_task('tools')
|
| 138 |
+
validate_calls(tools, [])
|
| 139 |
+
tokens = self.packet('tools', [{'type': 'tools', 'value': tools}, {'type': 'text', 'text': 'user: ' + prompt}],
|
| 140 |
+
'calls', sp or Sampling(temperature=.5, max_new_tokens=256))
|
| 141 |
+
text = self.tok.decode([t for t in tokens if t < self.vocab.text_size])
|
| 142 |
+
def reject_constant(value):
|
| 143 |
+
raise ValueError('Non-finite JSON constant: ' + value)
|
| 144 |
+
result = json.loads(text, parse_constant=reject_constant)
|
| 145 |
+
if not isinstance(result, list):
|
| 146 |
+
raise ValueError('Generated calls must be a JSON list')
|
| 147 |
+
validate_calls(tools, result)
|
| 148 |
+
return result
|
quasar/inference/generate.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unified generation engine: every capability is one call into the same trained backbone.
|
| 2 |
+
|
| 3 |
+
eng = QuasarEngine("checkpoints/quasar-exp/instruct/final")
|
| 4 |
+
eng.generate_text("Once upon a time")
|
| 5 |
+
eng.generate_image("a large red circle in the center on a black background")
|
| 6 |
+
eng.generate_video("a small blue circle moving right on a navy background")
|
| 7 |
+
eng.describe_image(img); eng.describe_video(frames); eng.continue_video(frames)
|
| 8 |
+
eng.edit_image(img, "change the color to green"); eng.image_to_video(img, "... moving up ...")
|
| 9 |
+
eng.chat("Make a video of a blue ball bouncing.")
|
| 10 |
+
|
| 11 |
+
The visual tokenizer only converts pixels <-> tokens; all content tokens are sampled from the
|
| 12 |
+
Quasar transformer. Capabilities the checkpoint was never trained on raise CapabilityError
|
| 13 |
+
unless force=True, so untrained noise is never presented as a result.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
from PIL import Image
|
| 22 |
+
|
| 23 |
+
from ..data.preprocessing import load_video, prepare_image
|
| 24 |
+
from ..tokenizers.text import TextTokenizer
|
| 25 |
+
from ..tokenizers.vision import VisualTokenizer
|
| 26 |
+
from ..training.checkpoint import load_quasar
|
| 27 |
+
from .decoding import Masks, Sampling, StructureFSM, sample_next
|
| 28 |
+
|
| 29 |
+
CAPABILITY_TASKS = {
|
| 30 |
+
"generate_text": ("text",), "chat": ("chat_text", "chat_mm"), "generate_image": ("t2i",), "complete_image": ("t2i",),
|
| 31 |
+
"describe_image": ("i2t",), "edit_image": ("i2i",), "generate_video": ("t2v",), "image_to_video": ("t2v",),
|
| 32 |
+
"continue_video": ("vcont",), "describe_video": ("v2t",),
|
| 33 |
+
}
|
| 34 |
+
TRAINED_FRAMES = 8 # clips in training have 8 frames (t0..t7)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class CapabilityError(RuntimeError):
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ------------------------------------------------------------------------------- media io
|
| 42 |
+
def save_video(frames: np.ndarray, path: str | Path, fps: int = 4, scale: int = 4) -> Path:
|
| 43 |
+
"""Write uint8 [T,H,W,3] as .mp4 (H.264, browser-playable) or .gif, upscaled `scale`x (nearest)."""
|
| 44 |
+
path = Path(path)
|
| 45 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 46 |
+
big = np.asarray(frames).repeat(scale, 1).repeat(scale, 2)
|
| 47 |
+
if path.suffix.lower() == ".gif":
|
| 48 |
+
ims = [Image.fromarray(f) for f in big]
|
| 49 |
+
ims[0].save(path, save_all=True, append_images=ims[1:], duration=int(1000 / fps), loop=0)
|
| 50 |
+
return path
|
| 51 |
+
import imageio_ffmpeg
|
| 52 |
+
|
| 53 |
+
T, H, W, _ = big.shape
|
| 54 |
+
w = imageio_ffmpeg.write_frames(str(path), (W, H), fps=fps, codec="libx264", pix_fmt_out="yuv420p", macro_block_size=1,
|
| 55 |
+
output_params=["-crf", "18"])
|
| 56 |
+
w.send(None)
|
| 57 |
+
for f in big:
|
| 58 |
+
w.send(np.ascontiguousarray(f))
|
| 59 |
+
w.close()
|
| 60 |
+
return path
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def normalize_prompt(p: str) -> str:
|
| 64 |
+
"""Training captions are lower-case without a trailing period."""
|
| 65 |
+
return p.strip().rstrip(".").lower()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ------------------------------------------------------------------------------- engine
|
| 69 |
+
class QuasarEngine:
|
| 70 |
+
def __init__(self, ckpt: str | Path, device: str | None = None):
|
| 71 |
+
self.ckpt = Path(ckpt)
|
| 72 |
+
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
|
| 73 |
+
self.model, self.vocab, self.meta = load_quasar(self.ckpt, self.device)
|
| 74 |
+
if getattr(self.vocab, "schema_version", 1) in (2,3):
|
| 75 |
+
raise CapabilityError("This is a v2 preparation checkpoint. V2 generation/codec reconstruction must be validated and trained before exposing it in the legacy demo.")
|
| 76 |
+
self.model.eval()
|
| 77 |
+
self.tok = TextTokenizer.load(self.ckpt / "tokenizers" / "text_tokenizer.json")
|
| 78 |
+
self.vq = VisualTokenizer.load(self.ckpt / "tokenizers" / "vq", self.device)
|
| 79 |
+
self.masks = Masks(self.vocab, self.device)
|
| 80 |
+
self.amp = torch.bfloat16 if self.device.type == "cuda" and torch.cuda.is_bf16_supported() else None
|
| 81 |
+
|
| 82 |
+
# -- capability bookkeeping ------------------------------------------------------------
|
| 83 |
+
def capabilities(self) -> dict[str, dict]:
|
| 84 |
+
tt: dict[str, int] = {}
|
| 85 |
+
for k, n in self.meta.get("task_target_tokens", {}).items(): # 'user/t2i' counts toward 't2i'
|
| 86 |
+
base = k.rpartition("/")[2]
|
| 87 |
+
tt[base] = tt.get(base, 0) + int(n)
|
| 88 |
+
return {m: {"trained": all(tt.get(t, 0) > 0 for t in ts), "target_tokens": {t: tt.get(t, 0) for t in ts}}
|
| 89 |
+
for m, ts in CAPABILITY_TASKS.items()}
|
| 90 |
+
|
| 91 |
+
def _require(self, method: str, force: bool) -> None:
|
| 92 |
+
if force:
|
| 93 |
+
return
|
| 94 |
+
cap = self.capabilities()[method]
|
| 95 |
+
if not cap["trained"]:
|
| 96 |
+
raise CapabilityError(
|
| 97 |
+
f"'{method}' has not been trained in this checkpoint (stage '{self.meta.get('stage')}', "
|
| 98 |
+
f"target tokens seen: {cap['target_tokens']}). Train a stage that includes these tasks, "
|
| 99 |
+
f"or pass force=True to sample the untrained model anyway.")
|
| 100 |
+
|
| 101 |
+
# -- token helpers ---------------------------------------------------------------------
|
| 102 |
+
def enc(self, s: str) -> list[int]:
|
| 103 |
+
return self.tok.encode(s)
|
| 104 |
+
|
| 105 |
+
def image_codes(self, image) -> np.ndarray:
|
| 106 |
+
return self.vq.encode(prepare_image(image)[None])[0]
|
| 107 |
+
|
| 108 |
+
def video_codes(self, frames) -> np.ndarray:
|
| 109 |
+
return self.vq.encode(load_video(frames)[None])[0]
|
| 110 |
+
|
| 111 |
+
# -- core decode loop ------------------------------------------------------------------
|
| 112 |
+
@torch.no_grad()
|
| 113 |
+
def _decode(self, prefix: list[int], fsm_kwargs: dict, n: int, sp: Sampling, uncond_prefix: list[int] | None = None) -> list[list[int]]:
|
| 114 |
+
m, dev = self.model, self.device
|
| 115 |
+
if len(prefix) >= m.cfg.max_seq_len:
|
| 116 |
+
raise ValueError(f"prompt is {len(prefix)} tokens; the model context is {m.cfg.max_seq_len}")
|
| 117 |
+
max_len = min(m.cfg.max_seq_len, len(prefix) + sp.max_new_tokens)
|
| 118 |
+
guided = uncond_prefix is not None and sp.cfg_scale != 1.0
|
| 119 |
+
amp = torch.autocast(dev.type, dtype=self.amp) if self.amp else torch.autocast(dev.type, enabled=False)
|
| 120 |
+
|
| 121 |
+
def start(pref):
|
| 122 |
+
cache = m.new_cache(n, max_len)
|
| 123 |
+
hist = torch.tensor(pref, device=dev).repeat(n, 1)
|
| 124 |
+
with amp:
|
| 125 |
+
return cache, hist, m(hist, cache=cache)[:, -1].float()
|
| 126 |
+
|
| 127 |
+
cache, hist, logits = start(prefix)
|
| 128 |
+
if guided:
|
| 129 |
+
ucache, uhist, ulogits = start(uncond_prefix)
|
| 130 |
+
fsms = [StructureFSM(self.vocab, **fsm_kwargs) for _ in range(n)]
|
| 131 |
+
gen = torch.Generator(device=dev).manual_seed(sp.seed)
|
| 132 |
+
out = torch.empty(n, 0, dtype=torch.long, device=dev)
|
| 133 |
+
pos, upos = len(prefix), len(uncond_prefix or [])
|
| 134 |
+
while pos < max_len and not all(f.done for f in fsms):
|
| 135 |
+
cons = [f.constraint(self.masks) for f in fsms]
|
| 136 |
+
forced = torch.tensor([c[0] if c[0] is not None else -1 for c in cons], device=dev)
|
| 137 |
+
if (forced >= 0).all():
|
| 138 |
+
nxt = forced
|
| 139 |
+
else:
|
| 140 |
+
legal = torch.stack([c[1] if c[1] is not None else self.masks.text for c in cons])
|
| 141 |
+
lg = logits
|
| 142 |
+
if guided:
|
| 143 |
+
vis = torch.tensor([f.in_visual for f in fsms], device=dev)[:, None]
|
| 144 |
+
lg = torch.where(vis, ulogits + sp.cfg_scale * (logits - ulogits), logits)
|
| 145 |
+
nxt = sample_next(lg, legal, sp, gen, out, self.vocab.text_size)
|
| 146 |
+
nxt = torch.where(forced >= 0, forced, nxt)
|
| 147 |
+
for f, t in zip(fsms, nxt.tolist()):
|
| 148 |
+
f.advance(t)
|
| 149 |
+
out = torch.cat([out, nxt[:, None]], 1)
|
| 150 |
+
hist = torch.cat([hist, nxt[:, None]], 1)
|
| 151 |
+
if all(f.done for f in fsms) or pos + 1 >= max_len:
|
| 152 |
+
break
|
| 153 |
+
with amp:
|
| 154 |
+
logits = m(nxt[:, None], vis_pos=m.visual_positions(hist)[:, -1:], cache=cache, start=pos)[:, -1].float()
|
| 155 |
+
if guided:
|
| 156 |
+
uhist = torch.cat([uhist, nxt[:, None]], 1)
|
| 157 |
+
ulogits = m(nxt[:, None], vis_pos=m.visual_positions(uhist)[:, -1:], cache=ucache, start=upos)[:, -1].float()
|
| 158 |
+
pos += 1
|
| 159 |
+
upos += 1
|
| 160 |
+
res = []
|
| 161 |
+
for row in out.tolist():
|
| 162 |
+
res.append(row[: row.index(self.vocab.eos)] if self.vocab.eos in row else row)
|
| 163 |
+
return res
|
| 164 |
+
|
| 165 |
+
def _text(self, ids: list[int]) -> str:
|
| 166 |
+
return self.tok.decode([i for i in ids if i < self.vocab.text_size]).strip()
|
| 167 |
+
|
| 168 |
+
def _visual(self, ids: list[int], kind: str) -> np.ndarray:
|
| 169 |
+
segs = [s for k, s in self.vocab.parse(ids) if k == kind]
|
| 170 |
+
if not segs:
|
| 171 |
+
raise RuntimeError(f"generation produced no {kind} block")
|
| 172 |
+
return self.vq.decode(segs[0][None])[0]
|
| 173 |
+
|
| 174 |
+
# -- public API ------------------------------------------------------------------------
|
| 175 |
+
def generate_text(self, prompt: str, sp: Sampling | None = None, force: bool = False) -> str:
|
| 176 |
+
"""Continue `prompt` with the base language-model head (TinyStories-style text)."""
|
| 177 |
+
self._require("generate_text", force)
|
| 178 |
+
sp = sp or Sampling(temperature=0.8, top_k=50, top_p=0.95, repetition_penalty=1.1, max_new_tokens=200)
|
| 179 |
+
return self._text(self._decode([self.vocab.bos, self.vocab.task("text"), *self.enc(prompt)], {"mode": "text"}, 1, sp)[0])
|
| 180 |
+
|
| 181 |
+
def generate_image(self, prompt: str, n: int = 1, sp: Sampling | None = None, force: bool = False) -> list[np.ndarray]:
|
| 182 |
+
self._require("generate_image", force)
|
| 183 |
+
sp = sp or Sampling(temperature=1.0, top_k=100, cfg_scale=3.0)
|
| 184 |
+
v = self.vocab
|
| 185 |
+
prefix = [v.bos, v.task("t2i"), *self.enc(normalize_prompt(prompt)), v.img_start]
|
| 186 |
+
rows = self._decode(prefix, {"mode": "image"}, n, sp, uncond_prefix=[v.bos, v.task("t2i"), v.img_start])
|
| 187 |
+
return [self.vq.decode(np.array(r[: v.tokens_per_frame]).reshape(1, *v.grid) - v.visual_offset)[0] for r in rows]
|
| 188 |
+
|
| 189 |
+
def complete_image(self, image, prompt: str = "", keep_rows: int = 4, n: int = 1, sp: Sampling | None = None, force: bool = False) -> list[np.ndarray]:
|
| 190 |
+
"""Keep the top `keep_rows` token rows of `image` and let the model fill in the rest."""
|
| 191 |
+
self._require("complete_image", force)
|
| 192 |
+
sp = sp or Sampling(temperature=1.0, top_k=100)
|
| 193 |
+
v = self.vocab
|
| 194 |
+
keep = (self.image_codes(image).reshape(-1)[: keep_rows * v.grid[1]] + v.visual_offset).tolist()
|
| 195 |
+
prefix = [v.bos, v.task("t2i"), *self.enc(normalize_prompt(prompt)), v.img_start, *keep]
|
| 196 |
+
rows = self._decode(prefix, {"mode": "image", "codes_done": len(keep)}, n, sp)
|
| 197 |
+
return [self.vq.decode(np.array(keep + r[: v.tokens_per_frame - len(keep)]).reshape(1, *v.grid) - v.visual_offset)[0] for r in rows]
|
| 198 |
+
|
| 199 |
+
def describe_image(self, image, sp: Sampling | None = None, force: bool = False) -> str:
|
| 200 |
+
self._require("describe_image", force)
|
| 201 |
+
sp = sp or Sampling(temperature=0.0, max_new_tokens=48)
|
| 202 |
+
v = self.vocab
|
| 203 |
+
return self._text(self._decode([v.bos, v.task("i2t"), *v.image_tokens(self.image_codes(image))], {"mode": "text"}, 1, sp)[0])
|
| 204 |
+
|
| 205 |
+
def edit_image(self, image, instruction: str, n: int = 1, sp: Sampling | None = None, force: bool = False) -> list[np.ndarray]:
|
| 206 |
+
self._require("edit_image", force)
|
| 207 |
+
sp = sp or Sampling(temperature=1.0, top_k=100)
|
| 208 |
+
v = self.vocab
|
| 209 |
+
prefix = [v.bos, v.task("i2i"), *self.enc(normalize_prompt(instruction)), *v.image_tokens(self.image_codes(image)), v.img_start]
|
| 210 |
+
rows = self._decode(prefix, {"mode": "image"}, n, sp)
|
| 211 |
+
return [self.vq.decode(np.array(r[: v.tokens_per_frame]).reshape(1, *v.grid) - v.visual_offset)[0] for r in rows]
|
| 212 |
+
|
| 213 |
+
def _check_frames(self, n_frames: int, force: bool) -> None:
|
| 214 |
+
if n_frames > TRAINED_FRAMES and not force:
|
| 215 |
+
raise CapabilityError(f"clips were trained with {TRAINED_FRAMES} frames; use continue_video to extend beyond that")
|
| 216 |
+
|
| 217 |
+
def generate_video(self, prompt: str, n_frames: int = 8, n: int = 1, sp: Sampling | None = None, force: bool = False) -> list[np.ndarray]:
|
| 218 |
+
self._require("generate_video", force)
|
| 219 |
+
self._check_frames(n_frames, force)
|
| 220 |
+
sp = sp or Sampling(temperature=1.0, top_k=100, cfg_scale=3.0, max_new_tokens=n_frames * (self.vocab.tokens_per_frame + 3) + 4)
|
| 221 |
+
v = self.vocab
|
| 222 |
+
prefix = [v.bos, v.task("t2v"), *self.enc(normalize_prompt(prompt)), v.vid_start]
|
| 223 |
+
rows = self._decode(prefix, {"mode": "video", "n_frames": n_frames, "first_ts": 0}, n, sp, uncond_prefix=[v.bos, v.task("t2v"), v.vid_start])
|
| 224 |
+
return [self.vq.decode(self.vocab.parse([v.vid_start, *r])[0][1]) for r in rows]
|
| 225 |
+
|
| 226 |
+
def image_to_video(self, image, prompt: str, n_frames: int = 8, n: int = 1, sp: Sampling | None = None, force: bool = False) -> list[np.ndarray]:
|
| 227 |
+
"""Use `image` as frame 0 of a text-conditioned clip and generate the remaining frames."""
|
| 228 |
+
self._require("image_to_video", force)
|
| 229 |
+
self._check_frames(n_frames, force)
|
| 230 |
+
sp = sp or Sampling(temperature=1.0, top_k=100, cfg_scale=3.0, max_new_tokens=n_frames * (self.vocab.tokens_per_frame + 3) + 4)
|
| 231 |
+
v = self.vocab
|
| 232 |
+
f0 = v.frame_tokens(self.image_codes(image), 0)
|
| 233 |
+
prefix = [v.bos, v.task("t2v"), *self.enc(normalize_prompt(prompt)), v.vid_start, *f0]
|
| 234 |
+
rows = self._decode(prefix, {"mode": "video", "n_frames": n_frames, "first_ts": 1, "frames_done": 1}, n, sp,
|
| 235 |
+
uncond_prefix=[v.bos, v.task("t2v"), v.vid_start, *f0])
|
| 236 |
+
return [self.vq.decode(self.vocab.parse([v.vid_start, *f0, *r])[0][1]) for r in rows]
|
| 237 |
+
|
| 238 |
+
def continue_video(self, frames, n_new: int = 4, context: int = 4, sp: Sampling | None = None, force: bool = False) -> np.ndarray:
|
| 239 |
+
"""Append `n_new` frames. Rolls a window of the last `context` frames (re-timestamped from t0),
|
| 240 |
+
so clips can be extended beyond the 8-frame training length."""
|
| 241 |
+
self._require("continue_video", force)
|
| 242 |
+
v = self.vocab
|
| 243 |
+
codes = self.video_codes(frames)
|
| 244 |
+
context = min(context, TRAINED_FRAMES - 1)
|
| 245 |
+
out = list(codes)
|
| 246 |
+
while len(out) < len(codes) + n_new:
|
| 247 |
+
ctx = np.stack(out[-context:])
|
| 248 |
+
k = len(ctx)
|
| 249 |
+
todo = min(TRAINED_FRAMES - k, len(codes) + n_new - len(out))
|
| 250 |
+
s = sp or Sampling(temperature=1.0, top_k=100, seed=len(out))
|
| 251 |
+
s = Sampling(**{**s.__dict__, "max_new_tokens": todo * (v.tokens_per_frame + 3) + 4})
|
| 252 |
+
prefix = [v.bos, v.task("vcont"), *v.video_tokens(ctx, close=False)]
|
| 253 |
+
r = self._decode(prefix, {"mode": "video", "n_frames": k + todo, "first_ts": k, "frames_done": k}, 1, s)[0]
|
| 254 |
+
new = v.parse([v.vid_start, *v.video_tokens(ctx, close=False)[1:], *r])[0][1][k:]
|
| 255 |
+
out += list(new)
|
| 256 |
+
return self.vq.decode(np.stack(out))
|
| 257 |
+
|
| 258 |
+
def describe_video(self, frames, sp: Sampling | None = None, force: bool = False) -> str:
|
| 259 |
+
self._require("describe_video", force)
|
| 260 |
+
sp = sp or Sampling(temperature=0.0, max_new_tokens=48)
|
| 261 |
+
v = self.vocab
|
| 262 |
+
return self._text(self._decode([v.bos, v.task("v2t"), *v.video_tokens(self.video_codes(frames))], {"mode": "text"}, 1, sp)[0])
|
| 263 |
+
|
| 264 |
+
def chat(self, message: str, image=None, video=None, sp: Sampling | None = None, force: bool = False) -> list[tuple[str, object]]:
|
| 265 |
+
"""Instruction-following turn. Returns segments: ('text', str) / ('image', array) / ('video', array)."""
|
| 266 |
+
self._require("chat", force)
|
| 267 |
+
v = self.vocab
|
| 268 |
+
sp = sp or Sampling(temperature=0.7, top_k=50, top_p=0.95, repetition_penalty=1.1, max_new_tokens=700)
|
| 269 |
+
user: list[int] = []
|
| 270 |
+
if image is not None:
|
| 271 |
+
user += v.image_tokens(self.image_codes(image))
|
| 272 |
+
if video is not None:
|
| 273 |
+
user += v.video_tokens(self.video_codes(video))
|
| 274 |
+
user += self.enc((" " if user else "") + message.strip())
|
| 275 |
+
prefix = [v.bos, v.user, *user, v.assistant]
|
| 276 |
+
ids = self._decode(prefix, {"mode": "text", "allow_media": True, "stop_after_media": False}, 1, sp)[0]
|
| 277 |
+
segs: list[tuple[str, object]] = []
|
| 278 |
+
for kind, val in v.parse(ids):
|
| 279 |
+
if kind == "text":
|
| 280 |
+
segs.append(("text", self._text(val)))
|
| 281 |
+
elif kind == "image":
|
| 282 |
+
segs.append(("image", self.vq.decode(val[None])[0]))
|
| 283 |
+
else:
|
| 284 |
+
segs.append(("video", self.vq.decode(val)))
|
| 285 |
+
return segs
|
quasar/inference/universal.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generation for the actual v2 packet grammar and a shared Axiom checkpoint."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from ..tokenizers.text import TextTokenizer
|
| 7 |
+
from ..tokenizers.vision import VisualTokenizer
|
| 8 |
+
from ..training.checkpoint import load_quasar
|
| 9 |
+
from .decoding import Sampling,sample_next
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class UniversalGenerator:
|
| 13 |
+
def __init__(self,checkpoint: str|Path,device: str='cuda'):
|
| 14 |
+
self.path=Path(checkpoint)
|
| 15 |
+
self.model,self.vocab,self.meta=load_quasar(self.path,device)
|
| 16 |
+
self.model.eval()
|
| 17 |
+
if not hasattr(self.vocab,'special'):raise ValueError('UniversalGenerator requires a packet-v2 checkpoint')
|
| 18 |
+
self.tok=TextTokenizer.load(self.path/'tokenizers/text_tokenizer.json')
|
| 19 |
+
self.codec=None;self.device=device
|
| 20 |
+
|
| 21 |
+
def vision(self):
|
| 22 |
+
if self.codec is None:self.codec=VisualTokenizer.load(self.path/'tokenizers/vq',self.device)
|
| 23 |
+
return self.codec
|
| 24 |
+
|
| 25 |
+
@torch.inference_mode()
|
| 26 |
+
def _predict(self,task,inputs,kind,sp: Sampling,frames=8,resolution=64):
|
| 27 |
+
if not self.meta.get('task_target_tokens',{}).get(task):
|
| 28 |
+
raise RuntimeError(f'This checkpoint has no recorded training exposure for {task}')
|
| 29 |
+
v=self.vocab;s=v.special
|
| 30 |
+
prefix=[s('bos'),s('task:'+task),s('input')]
|
| 31 |
+
for segment in inputs:prefix+=v.packet(segment,self.tok.encode)
|
| 32 |
+
prefix+=[s('output'),s(kind)]
|
| 33 |
+
# This is structural forcing only. All content codes/tokens are predicted by Quasar.
|
| 34 |
+
if kind=='image':
|
| 35 |
+
if resolution not in (64,128) or (resolution==128 and v.schema_version!=3):
|
| 36 |
+
raise ValueError('128px image generation requires a schema-v3 checkpoint')
|
| 37 |
+
if resolution==128:prefix += [s('resolution:128')]
|
| 38 |
+
plan=[('vision',(resolution//8)**2),('/image',1),('eos',1)]
|
| 39 |
+
elif kind=='video':
|
| 40 |
+
if not 1<=frames<=v.max_frames:raise ValueError('frame count exceeds vocabulary')
|
| 41 |
+
plan=[]
|
| 42 |
+
for i in range(frames):plan += [('frame',1),('time:'+str(i),1),('vision',64),('/frame',1)]
|
| 43 |
+
plan += [('/video',1),('eos',1)]
|
| 44 |
+
else:plan=[('free_text',sp.max_new_tokens)]
|
| 45 |
+
needed=sum(n for _,n in plan)
|
| 46 |
+
if len(prefix)+needed>self.model.cfg.max_seq_len:raise ValueError('requested input/output exceeds trained context')
|
| 47 |
+
device=next(self.model.parameters()).device;use_amp=device.type=='cuda'
|
| 48 |
+
cache=self.model.new_cache(1,dtype=torch.bfloat16 if use_amp else torch.float32)
|
| 49 |
+
gen=torch.Generator(device=device).manual_seed(sp.seed)
|
| 50 |
+
emitted=[];content=[];stop=False
|
| 51 |
+
with torch.autocast(device.type,dtype=torch.bfloat16,enabled=use_amp):
|
| 52 |
+
logits=self.model(torch.tensor([prefix],device=device),cache=cache)[:,-1]
|
| 53 |
+
for state,n in plan:
|
| 54 |
+
for _ in range(n):
|
| 55 |
+
if state in ('vision','free_text'):
|
| 56 |
+
legal=torch.zeros((1,v.size),dtype=torch.bool,device=device)
|
| 57 |
+
lo,hi=v.ranges['vision' if state=='vision' else 'text'];legal[:,lo:hi]=True
|
| 58 |
+
if state=='free_text':legal[:,s('/text')]=True
|
| 59 |
+
history=torch.tensor([content],dtype=torch.long,device=device)
|
| 60 |
+
token=int(sample_next(logits,legal,sp,gen,history,v.text_size)[0])
|
| 61 |
+
if state=='free_text' and token==s('/text'):
|
| 62 |
+
emitted += [token,s('eos')];stop=True;break
|
| 63 |
+
content.append(token)
|
| 64 |
+
else:token=s(state)
|
| 65 |
+
emitted.append(token)
|
| 66 |
+
# A one-token cache append cannot infer its location in the
|
| 67 |
+
# current visual run. Preserve the full-prefix spatial slot,
|
| 68 |
+
# resetting at image/frame boundaries exactly as training does.
|
| 69 |
+
history_tokens=torch.tensor([prefix+emitted],device=device)
|
| 70 |
+
position=self.model.visual_positions(history_tokens)[:,-1:]
|
| 71 |
+
logits=self.model(history_tokens[:,-1:],vis_pos=position,
|
| 72 |
+
cache=cache,start=cache.next_pos)[:,-1]
|
| 73 |
+
if stop:break
|
| 74 |
+
if kind=='text':return self.tok.decode(content),{'learned_text_stop':stop,'token_limit':sp.max_new_tokens}
|
| 75 |
+
v.validate(prefix+emitted)
|
| 76 |
+
codes=np.asarray(content,dtype=np.int64)-v.visual_offset
|
| 77 |
+
return codes.reshape((resolution//8,resolution//8) if kind=='image' else (frames,8,8)),{'structurally_valid':True}
|
| 78 |
+
|
| 79 |
+
def generate_text(self,prompt,sp=None):
|
| 80 |
+
return self._predict('chat_text',[{'type':'text','text':prompt}],'text',sp or Sampling(temperature=.5,top_p=.9))[0]
|
| 81 |
+
|
| 82 |
+
def generate_image(self,prompt,sp=None,resolution=64):
|
| 83 |
+
if resolution==128 and not self.meta.get('image_128_trained',False):
|
| 84 |
+
raise RuntimeError('128px image generation has no verified training exposure in this checkpoint')
|
| 85 |
+
codes,_=self._predict('t2i',[{'type':'text','text':prompt}],'image',sp or Sampling(),resolution=resolution)
|
| 86 |
+
return self.vision().decode(codes[None])[0]
|
| 87 |
+
|
| 88 |
+
def generate_video(self,prompt,frames=8,sp=None):
|
| 89 |
+
codes,_=self._predict('t2v',[{'type':'text','text':prompt}],'video',sp or Sampling(),frames)
|
| 90 |
+
return self.vision().decode(codes)
|
| 91 |
+
|
| 92 |
+
def describe_image(self,pixels,sp=None):
|
| 93 |
+
codes=self.vision().encode(np.asarray(pixels)[None])[0]
|
| 94 |
+
return self._predict('i2t',[{'type':'image','codes':codes}],'text',sp or Sampling(temperature=.5))[0]
|
| 95 |
+
|
| 96 |
+
def describe_video(self,frames,sp=None):
|
| 97 |
+
codes=self.vision().encode(np.asarray(frames))
|
| 98 |
+
return self._predict('v2t',[{'type':'video','codes':codes}],'text',sp or Sampling(temperature=.5))[0]
|
| 99 |
+
|
| 100 |
+
def continue_video(self,frames,new_frames=4,sp=None):
|
| 101 |
+
codes=self.vision().encode(np.asarray(frames))
|
| 102 |
+
generated,_=self._predict('vcont',[{'type':'video','codes':codes}],'video',sp or Sampling(),new_frames)
|
| 103 |
+
return self.vision().decode(generated)
|
quasar/model/__init__.py
ADDED
|
File without changes
|
quasar/model/attention.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal (grouped-query) self-attention on top of torch's scaled_dot_product_attention.
|
| 2 |
+
|
| 3 |
+
SDPA picks the best available backend at runtime (cuDNN / memory-efficient / flash / math);
|
| 4 |
+
nothing here requires a specific kernel, so CPU and any CUDA build work.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from .config import ModelConfig
|
| 13 |
+
from .layers import apply_rope
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class KVCache:
|
| 17 |
+
"""Pre-allocated per-layer key/value buffers stored with n_kv_heads (GQA saves cache memory)."""
|
| 18 |
+
|
| 19 |
+
def __init__(self, cfg: ModelConfig, batch: int, max_len: int, device, dtype):
|
| 20 |
+
shape = (batch, cfg.n_kv_heads, max_len, cfg.head_dim)
|
| 21 |
+
self.k = [torch.zeros(shape, device=device, dtype=dtype) for _ in range(cfg.n_layers)]
|
| 22 |
+
self.v = [torch.zeros(shape, device=device, dtype=dtype) for _ in range(cfg.n_layers)]
|
| 23 |
+
self.max_len = max_len
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Attention(nn.Module):
|
| 27 |
+
def __init__(self, cfg: ModelConfig):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.n_heads, self.n_kv, self.hd = cfg.n_heads, cfg.n_kv_heads, cfg.head_dim
|
| 30 |
+
self.wq = nn.Linear(cfg.d_model, cfg.n_heads * cfg.head_dim, bias=False)
|
| 31 |
+
self.wk = nn.Linear(cfg.d_model, cfg.n_kv_heads * cfg.head_dim, bias=False)
|
| 32 |
+
self.wv = nn.Linear(cfg.d_model, cfg.n_kv_heads * cfg.head_dim, bias=False)
|
| 33 |
+
self.wo = nn.Linear(cfg.n_heads * cfg.head_dim, cfg.d_model, bias=False)
|
| 34 |
+
|
| 35 |
+
def forward(self, x, cos, sin, cache: KVCache | None = None, layer: int = 0, start: int = 0) -> torch.Tensor:
|
| 36 |
+
B, L, _ = x.shape
|
| 37 |
+
q = self.wq(x).view(B, L, self.n_heads, self.hd).transpose(1, 2)
|
| 38 |
+
k = self.wk(x).view(B, L, self.n_kv, self.hd).transpose(1, 2)
|
| 39 |
+
v = self.wv(x).view(B, L, self.n_kv, self.hd).transpose(1, 2)
|
| 40 |
+
q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
|
| 41 |
+
|
| 42 |
+
if cache is not None:
|
| 43 |
+
cache.k[layer][:, :, start : start + L] = k
|
| 44 |
+
cache.v[layer][:, :, start : start + L] = v
|
| 45 |
+
k, v = cache.k[layer][:, :, : start + L], cache.v[layer][:, :, : start + L]
|
| 46 |
+
|
| 47 |
+
if self.n_kv != self.n_heads: # expand KV groups; works with every SDPA backend
|
| 48 |
+
k = k.repeat_interleave(self.n_heads // self.n_kv, dim=1)
|
| 49 |
+
v = v.repeat_interleave(self.n_heads // self.n_kv, dim=1)
|
| 50 |
+
|
| 51 |
+
if L == 1: # single decode step attends to the whole cache
|
| 52 |
+
o = F.scaled_dot_product_attention(q, k, v)
|
| 53 |
+
elif start == 0:
|
| 54 |
+
o = F.scaled_dot_product_attention(q, k, v, is_causal=True)
|
| 55 |
+
else: # chunk appended after a cached prefix: causal w.r.t. absolute positions
|
| 56 |
+
mask = torch.ones(L, start + L, dtype=torch.bool, device=x.device).tril(diagonal=start)
|
| 57 |
+
o = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
|
| 58 |
+
return self.wo(o.transpose(1, 2).reshape(B, L, -1))
|
quasar/model/axiom.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Axiom-1.1 token mixers and dense LawFFN, adapted to Quasar's interface.
|
| 2 |
+
|
| 3 |
+
Architecture reference: user-owned Axiom/axiom/lm.py and lm_variants.py.
|
| 4 |
+
No fission, dormant slots, or Mamba dependency. FP32 recurrence state; ordinary
|
| 5 |
+
PyTorch SDPA attention. Cache appends support both one token and causal chunks.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import math
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch import nn
|
| 13 |
+
from torch.nn import functional as F
|
| 14 |
+
|
| 15 |
+
from .config import ModelConfig
|
| 16 |
+
from .layers import RMSNorm
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def scan(log_a: torch.Tensor, u: torch.Tensor, chunk: int = 64, initial: torch.Tensor | None = None):
|
| 20 |
+
"""Parallel-within-chunk stable diagonal recurrence; inputs [B,T,H], [B,T,H,P]."""
|
| 21 |
+
B, T, H, P = u.shape
|
| 22 |
+
pad = (-T) % chunk
|
| 23 |
+
la = F.pad(log_a.float(), (0, 0, 0, pad)).reshape(B, -1, chunk, H)
|
| 24 |
+
uu = F.pad(u.float(), (0, 0, 0, 0, 0, pad)).reshape(B, -1, chunk, H, P)
|
| 25 |
+
A = la.cumsum(2)
|
| 26 |
+
causal = torch.ones(chunk, chunk, dtype=torch.bool, device=u.device).tril()
|
| 27 |
+
diff = (A[:, :, :, None] - A[:, :, None]).masked_fill(~causal[None, None, :, :, None], -math.inf)
|
| 28 |
+
weights = diff.exp() * -torch.expm1(la)[:, :, None]
|
| 29 |
+
local = torch.einsum("bnijh,bnjhp->bnihp", weights, uu)
|
| 30 |
+
state = u.new_zeros(B, H, P, dtype=torch.float32) if initial is None else initial.float()
|
| 31 |
+
chunks = []
|
| 32 |
+
for k in range(la.shape[1]):
|
| 33 |
+
out = local[:, k] + A[:, k].exp()[..., None] * state[:, None]
|
| 34 |
+
state = out[:, -1]
|
| 35 |
+
chunks.append(out)
|
| 36 |
+
result = torch.cat(chunks, 1)[:, :T]
|
| 37 |
+
return result, result[:, -1]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class HybridCache:
|
| 41 |
+
"""Only attention layers store growing KV; recurrent layers store one FP32 vector."""
|
| 42 |
+
def __init__(self, cfg: ModelConfig, batch: int, max_len: int, device, dtype):
|
| 43 |
+
self.max_len, self.next_pos = max_len, 0
|
| 44 |
+
shape = (batch, cfg.n_heads, max_len, cfg.head_dim)
|
| 45 |
+
self.k = {i: torch.zeros(shape, device=device, dtype=dtype) for i in cfg.attention_layers}
|
| 46 |
+
self.v = {i: torch.zeros(shape, device=device, dtype=dtype) for i in cfg.attention_layers}
|
| 47 |
+
self.states = {i: torch.zeros(batch, cfg.n_heads, cfg.head_dim, device=device, dtype=torch.float32)
|
| 48 |
+
for i in range(cfg.n_layers) if i not in cfg.attention_layers}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Recurrence(nn.Module):
|
| 52 |
+
def __init__(self, cfg: ModelConfig):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.h, self.chunk = cfg.n_heads, cfg.scan_chunk
|
| 55 |
+
self.inp = nn.Linear(cfg.d_model, 2 * cfg.d_model + cfg.n_heads, bias=False)
|
| 56 |
+
self.out = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 57 |
+
self.decay_bias = nn.Parameter(torch.linspace(math.log(math.expm1(1.0)), math.log(math.expm1(1e-3)), self.h))
|
| 58 |
+
|
| 59 |
+
def forward(self, x, cache=None, layer=0):
|
| 60 |
+
B, T, D = x.shape
|
| 61 |
+
u, g, z = self.inp(x).split([D, D, self.h], -1)
|
| 62 |
+
with torch.autocast(x.device.type, enabled=False):
|
| 63 |
+
la = -F.softplus(z.float() + self.decay_bias)
|
| 64 |
+
s, final = scan(la, u.reshape(B, T, self.h, D // self.h), self.chunk,
|
| 65 |
+
None if cache is None else cache.states[layer])
|
| 66 |
+
if cache is not None:
|
| 67 |
+
cache.states[layer] = final.detach()
|
| 68 |
+
return self.out(s.reshape(B, T, D).to(g.dtype) * F.silu(g))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class AxiomAttention(nn.Module):
|
| 72 |
+
def __init__(self, cfg: ModelConfig):
|
| 73 |
+
super().__init__()
|
| 74 |
+
self.h = cfg.n_heads
|
| 75 |
+
self.qkv = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=False)
|
| 76 |
+
self.out = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 77 |
+
|
| 78 |
+
def forward(self, x, cos, sin, cache=None, layer=0, start=0):
|
| 79 |
+
B, T, D = x.shape
|
| 80 |
+
q, k, v = self.qkv(x).view(B, T, 3, self.h, D // self.h).permute(2, 0, 3, 1, 4)
|
| 81 |
+
def rotate(t): # Axiom uses interleaved pairs, rather than Quasar-v1's half pairs
|
| 82 |
+
a, b = t.float()[..., 0::2], t.float()[..., 1::2]
|
| 83 |
+
return torch.stack((a * cos - b * sin, a * sin + b * cos), -1).flatten(-2).to(t.dtype)
|
| 84 |
+
q, k = rotate(q), rotate(k)
|
| 85 |
+
if cache is not None:
|
| 86 |
+
cache.k[layer][:, :, start:start + T] = k
|
| 87 |
+
cache.v[layer][:, :, start:start + T] = v
|
| 88 |
+
k, v = cache.k[layer][:, :, :start + T], cache.v[layer][:, :, :start + T]
|
| 89 |
+
mask = None
|
| 90 |
+
if start and T > 1:
|
| 91 |
+
mask = torch.ones(T, start + T, dtype=torch.bool, device=x.device).tril(start)
|
| 92 |
+
o = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, is_causal=start == 0 and T > 1)
|
| 93 |
+
return self.out(o.transpose(1, 2).reshape(B, T, D))
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class LawFFN(nn.Module):
|
| 97 |
+
def __init__(self, cfg: ModelConfig):
|
| 98 |
+
super().__init__()
|
| 99 |
+
self.router = nn.Linear(cfg.d_model, cfg.n_cells)
|
| 100 |
+
self.cells = nn.ModuleList(nn.Sequential(nn.Linear(cfg.d_model, cfg.d_cell), nn.GELU(),
|
| 101 |
+
nn.Linear(cfg.d_cell, cfg.d_model)) for _ in range(cfg.n_cells))
|
| 102 |
+
|
| 103 |
+
def forward(self, x):
|
| 104 |
+
weights = self.router(x).float().softmax(-1)
|
| 105 |
+
y = sum(cell(x) * weights[..., i:i + 1].to(x.dtype) for i, cell in enumerate(self.cells))
|
| 106 |
+
# Switch-style fraction is 1/K for top-K == K. This is identically 1;
|
| 107 |
+
# retain source semantics without claiming it regularizes expert weights.
|
| 108 |
+
balance = weights.mean(dim=(0, 1)).sum()
|
| 109 |
+
return y, balance
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class HybridBlock(nn.Module):
|
| 113 |
+
def __init__(self, cfg: ModelConfig, index: int):
|
| 114 |
+
super().__init__()
|
| 115 |
+
self.is_attention = index in cfg.attention_layers
|
| 116 |
+
self.n1, self.n2 = RMSNorm(cfg.d_model, cfg.norm_eps), RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 117 |
+
self.mix = AxiomAttention(cfg) if self.is_attention else Recurrence(cfg)
|
| 118 |
+
self.ffn = LawFFN(cfg)
|
| 119 |
+
|
| 120 |
+
def forward(self, x, cos, sin, cache=None, layer=0, start=0):
|
| 121 |
+
y = self.mix(self.n1(x), cos, sin, cache, layer, start) if self.is_attention else self.mix(self.n1(x), cache, layer)
|
| 122 |
+
x = x + y
|
| 123 |
+
f, balance = self.ffn(self.n2(x))
|
| 124 |
+
return x + f, balance
|
quasar/model/config.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model configuration and YAML profile loading."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import asdict, dataclass, fields
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class ModelConfig:
|
| 12 |
+
architecture: str = "transformer" # old checkpoints default to the original transformer
|
| 13 |
+
vocab_size: int = 9280
|
| 14 |
+
d_model: int = 512
|
| 15 |
+
n_layers: int = 8
|
| 16 |
+
n_heads: int = 8
|
| 17 |
+
n_kv_heads: int = 8 # < n_heads => grouped-query attention
|
| 18 |
+
ffn_mult: float = 8 / 3 # SwiGLU hidden = ffn_mult * d_model, rounded up to a multiple of 128
|
| 19 |
+
max_seq_len: int = 1024
|
| 20 |
+
rope_theta: float = 10000.0
|
| 21 |
+
norm_eps: float = 1e-5
|
| 22 |
+
vis_positions: int = 64 # spatial slots per frame (8x8 grid); 0 disables visual position embedding
|
| 23 |
+
visual_offset: int = 0 # first visual-code token id (from UnifiedVocab)
|
| 24 |
+
visual_size: int = 0 # number of visual-code ids
|
| 25 |
+
tie_embeddings: bool = True
|
| 26 |
+
init_std: float = 0.02
|
| 27 |
+
grad_checkpoint: bool = False
|
| 28 |
+
attention_layers: tuple[int, ...] = (3, 7, 11) # zero-based Axiom-1.1 indices
|
| 29 |
+
d_cell: int = 1280
|
| 30 |
+
n_cells: int = 2
|
| 31 |
+
top_k: int = 2
|
| 32 |
+
scan_chunk: int = 64
|
| 33 |
+
balance_weight: float = 0.01
|
| 34 |
+
|
| 35 |
+
def __post_init__(self):
|
| 36 |
+
assert self.d_model % self.n_heads == 0, "d_model must divide by n_heads"
|
| 37 |
+
assert self.n_heads % self.n_kv_heads == 0, "n_heads must divide by n_kv_heads"
|
| 38 |
+
if self.architecture not in ("transformer", "axiom1_1"):
|
| 39 |
+
raise ValueError(f"unknown architecture: {self.architecture}")
|
| 40 |
+
if self.architecture == "axiom1_1":
|
| 41 |
+
self.attention_layers = tuple(self.attention_layers)
|
| 42 |
+
if self.n_cells != 2 or self.top_k != 2:
|
| 43 |
+
raise ValueError("Axiom-1.1 uses two cells, both active; sparse routing is not implemented")
|
| 44 |
+
if self.n_kv_heads != self.n_heads or self.head_dim % 2:
|
| 45 |
+
raise ValueError("Axiom-1.1 requires MHA and an even head dimension")
|
| 46 |
+
if len(set(self.attention_layers)) != len(self.attention_layers) or any(i < 0 or i >= self.n_layers for i in self.attention_layers):
|
| 47 |
+
raise ValueError("attention_layers must contain distinct zero-based layer indices")
|
| 48 |
+
if self.scan_chunk < 1 or self.d_cell < 1:
|
| 49 |
+
raise ValueError("scan_chunk and d_cell must be positive")
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def head_dim(self) -> int:
|
| 53 |
+
return self.d_model // self.n_heads
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def ffn_hidden(self) -> int:
|
| 57 |
+
return -(-int(self.ffn_mult * self.d_model) // 128) * 128
|
| 58 |
+
|
| 59 |
+
def to_dict(self) -> dict:
|
| 60 |
+
return asdict(self)
|
| 61 |
+
|
| 62 |
+
@classmethod
|
| 63 |
+
def from_dict(cls, d: dict) -> "ModelConfig":
|
| 64 |
+
names = {f.name for f in fields(cls)}
|
| 65 |
+
unknown = set(d) - names
|
| 66 |
+
if unknown:
|
| 67 |
+
raise ValueError(f"unknown model config keys: {sorted(unknown)}")
|
| 68 |
+
return cls(**d)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def load_yaml(path: str | Path) -> dict:
|
| 72 |
+
with open(path) as f:
|
| 73 |
+
return yaml.safe_load(f) or {}
|
quasar/model/layers.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformer building blocks: RMSNorm, rotary position embedding, SwiGLU."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class RMSNorm(nn.Module):
|
| 10 |
+
def __init__(self, dim: int, eps: float = 1e-5):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.eps = eps
|
| 13 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 14 |
+
|
| 15 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 16 |
+
xf = x.float()
|
| 17 |
+
return (xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + self.eps)).type_as(x) * self.weight
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def rope_tables(head_dim: int, max_len: int, theta: float, device=None) -> tuple[torch.Tensor, torch.Tensor]:
|
| 21 |
+
"""cos/sin tables [max_len, head_dim/2] (float32)."""
|
| 22 |
+
inv = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
|
| 23 |
+
ang = torch.outer(torch.arange(max_len, device=device, dtype=torch.float32), inv)
|
| 24 |
+
return ang.cos(), ang.sin()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
| 28 |
+
"""x: [B, H, L, D]; cos/sin: [L, D/2]. Rotates interleaved-as-halves pairs (GPT-NeoX style)."""
|
| 29 |
+
x1, x2 = x.float().chunk(2, dim=-1)
|
| 30 |
+
out = torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
|
| 31 |
+
return out.type_as(x)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class SwiGLU(nn.Module):
|
| 35 |
+
def __init__(self, dim: int, hidden: int):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.w_gate = nn.Linear(dim, hidden, bias=False)
|
| 38 |
+
self.w_up = nn.Linear(dim, hidden, bias=False)
|
| 39 |
+
self.w_down = nn.Linear(hidden, dim, bias=False)
|
| 40 |
+
|
| 41 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 42 |
+
return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))
|
quasar/model/transformer.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quasar: one decoder-only transformer over the unified text/image/video token space.
|
| 2 |
+
|
| 3 |
+
Pre-norm blocks (RMSNorm -> GQA attention with RoPE -> residual -> RMSNorm -> SwiGLU -> residual),
|
| 4 |
+
tied input/output embeddings, and one extra signal for visual tokens: a learned embedding of the
|
| 5 |
+
token's spatial slot inside its frame (0 for non-visual tokens). The slot is a pure function of
|
| 6 |
+
the token sequence (index within the current contiguous run of visual codes), so it carries no
|
| 7 |
+
information the tokens do not already imply; it just saves a small model from learning to count.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import math
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
from torch.utils.checkpoint import checkpoint
|
| 17 |
+
|
| 18 |
+
from .attention import Attention, KVCache
|
| 19 |
+
from .axiom import HybridBlock, HybridCache
|
| 20 |
+
from .config import ModelConfig
|
| 21 |
+
from .layers import RMSNorm, SwiGLU, rope_tables
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Block(nn.Module):
|
| 25 |
+
def __init__(self, cfg: ModelConfig):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.attn_norm = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 28 |
+
self.attn = Attention(cfg)
|
| 29 |
+
self.ffn_norm = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 30 |
+
self.ffn = SwiGLU(cfg.d_model, cfg.ffn_hidden)
|
| 31 |
+
|
| 32 |
+
def forward(self, x, cos, sin, cache=None, layer=0, start=0):
|
| 33 |
+
x = x + self.attn(self.attn_norm(x), cos, sin, cache, layer, start)
|
| 34 |
+
return x + self.ffn(self.ffn_norm(x))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Quasar(nn.Module):
|
| 38 |
+
def __init__(self, cfg: ModelConfig):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.cfg = cfg
|
| 41 |
+
self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
|
| 42 |
+
self.vis_embed = nn.Embedding(cfg.vis_positions + 1, cfg.d_model, padding_idx=0) if cfg.vis_positions else None
|
| 43 |
+
self.hybrid = cfg.architecture == "axiom1_1"
|
| 44 |
+
self.blocks = nn.ModuleList(HybridBlock(cfg, i) if self.hybrid else Block(cfg) for i in range(cfg.n_layers))
|
| 45 |
+
self.norm = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 46 |
+
self.head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
|
| 47 |
+
if cfg.tie_embeddings:
|
| 48 |
+
self.head.weight = self.embed.weight
|
| 49 |
+
cos, sin = rope_tables(cfg.head_dim, cfg.max_seq_len, cfg.rope_theta)
|
| 50 |
+
self.register_buffer("rope_cos", cos, persistent=False)
|
| 51 |
+
self.register_buffer("rope_sin", sin, persistent=False)
|
| 52 |
+
self.apply(self._init)
|
| 53 |
+
for b in self.blocks: # scale residual-branch outputs (GPT-2 style)
|
| 54 |
+
residuals = [b.mix.out.weight, *(c[2].weight for c in b.ffn.cells)] if self.hybrid else [b.attn.wo.weight, b.ffn.w_down.weight]
|
| 55 |
+
for w in residuals:
|
| 56 |
+
nn.init.normal_(w, std=cfg.init_std / math.sqrt(2 * cfg.n_layers))
|
| 57 |
+
|
| 58 |
+
def _init(self, m: nn.Module) -> None:
|
| 59 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 60 |
+
nn.init.normal_(m.weight, std=self.cfg.init_std)
|
| 61 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 62 |
+
nn.init.zeros_(m.bias)
|
| 63 |
+
if isinstance(m, nn.Embedding) and m.padding_idx is not None:
|
| 64 |
+
with torch.no_grad():
|
| 65 |
+
m.weight[m.padding_idx].zero_()
|
| 66 |
+
|
| 67 |
+
def num_params(self, include_embedding: bool = True) -> int:
|
| 68 |
+
n = sum(p.numel() for p in self.parameters()) # tied weights counted once
|
| 69 |
+
return n if include_embedding else n - self.embed.weight.numel()
|
| 70 |
+
|
| 71 |
+
def visual_positions(self, tokens: torch.Tensor) -> torch.Tensor:
|
| 72 |
+
"""1-based index of each visual token inside its contiguous run (0 for other tokens)."""
|
| 73 |
+
lo, hi = self.cfg.visual_offset, self.cfg.visual_offset + self.cfg.visual_size
|
| 74 |
+
is_vis = (tokens >= lo) & (tokens < hi)
|
| 75 |
+
idx = torch.arange(tokens.shape[-1], device=tokens.device).expand_as(tokens)
|
| 76 |
+
run_start = torch.where(is_vis, torch.zeros_like(idx), idx + 1).cummax(dim=-1).values
|
| 77 |
+
return torch.where(is_vis, idx - run_start + 1, torch.zeros_like(idx)).clamp(max=self.cfg.vis_positions)
|
| 78 |
+
|
| 79 |
+
def forward(self, tokens: torch.Tensor, vis_pos: torch.Tensor | None = None, cache: KVCache | HybridCache | None = None, start: int = 0,
|
| 80 |
+
return_aux: bool = False):
|
| 81 |
+
"""tokens [B, L] -> logits [B, L, V]. With a cache, `start` is the absolute position of tokens[:, 0]."""
|
| 82 |
+
B, L = tokens.shape
|
| 83 |
+
if start + L > self.cfg.max_seq_len:
|
| 84 |
+
raise ValueError(f"sequence length {start + L} exceeds max_seq_len {self.cfg.max_seq_len}")
|
| 85 |
+
if cache is not None and self.hybrid:
|
| 86 |
+
if not isinstance(cache, HybridCache) or start != cache.next_pos or start + L > cache.max_len:
|
| 87 |
+
raise ValueError("hybrid cache requires consecutive appends within its capacity; use a fresh cache for a new sequence")
|
| 88 |
+
x = self.embed(tokens)
|
| 89 |
+
if self.vis_embed is not None:
|
| 90 |
+
x = x + self.vis_embed(self.visual_positions(tokens) if vis_pos is None else vis_pos)
|
| 91 |
+
cos, sin = self.rope_cos[start : start + L], self.rope_sin[start : start + L]
|
| 92 |
+
balances = []
|
| 93 |
+
for i, blk in enumerate(self.blocks):
|
| 94 |
+
if self.cfg.grad_checkpoint and self.training and cache is None:
|
| 95 |
+
result = checkpoint(blk, x, cos, sin, use_reentrant=False)
|
| 96 |
+
else:
|
| 97 |
+
result = blk(x, cos, sin, cache, i, start)
|
| 98 |
+
if self.hybrid:
|
| 99 |
+
x, balance = result
|
| 100 |
+
balances.append(balance)
|
| 101 |
+
else:
|
| 102 |
+
x = result
|
| 103 |
+
if cache is not None and self.hybrid:
|
| 104 |
+
cache.next_pos = start + L
|
| 105 |
+
logits = self.head(self.norm(x))
|
| 106 |
+
aux = torch.stack(balances).mean() if balances else logits.new_zeros(())
|
| 107 |
+
return (logits, aux) if return_aux else logits
|
| 108 |
+
|
| 109 |
+
def loss(self, tokens: torch.Tensor, labels: torch.Tensor, include_aux: bool = True) -> tuple[torch.Tensor, torch.Tensor]:
|
| 110 |
+
"""Mean target-token CE, optionally plus Axiom balance loss; (objective, n_targets)."""
|
| 111 |
+
logits, aux = self(tokens, return_aux=True)
|
| 112 |
+
n = (labels != -100).sum()
|
| 113 |
+
loss = F.cross_entropy(logits.float().reshape(-1, logits.size(-1)), labels.reshape(-1), ignore_index=-100)
|
| 114 |
+
return loss + (self.cfg.balance_weight * aux if include_aux else 0), n
|
| 115 |
+
|
| 116 |
+
def new_cache(self, batch: int, max_len: int | None = None, dtype=None) -> KVCache:
|
| 117 |
+
p = next(self.parameters())
|
| 118 |
+
cache_type = HybridCache if self.hybrid else KVCache
|
| 119 |
+
return cache_type(self.cfg, batch, max_len or self.cfg.max_seq_len, p.device, dtype or p.dtype)
|
quasar/tokenizers/__init__.py
ADDED
|
File without changes
|
quasar/tokenizers/audio.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frozen DAC interface. Loads ONLY explicitly supplied local safetensors weights.
|
| 2 |
+
|
| 3 |
+
No implicit downloads. The shared Quasar model predicts the codes; this module
|
| 4 |
+
only reconstructs waveforms. Four-of-32 codebook quality is unmeasured here.
|
| 5 |
+
"""
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import math
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AudioTokenizer:
|
| 14 |
+
def __init__(self, local_path: str | Path, codebooks: int = 4, max_seconds: float = 2.0, device: str = "cpu"):
|
| 15 |
+
p = Path(local_path)
|
| 16 |
+
if not (p / "config.json").exists() or not (p / "model.safetensors").exists():
|
| 17 |
+
raise FileNotFoundError(f"DAC is not installed at {p}; provision the pinned codec explicitly before audio preprocessing")
|
| 18 |
+
from transformers import DacModel
|
| 19 |
+
self.model = DacModel.from_pretrained(str(p), local_files_only=True, use_safetensors=True).to(device).eval()
|
| 20 |
+
self.model.requires_grad_(False)
|
| 21 |
+
self.sr, self.books, self.max_seconds = self.model.config.sampling_rate, codebooks, max_seconds
|
| 22 |
+
self.hop = math.prod(self.model.config.downsampling_ratios)
|
| 23 |
+
if not 1 <= codebooks <= self.model.config.n_codebooks:
|
| 24 |
+
raise ValueError("requested codebooks exceed the codec")
|
| 25 |
+
self.device = device
|
| 26 |
+
|
| 27 |
+
@torch.no_grad()
|
| 28 |
+
def encode(self, waveform, sample_rate: int) -> dict:
|
| 29 |
+
x = torch.as_tensor(waveform, dtype=torch.float32, device=self.device)
|
| 30 |
+
if sample_rate != self.sr:
|
| 31 |
+
raise ValueError(f"resample to {self.sr} Hz before encoding; received {sample_rate}")
|
| 32 |
+
if x.ndim != 1 or not x.numel() or not torch.isfinite(x).all() or x.abs().max() > 1.001:
|
| 33 |
+
raise ValueError("expected finite mono waveform in [-1,1]")
|
| 34 |
+
if x.numel() > self.sr * self.max_seconds:
|
| 35 |
+
raise ValueError("audio exceeds duration budget; use transcript-aligned clips")
|
| 36 |
+
n = x.numel()
|
| 37 |
+
x = F.pad(x, (0, (-n) % self.hop))[None, None]
|
| 38 |
+
codes = self.model.encode(x, n_quantizers=self.books).audio_codes[0].T
|
| 39 |
+
return {"type": "audio", "codes": codes.cpu().numpy().astype(np.int32).tolist(), "samples": n, "sample_rate": self.sr}
|
| 40 |
+
|
| 41 |
+
@torch.no_grad()
|
| 42 |
+
def decode(self, packet: dict) -> np.ndarray:
|
| 43 |
+
a = np.asarray(packet["codes"])
|
| 44 |
+
if a.ndim != 2 or a.shape[1] != self.books or a.dtype.kind not in "iu" or not a.size or a.min() < 0 or a.max() >= self.model.config.codebook_size:
|
| 45 |
+
raise ValueError("invalid DAC codes")
|
| 46 |
+
codes = torch.tensor(a.T[None], dtype=torch.long, device=self.device)
|
| 47 |
+
y = self.model.decode(audio_codes=codes).audio_values[0]
|
| 48 |
+
return y[:packet.get("samples", len(y))].cpu().numpy()
|
quasar/tokenizers/audio_aligned.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frozen DAC interface with explicit, training-calibrated boundary alignment.
|
| 2 |
+
|
| 3 |
+
Source utterances remain whole. Silent boundary guards are codec padding, and
|
| 4 |
+
the fixed decoder offset is recorded separately from the generative weights.
|
| 5 |
+
"""
|
| 6 |
+
import numpy as np
|
| 7 |
+
from .audio import AudioTokenizer
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AlignedAudioTokenizer(AudioTokenizer):
|
| 11 |
+
def __init__(self, local_path, alignment_samples: int, codebooks=4, max_seconds=2.0, device='cpu'):
|
| 12 |
+
super().__init__(local_path, codebooks, max_seconds, device)
|
| 13 |
+
if self.sr != 24000 or self.hop != 320 or abs(alignment_samples) > 32:
|
| 14 |
+
raise ValueError('alignment is only supported for the pinned 24 kHz/320-hop codec')
|
| 15 |
+
self.source_max_seconds = max_seconds
|
| 16 |
+
self.max_seconds = max_seconds + 2 * self.hop / self.sr
|
| 17 |
+
self.alignment_samples = alignment_samples
|
| 18 |
+
|
| 19 |
+
def encode(self, waveform, sample_rate):
|
| 20 |
+
x = np.asarray(waveform)
|
| 21 |
+
if x.ndim != 1 or not 0 < len(x) <= self.source_max_seconds * self.sr:
|
| 22 |
+
raise ValueError('complete source utterance exceeds duration budget')
|
| 23 |
+
packet = super().encode(np.pad(x, (self.hop, self.hop)), sample_rate)
|
| 24 |
+
packet['samples'] = len(x)
|
| 25 |
+
return packet
|
| 26 |
+
|
| 27 |
+
def decode(self, packet):
|
| 28 |
+
# Ask the base decoder for all samples, before applying boundary guards.
|
| 29 |
+
expanded = dict(packet, samples=(len(packet['codes']) + 2) * self.hop)
|
| 30 |
+
raw = super().decode(expanded)
|
| 31 |
+
start = self.hop + self.alignment_samples
|
| 32 |
+
n = packet.get('samples', min(int(self.source_max_seconds * self.sr), len(raw) - 2 * self.hop))
|
| 33 |
+
if not 0 < n <= self.source_max_seconds * self.sr or start + n > len(raw):
|
| 34 |
+
raise ValueError('decoded audio cannot preserve the complete source duration')
|
| 35 |
+
return raw[start:start + n]
|
quasar/tokenizers/audio_complete.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DAC boundary padding for complete source utterances; no source cropping."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from .audio import AudioTokenizer
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CompleteAudioTokenizer(AudioTokenizer):
|
| 7 |
+
"""Guard the decoder's eight-sample edge shortfall with a full silent hop.
|
| 8 |
+
|
| 9 |
+
The original source must fit the two-second budget. Only codec boundary padding
|
| 10 |
+
extends beyond it; the packet retains the original sample count for decoding.
|
| 11 |
+
"""
|
| 12 |
+
def __init__(self, local_path, codebooks=4, max_seconds=2.0, device='cpu'):
|
| 13 |
+
super().__init__(local_path, codebooks, max_seconds, device)
|
| 14 |
+
self.source_max_seconds = max_seconds
|
| 15 |
+
self.max_seconds = max_seconds + self.hop / self.sr
|
| 16 |
+
|
| 17 |
+
def encode(self, waveform, sample_rate):
|
| 18 |
+
x = np.asarray(waveform)
|
| 19 |
+
if x.ndim != 1 or not 0 < len(x) <= self.source_max_seconds * self.sr:
|
| 20 |
+
raise ValueError('complete source utterance exceeds duration budget')
|
| 21 |
+
packet = super().encode(np.pad(x, (0, self.hop)), sample_rate)
|
| 22 |
+
packet['samples'] = len(x)
|
| 23 |
+
return packet
|
quasar/tokenizers/mesh.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic quantized triangle-mesh codec; no pretrained generator or mesh simplification."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def quantize_mesh(vertices, faces, bins: int = 512, max_vertices: int = 128, max_faces: int = 128):
|
| 7 |
+
v, f = np.asarray(vertices, dtype=np.float64), np.asarray(faces)
|
| 8 |
+
if v.ndim != 2 or v.shape[1] != 3 or not 3 <= len(v) <= max_vertices or not np.isfinite(v).all():
|
| 9 |
+
raise ValueError("expected finite mesh vertices; simplify externally if over the vertex budget")
|
| 10 |
+
if f.ndim != 2 or f.shape[1] != 3 or not 1 <= len(f) <= max_faces or f.dtype.kind not in "iu" or f.min() < 0 or f.max() >= len(v):
|
| 11 |
+
raise ValueError("invalid triangle faces or face budget exceeded")
|
| 12 |
+
center = (v.min(0) + v.max(0)) / 2
|
| 13 |
+
scale = float(np.ptp(v, axis=0).max())
|
| 14 |
+
if scale <= 1e-12:
|
| 15 |
+
raise ValueError("zero-extent mesh")
|
| 16 |
+
q = np.rint(((v - center) / scale + 0.5) * (bins - 1)).clip(0, bins - 1).astype(np.int32)
|
| 17 |
+
# Quantization can merge vertices or collapse skinny triangles: reject these samples.
|
| 18 |
+
if len(np.unique(q, axis=0)) != len(q) or np.any(np.linalg.norm(np.cross(q[f[:, 1]] - q[f[:, 0]], q[f[:, 2]] - q[f[:, 0]]), axis=1) == 0):
|
| 19 |
+
raise ValueError("degenerate geometry after quantization")
|
| 20 |
+
return {"type": "mesh", "vertices": q.tolist(), "faces": f.tolist()}, {"center": center.tolist(), "scale": scale, "bins": bins}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def decode_mesh(packet: dict, transform: dict | None = None, bins: int = 512):
|
| 24 |
+
v = np.asarray(packet["vertices"], dtype=np.float64) / (bins - 1) - 0.5
|
| 25 |
+
if transform is not None:
|
| 26 |
+
v = v * transform["scale"] + transform["center"]
|
| 27 |
+
return v, np.asarray(packet["faces"], dtype=np.int32)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def write_obj(packet: dict, path: str | Path, transform: dict | None = None):
|
| 31 |
+
"""Export geometry as a real OBJ; materials/textures are deliberately outside this codec."""
|
| 32 |
+
v, f = decode_mesh(packet, transform)
|
| 33 |
+
text = "".join(f"v {x:.7f} {y:.7f} {z:.7f}\n" for x, y, z in v)
|
| 34 |
+
text += "".join(f"f {a+1} {b+1} {c+1}\n" for a, b, c in f)
|
| 35 |
+
Path(path).write_text(text, encoding="utf-8")
|
quasar/tokenizers/multimodal.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unified token space shared by text, images and video.
|
| 2 |
+
|
| 3 |
+
ID layout (non-overlapping ranges):
|
| 4 |
+
|
| 5 |
+
[0, T) text BPE tokens
|
| 6 |
+
[T, T + 64) structural / special tokens (64 reserved slots)
|
| 7 |
+
[T + 64, T + 64 + V) visual codes from the frozen FSQ tokenizer
|
| 8 |
+
padded up to a multiple of 64 (unused tail ids are never produced or targeted)
|
| 9 |
+
|
| 10 |
+
Sequence formats (one per task; loss is applied from `target_start` onward):
|
| 11 |
+
|
| 12 |
+
text <bos> text... <eos>
|
| 13 |
+
t2i <bos><task:t2i> caption <image> v*G </image> <eos>
|
| 14 |
+
i2t <bos><task:i2t> <image> v*G </image> caption <eos>
|
| 15 |
+
i2i <bos><task:i2i> instruction <image> src </image> <image> tgt </image> <eos>
|
| 16 |
+
t2v <bos><task:t2v> caption <video> (<frame><t_k> v*G </frame>)*F </video> <eos>
|
| 17 |
+
v2t <bos><task:v2t> <video> ... </video> caption <eos>
|
| 18 |
+
vcont <bos><task:vcont> <video> ... </video> <eos>
|
| 19 |
+
chat <bos><user> text [media] <assistant> text [media] <eos>
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import json
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
N_SPECIAL_SLOTS = 64
|
| 29 |
+
MAX_FRAMES = 32
|
| 30 |
+
SPECIALS = [
|
| 31 |
+
"<pad>", "<bos>", "<eos>",
|
| 32 |
+
"<task:text>", "<task:t2i>", "<task:i2t>", "<task:i2i>", "<task:t2v>", "<task:v2t>", "<task:vcont>",
|
| 33 |
+
"<user>", "<assistant>",
|
| 34 |
+
"<image>", "</image>", "<video>", "</video>", "<frame>", "</frame>",
|
| 35 |
+
] + [f"<t{i}>" for i in range(MAX_FRAMES)] # frame timestamps (index; seconds = k / fps)
|
| 36 |
+
assert len(SPECIALS) <= N_SPECIAL_SLOTS
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class Sample:
|
| 41 |
+
tokens: np.ndarray # int32 [L]
|
| 42 |
+
target_start: int # loss on predicting tokens[target_start:]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class UnifiedVocab:
|
| 46 |
+
def __init__(self, text_size: int, visual_size: int, grid: tuple[int, int] = (8, 8), fps: int = 4):
|
| 47 |
+
self.text_size = text_size
|
| 48 |
+
self.visual_size = visual_size
|
| 49 |
+
self.grid = tuple(grid)
|
| 50 |
+
self.fps = fps
|
| 51 |
+
self.special_offset = text_size
|
| 52 |
+
self.visual_offset = text_size + N_SPECIAL_SLOTS
|
| 53 |
+
self.size = -(-(self.visual_offset + visual_size) // 64) * 64
|
| 54 |
+
self.ids = {name: self.special_offset + i for i, name in enumerate(SPECIALS)}
|
| 55 |
+
self.names = {v: k for k, v in self.ids.items()}
|
| 56 |
+
self.pad, self.bos, self.eos = self.ids["<pad>"], self.ids["<bos>"], self.ids["<eos>"]
|
| 57 |
+
self.img_start, self.img_end = self.ids["<image>"], self.ids["</image>"]
|
| 58 |
+
self.vid_start, self.vid_end = self.ids["<video>"], self.ids["</video>"]
|
| 59 |
+
self.frm_start, self.frm_end = self.ids["<frame>"], self.ids["</frame>"]
|
| 60 |
+
self.user, self.assistant = self.ids["<user>"], self.ids["<assistant>"]
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def tokens_per_frame(self) -> int:
|
| 64 |
+
return self.grid[0] * self.grid[1]
|
| 65 |
+
|
| 66 |
+
def task(self, name: str) -> int:
|
| 67 |
+
return self.ids[f"<task:{name}>"]
|
| 68 |
+
|
| 69 |
+
def timestamp(self, k: int) -> int:
|
| 70 |
+
return self.ids[f"<t{k}>"]
|
| 71 |
+
|
| 72 |
+
def is_visual(self, t) -> np.ndarray:
|
| 73 |
+
t = np.asarray(t)
|
| 74 |
+
return (t >= self.visual_offset) & (t < self.visual_offset + self.visual_size)
|
| 75 |
+
|
| 76 |
+
def is_text(self, t) -> np.ndarray:
|
| 77 |
+
t = np.asarray(t)
|
| 78 |
+
return (t >= 0) & (t < self.text_size)
|
| 79 |
+
|
| 80 |
+
# ------------------------------------------------------------------ media -> tokens
|
| 81 |
+
def image_tokens(self, codes: np.ndarray) -> list[int]:
|
| 82 |
+
codes = np.asarray(codes).reshape(-1)
|
| 83 |
+
assert codes.size == self.tokens_per_frame and codes.min() >= 0 and codes.max() < self.visual_size
|
| 84 |
+
return [self.img_start, *(codes + self.visual_offset).tolist(), self.img_end]
|
| 85 |
+
|
| 86 |
+
def frame_tokens(self, codes: np.ndarray, k: int) -> list[int]:
|
| 87 |
+
codes = np.asarray(codes).reshape(-1)
|
| 88 |
+
return [self.frm_start, self.timestamp(k), *(codes + self.visual_offset).tolist(), self.frm_end]
|
| 89 |
+
|
| 90 |
+
def video_tokens(self, codes: np.ndarray, first_index: int = 0, close: bool = True) -> list[int]:
|
| 91 |
+
out = [self.vid_start]
|
| 92 |
+
for k, f in enumerate(np.asarray(codes)):
|
| 93 |
+
out += self.frame_tokens(f, first_index + k)
|
| 94 |
+
return out + ([self.vid_end] if close else [])
|
| 95 |
+
|
| 96 |
+
# ------------------------------------------------------------------ task builders
|
| 97 |
+
def build(self, task: str, text: list[int] | None = None, image=None, image2=None, video=None) -> Sample:
|
| 98 |
+
b = [self.bos]
|
| 99 |
+
if task == "t2i":
|
| 100 |
+
p = b + [self.task("t2i"), *text]
|
| 101 |
+
return self._s(p, self.image_tokens(image) + [self.eos])
|
| 102 |
+
if task == "i2t":
|
| 103 |
+
return self._s(b + [self.task("i2t"), *self.image_tokens(image)], [*text, self.eos])
|
| 104 |
+
if task == "i2i":
|
| 105 |
+
return self._s(b + [self.task("i2i"), *text, *self.image_tokens(image)], self.image_tokens(image2) + [self.eos])
|
| 106 |
+
if task == "t2v":
|
| 107 |
+
return self._s(b + [self.task("t2v"), *text], self.video_tokens(video) + [self.eos])
|
| 108 |
+
if task == "v2t":
|
| 109 |
+
return self._s(b + [self.task("v2t"), *self.video_tokens(video)], [*text, self.eos])
|
| 110 |
+
if task == "vcont":
|
| 111 |
+
return self._s(b + [self.task("vcont")], self.video_tokens(video) + [self.eos])
|
| 112 |
+
raise ValueError(f"unknown task {task}")
|
| 113 |
+
|
| 114 |
+
def chat(self, user: list[int], assistant: list[int]) -> Sample:
|
| 115 |
+
"""user/assistant are already-tokenized turn contents (text and/or media tokens)."""
|
| 116 |
+
return self._s([self.bos, self.user, *user, self.assistant], [*assistant, self.eos])
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def _s(prompt: list[int], target: list[int]) -> Sample:
|
| 120 |
+
return Sample(np.array(prompt + target, dtype=np.int32), len(prompt))
|
| 121 |
+
|
| 122 |
+
# ------------------------------------------------------------------ structure
|
| 123 |
+
def check_structure(self, tokens) -> None:
|
| 124 |
+
"""Validate media blocks: right number of codes, balanced tags, ordered timestamps."""
|
| 125 |
+
t = np.asarray(tokens).tolist()
|
| 126 |
+
G = self.tokens_per_frame
|
| 127 |
+
i, n = 0, len(t)
|
| 128 |
+
while i < n:
|
| 129 |
+
tok = t[i]
|
| 130 |
+
if tok == self.img_start:
|
| 131 |
+
body = t[i + 1 : i + 1 + G]
|
| 132 |
+
if len(body) != G or not self.is_visual(body).all() or i + 1 + G >= n or t[i + 1 + G] != self.img_end:
|
| 133 |
+
raise ValueError(f"malformed image block at {i}")
|
| 134 |
+
i += G + 2
|
| 135 |
+
elif tok == self.vid_start:
|
| 136 |
+
i += 1
|
| 137 |
+
k = None
|
| 138 |
+
while i < n and t[i] == self.frm_start:
|
| 139 |
+
ts = t[i + 1] - self.timestamp(0) if i + 1 < n else -1
|
| 140 |
+
if not (0 <= ts < MAX_FRAMES) or (k is not None and ts != k + 1):
|
| 141 |
+
raise ValueError(f"bad frame timestamp at {i + 1}")
|
| 142 |
+
k = ts
|
| 143 |
+
body = t[i + 2 : i + 2 + G]
|
| 144 |
+
if len(body) != G or not self.is_visual(body).all() or i + 2 + G >= n or t[i + 2 + G] != self.frm_end:
|
| 145 |
+
raise ValueError(f"malformed frame at {i}")
|
| 146 |
+
i += G + 3
|
| 147 |
+
if i >= n or t[i] != self.vid_end or k is None:
|
| 148 |
+
raise ValueError("video block not closed or empty")
|
| 149 |
+
i += 1
|
| 150 |
+
elif self.is_visual(tok) or tok in (self.img_end, self.vid_end, self.frm_start, self.frm_end):
|
| 151 |
+
raise ValueError(f"visual/structural token {tok} outside a media block at {i}")
|
| 152 |
+
else:
|
| 153 |
+
i += 1
|
| 154 |
+
|
| 155 |
+
def parse(self, tokens) -> list[tuple[str, object]]:
|
| 156 |
+
"""Split a generated sequence into ('text', ids) / ('image', codes[h,w]) / ('video', codes[T,h,w])."""
|
| 157 |
+
t = list(np.asarray(tokens).tolist())
|
| 158 |
+
G, (h, w) = self.tokens_per_frame, self.grid
|
| 159 |
+
out: list[tuple[str, object]] = []
|
| 160 |
+
txt: list[int] = []
|
| 161 |
+
i = 0
|
| 162 |
+
while i < len(t):
|
| 163 |
+
if t[i] == self.img_start and i + G < len(t):
|
| 164 |
+
out += [("text", txt)] if txt else []
|
| 165 |
+
txt = []
|
| 166 |
+
out.append(("image", (np.array(t[i + 1 : i + 1 + G]) - self.visual_offset).reshape(h, w)))
|
| 167 |
+
i += G + 2
|
| 168 |
+
elif t[i] == self.vid_start:
|
| 169 |
+
out += [("text", txt)] if txt else []
|
| 170 |
+
txt = []
|
| 171 |
+
frames = []
|
| 172 |
+
i += 1
|
| 173 |
+
while i + 2 + G <= len(t) and t[i] == self.frm_start:
|
| 174 |
+
frames.append((np.array(t[i + 2 : i + 2 + G]) - self.visual_offset).reshape(h, w))
|
| 175 |
+
i += G + 3
|
| 176 |
+
if frames:
|
| 177 |
+
out.append(("video", np.stack(frames)))
|
| 178 |
+
i += 1 if i < len(t) and t[i] == self.vid_end else 0
|
| 179 |
+
else:
|
| 180 |
+
if self.is_text(t[i]):
|
| 181 |
+
txt.append(t[i])
|
| 182 |
+
i += 1
|
| 183 |
+
return out + ([("text", txt)] if txt else [])
|
| 184 |
+
|
| 185 |
+
def to_dict(self) -> dict:
|
| 186 |
+
return {"text_size": self.text_size, "visual_size": self.visual_size, "grid": list(self.grid), "fps": self.fps,
|
| 187 |
+
"special_offset": self.special_offset, "visual_offset": self.visual_offset, "size": self.size, "specials": SPECIALS}
|
| 188 |
+
|
| 189 |
+
@classmethod
|
| 190 |
+
def from_dict(cls, d: dict) -> "UnifiedVocab":
|
| 191 |
+
v = cls(d["text_size"], d["visual_size"], tuple(d["grid"]), d.get("fps", 4))
|
| 192 |
+
assert v.to_dict()["specials"] == d.get("specials", SPECIALS), "special-token table changed since checkpoint"
|
| 193 |
+
return v
|
| 194 |
+
|
| 195 |
+
def save(self, path) -> None:
|
| 196 |
+
with open(path, "w") as f:
|
| 197 |
+
json.dump(self.to_dict(), f, indent=2)
|
quasar/tokenizers/multires_emu.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Isolated candidate codec adapter; not registered with the live v2 trainer.
|
| 2 |
+
|
| 3 |
+
Frozen full Emu3 weights support square 64/128 images. This changes shapes only;
|
| 4 |
+
packet grammar, spatial embedding migration and training admission are separate.
|
| 5 |
+
"""
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from .restricted_emu import RestrictedEmuCodec
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class MultiResolutionEmuCodec(RestrictedEmuCodec):
|
| 12 |
+
def __init__(self, path, device='cpu'):
|
| 13 |
+
super().__init__(path, device)
|
| 14 |
+
if self.config.get('codec') != 'emu3_full_32768' or self.codebook_size != 32768:
|
| 15 |
+
raise ValueError('Only the pinned full visual codebook is supported')
|
| 16 |
+
|
| 17 |
+
@torch.inference_mode()
|
| 18 |
+
def encode(self, images, batch=1):
|
| 19 |
+
images = np.asarray(images)
|
| 20 |
+
if images.dtype != np.uint8 or images.ndim < 4 or images.shape[-3:] not in ((64, 64, 3), (128, 128, 3)):
|
| 21 |
+
raise ValueError('Expected uint8 square 64px or 128px RGB batches')
|
| 22 |
+
if batch < 1 or images.size == 0:
|
| 23 |
+
raise ValueError('Empty image batch or invalid microbatch')
|
| 24 |
+
lead, resolution = images.shape[:-3], images.shape[-2]
|
| 25 |
+
flat, out = images.reshape(-1, resolution, resolution, 3), []
|
| 26 |
+
for i in range(0, len(flat), batch):
|
| 27 |
+
x = torch.tensor(flat[i:i + batch], device=self.selected.device).permute(0, 3, 1, 2).float() / 127.5 - 1
|
| 28 |
+
codes = self.full_to_local[self.model.encode(x)].cpu().numpy()
|
| 29 |
+
expected = (len(x), resolution // 8, resolution // 8)
|
| 30 |
+
if codes.shape != expected or codes.min() < 0 or codes.max() >= self.codebook_size:
|
| 31 |
+
raise ValueError('Codec produced unexpected shape or token range')
|
| 32 |
+
out.append(codes)
|
| 33 |
+
return np.concatenate(out).reshape(*lead, resolution // 8, resolution // 8)
|
| 34 |
+
|
| 35 |
+
@torch.inference_mode()
|
| 36 |
+
def decode(self, codes, batch=1):
|
| 37 |
+
codes = np.asarray(codes)
|
| 38 |
+
if (codes.ndim < 3 or codes.shape[-2:] not in ((8, 8), (16, 16)) or codes.dtype.kind not in 'iu'
|
| 39 |
+
or codes.size == 0 or batch < 1 or codes.min() < 0 or codes.max() >= self.codebook_size):
|
| 40 |
+
raise ValueError('Invalid visual codes or token grid')
|
| 41 |
+
lead, grid = codes.shape[:-2], codes.shape[-1]
|
| 42 |
+
flat, out = codes.reshape(-1, grid, grid), []
|
| 43 |
+
for i in range(0, len(flat), batch):
|
| 44 |
+
full = self.selected[torch.as_tensor(flat[i:i + batch], device=self.selected.device, dtype=torch.long)]
|
| 45 |
+
x = self.model.decode(full)
|
| 46 |
+
if x.shape != (len(full), 3, grid * 8, grid * 8):
|
| 47 |
+
raise ValueError('Codec decoder returned unexpected spatial shape')
|
| 48 |
+
out.append(((x.float().clamp(-1, 1) + 1) * 127.5).round().byte().permute(0, 2, 3, 1).cpu().numpy())
|
| 49 |
+
return np.concatenate(out).reshape(*lead, grid * 8, grid * 8, 3)
|
quasar/tokenizers/restricted_emu.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frozen pinned Emu3 pixel codec with a training-only 1,000-entry subset.
|
| 2 |
+
|
| 3 |
+
Quasar still predicts every image code. The frozen codec maps those codes back
|
| 4 |
+
to pixels. No pretrained generative backbone is loaded. Per-frame mode retains
|
| 5 |
+
the existing 64-code grammar and avoids changing the trained text vocabulary.
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import json
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
from safetensors.torch import load_file
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class RestrictedEmuCodec(nn.Module):
|
| 16 |
+
codebook_size = 1000
|
| 17 |
+
downsample = 8
|
| 18 |
+
|
| 19 |
+
def __init__(self, path, device='cpu'):
|
| 20 |
+
super().__init__()
|
| 21 |
+
from transformers import AutoModel
|
| 22 |
+
path = Path(path)
|
| 23 |
+
self.config = json.loads((path/'vq_config.json').read_text())
|
| 24 |
+
self.codebook_size = self.config['codebook_size']
|
| 25 |
+
base = Path(self.config['base_path'])
|
| 26 |
+
if not base.is_absolute():
|
| 27 |
+
base = (path / base).resolve()
|
| 28 |
+
self.model = AutoModel.from_pretrained(str(base), trust_remote_code=True,
|
| 29 |
+
local_files_only=True).to(device).eval()
|
| 30 |
+
self.model.requires_grad_(False)
|
| 31 |
+
mapping = load_file(str(path/'vq.safetensors'))
|
| 32 |
+
self.register_buffer('selected', mapping['selected'].to(device))
|
| 33 |
+
self.register_buffer('full_to_local', mapping['full_to_local'].to(device))
|
| 34 |
+
|
| 35 |
+
@torch.inference_mode()
|
| 36 |
+
def encode(self, images, batch=8):
|
| 37 |
+
images=np.asarray(images);lead=images.shape[:-3];flat=images.reshape(-1,*images.shape[-3:]);out=[]
|
| 38 |
+
for i in range(0,len(flat),batch):
|
| 39 |
+
x=torch.tensor(flat[i:i+batch],device=self.selected.device).permute(0,3,1,2).float()/127.5-1
|
| 40 |
+
full=self.model.encode(x)
|
| 41 |
+
out.append(self.full_to_local[full].cpu().numpy())
|
| 42 |
+
return np.concatenate(out).reshape(*lead,8,8)
|
| 43 |
+
|
| 44 |
+
@torch.inference_mode()
|
| 45 |
+
def decode(self, codes, batch=8):
|
| 46 |
+
codes=np.asarray(codes);lead=codes.shape[:-2];flat=codes.reshape(-1,8,8);out=[]
|
| 47 |
+
if codes.dtype.kind not in 'iu' or codes.min()<0 or codes.max()>=self.codebook_size:raise ValueError('invalid codec codes')
|
| 48 |
+
for i in range(0,len(flat),batch):
|
| 49 |
+
full=self.selected[torch.tensor(flat[i:i+batch],device=self.selected.device,dtype=torch.long)]
|
| 50 |
+
x=self.model.decode(full)
|
| 51 |
+
out.append(((x.float().clamp(-1,1)+1)*127.5).round().byte().permute(0,2,3,1).cpu().numpy())
|
| 52 |
+
return np.concatenate(out).reshape(*lead,64,64,3)
|
quasar/tokenizers/text.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Byte-level BPE text tokenizer (trained locally with HF `tokenizers`).
|
| 2 |
+
|
| 3 |
+
Byte-level means every UTF-8 string is encodable (no UNK). The tokenizer has no special
|
| 4 |
+
tokens of its own: structural tokens live in the unified vocabulary (multimodal.py), above the
|
| 5 |
+
text range, so text ids are exactly [0, vocab_size).
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Iterable
|
| 11 |
+
|
| 12 |
+
from tokenizers import Tokenizer, decoders, models, pre_tokenizers, trainers
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TextTokenizer:
|
| 16 |
+
def __init__(self, tok: Tokenizer):
|
| 17 |
+
self.tok = tok
|
| 18 |
+
|
| 19 |
+
@classmethod
|
| 20 |
+
def train(cls, texts: Iterable[str], vocab_size: int = 8192) -> "TextTokenizer":
|
| 21 |
+
tok = Tokenizer(models.BPE())
|
| 22 |
+
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
|
| 23 |
+
tok.decoder = decoders.ByteLevel()
|
| 24 |
+
trainer = trainers.BpeTrainer(vocab_size=vocab_size, initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), show_progress=False)
|
| 25 |
+
tok.train_from_iterator(texts, trainer)
|
| 26 |
+
return cls(tok)
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def vocab_size(self) -> int:
|
| 30 |
+
return self.tok.get_vocab_size()
|
| 31 |
+
|
| 32 |
+
def encode(self, text: str) -> list[int]:
|
| 33 |
+
return self.tok.encode(text).ids
|
| 34 |
+
|
| 35 |
+
def encode_batch(self, texts: list[str]) -> list[list[int]]:
|
| 36 |
+
return [e.ids for e in self.tok.encode_batch(texts)]
|
| 37 |
+
|
| 38 |
+
def decode(self, ids: Iterable[int]) -> str:
|
| 39 |
+
return self.tok.decode([i for i in ids if 0 <= i < self.vocab_size])
|
| 40 |
+
|
| 41 |
+
def save(self, path: str | Path) -> None:
|
| 42 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 43 |
+
self.tok.save(str(path))
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def load(cls, path: str | Path) -> "TextTokenizer":
|
| 47 |
+
return cls(Tokenizer.from_file(str(path)))
|