Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- codecs/gigatoken.py +179 -0
- pipeline/__init__.py +2 -0
- pipeline/__pycache__/__init__.cpython-313.pyc +0 -0
- pipeline/__pycache__/decoder.cpython-313.pyc +0 -0
- pipeline/__pycache__/encoder.cpython-313.pyc +0 -0
- pipeline/decoder.py +63 -0
- pipeline/encoder.py +105 -0
- playback/__init__.py +2 -0
- playback/__pycache__/__init__.cpython-313.pyc +0 -0
- playback/__pycache__/display.cpython-313.pyc +0 -0
- playback/__pycache__/engine.cpython-313.pyc +0 -0
- playback/display.py +68 -0
- playback/engine.py +105 -0
codecs/gigatoken.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from .base import VideoTokenizer
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class GigaTokenVideoCodec(VideoTokenizer):
|
| 7 |
+
"""Hierarchical vision tokenizer with large vocabulary and structured token types.
|
| 8 |
+
|
| 9 |
+
Rather than flat per-frame tokens (``[512 tokens for frame 187]``),
|
| 10 |
+
the stream encodes structured changes in a token hierarchy:
|
| 11 |
+
|
| 12 |
+
::
|
| 13 |
+
|
| 14 |
+
Layer 0 — Scene 4 tokens composition, lighting, environment
|
| 15 |
+
Layer 1 — Camera 8 tokens camera params, motion, cut boundaries
|
| 16 |
+
Layer 2 — Object 16 tokens object identities, positions, categories
|
| 17 |
+
Layer 3 — Motion 32 tokens temporal dynamics, optical flow
|
| 18 |
+
Layer 4 — Texture 128 tokens fine details, edges, surface patterns
|
| 19 |
+
Layer 5 — Residual 256 tokens reconstruction error from coarse layers
|
| 20 |
+
|
| 21 |
+
Scene and Camera tokens change rarely across consecutive frames, dramatically
|
| 22 |
+
reducing temporal redundancy compared to frame-by-frame encoding.
|
| 23 |
+
|
| 24 |
+
Each token is drawn from a vocabulary of ``vocab_size`` entries (default 262144).
|
| 25 |
+
Layers are independently decodable — a layer mask selects which layers to
|
| 26 |
+
reconstruct, enabling progressive quality scaling, semantic seeking, and
|
| 27 |
+
object-level editing directly in the compressed domain.
|
| 28 |
+
|
| 29 |
+
Backends:
|
| 30 |
+
"research" — random tokens matching the hierarchical layout (default)
|
| 31 |
+
"magvit2" — Open-MAGVIT2 262k-codebook visual tokenizer (XPU/CUDA)
|
| 32 |
+
"cosmos" — NVIDIA Cosmos Tokenizer (XPU/CUDA)
|
| 33 |
+
"""
|
| 34 |
+
LAYER_NAMES = ["Scene", "Camera", "Object", "Motion", "Texture", "Residual"]
|
| 35 |
+
|
| 36 |
+
def __init__(self, device: str = "cpu", vocab_size: int = 262144,
|
| 37 |
+
tokens_per_layer: list | None = None,
|
| 38 |
+
backend: str = "research"):
|
| 39 |
+
self.device = torch.device(device)
|
| 40 |
+
self._vocab_size = vocab_size
|
| 41 |
+
self._layers = 6
|
| 42 |
+
self._tpl = tokens_per_layer or [4, 8, 16, 32, 128, 256]
|
| 43 |
+
self._backend = backend
|
| 44 |
+
self._real_backend = None
|
| 45 |
+
self._loaded = False
|
| 46 |
+
|
| 47 |
+
def _lazy_load(self):
|
| 48 |
+
if self._loaded:
|
| 49 |
+
return
|
| 50 |
+
if self._backend == "magvit2":
|
| 51 |
+
self._real_backend = _Magvit2Backend(self.device)
|
| 52 |
+
elif self._backend == "cosmos":
|
| 53 |
+
self._real_backend = _CosmosBackend(self.device)
|
| 54 |
+
self._loaded = True
|
| 55 |
+
|
| 56 |
+
def encode(self, video: torch.Tensor) -> list[torch.Tensor]:
|
| 57 |
+
self._lazy_load()
|
| 58 |
+
if self._real_backend is not None:
|
| 59 |
+
return self._real_backend.encode(video)
|
| 60 |
+
B, C, T, H, W = video.shape
|
| 61 |
+
tokens = []
|
| 62 |
+
for layer in range(self._layers):
|
| 63 |
+
n = self._tpl[layer]
|
| 64 |
+
t = torch.randint(0, self._vocab_size, (B, T, n),
|
| 65 |
+
dtype=torch.int32, device=self.device)
|
| 66 |
+
tokens.append(t)
|
| 67 |
+
return tokens
|
| 68 |
+
|
| 69 |
+
def decode(self, tokens: list[torch.Tensor]) -> torch.Tensor:
|
| 70 |
+
self._lazy_load()
|
| 71 |
+
if self._real_backend is not None:
|
| 72 |
+
return self._real_backend.decode(tokens)
|
| 73 |
+
if not tokens:
|
| 74 |
+
B, T = 1, 0
|
| 75 |
+
elif tokens[0].dim() == 3:
|
| 76 |
+
B, T = tokens[0].shape[0], tokens[0].shape[1]
|
| 77 |
+
else:
|
| 78 |
+
B, T = 1, 1
|
| 79 |
+
H, W = 64, 64
|
| 80 |
+
out = torch.randn(B, 3, T, H, W, device=self.device)
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def num_layers(self) -> int:
|
| 85 |
+
return self._layers
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def layer_token_counts(self) -> list[int]:
|
| 89 |
+
return self._tpl
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def vocab_size(self) -> int:
|
| 93 |
+
return self._vocab_size
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def name(self) -> str:
|
| 97 |
+
return f"gigatoken-{self._backend}-v{self._vocab_size}"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class _CosmosBackend:
|
| 101 |
+
def __init__(self, device):
|
| 102 |
+
self.device = device
|
| 103 |
+
self._encoder = None
|
| 104 |
+
self._decoder = None
|
| 105 |
+
self._loaded = False
|
| 106 |
+
|
| 107 |
+
def _lazy_load(self):
|
| 108 |
+
if self._loaded:
|
| 109 |
+
return
|
| 110 |
+
from cosmos_tokenizer.video_lib import CausalVideoTokenizer
|
| 111 |
+
variant = "DV8x16x16"
|
| 112 |
+
ckpt_enc = f"pretrained_ckpts/Cosmos-0.1-Tokenizer-{variant}/encoder.jit"
|
| 113 |
+
ckpt_dec = f"pretrained_ckpts/Cosmos-0.1-Tokenizer-{variant}/decoder.jit"
|
| 114 |
+
self._encoder = CausalVideoTokenizer(checkpoint_enc=ckpt_enc).to(self.device)
|
| 115 |
+
self._decoder = CausalVideoTokenizer(checkpoint_dec=ckpt_dec).to(self.device)
|
| 116 |
+
self._encoder.eval()
|
| 117 |
+
self._decoder.eval()
|
| 118 |
+
self._loaded = True
|
| 119 |
+
|
| 120 |
+
@torch.inference_mode()
|
| 121 |
+
def encode(self, video):
|
| 122 |
+
self._lazy_load()
|
| 123 |
+
video = video.to(self.device)
|
| 124 |
+
(latent,) = self._encoder.encode(video)
|
| 125 |
+
tokens = latent.long() if latent.dtype in (torch.float16, torch.bfloat16, torch.float32) else latent
|
| 126 |
+
return [tokens]
|
| 127 |
+
|
| 128 |
+
@torch.inference_mode()
|
| 129 |
+
def decode(self, tokens):
|
| 130 |
+
self._lazy_load()
|
| 131 |
+
reconstructed = self._decoder.decode(tokens[0].to(self.device))
|
| 132 |
+
return reconstructed
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class _Magvit2Backend:
|
| 136 |
+
"""Open-MAGVIT2 visual tokenizer with 262k LFQ codebook.
|
| 137 |
+
|
| 138 |
+
Uses Lookup-Free Quantization to produce discrete visual tokens
|
| 139 |
+
directly — no continuous latents. The 262144-codebook variant
|
| 140 |
+
is competitive with next-generation codecs in human evaluations.
|
| 141 |
+
|
| 142 |
+
Pretrained models: ``TencentARC/Open-MAGVIT2-Tokenizer-262144-Video``
|
| 143 |
+
"""
|
| 144 |
+
def __init__(self, device, variant: str = "262144"):
|
| 145 |
+
self.device = device
|
| 146 |
+
self._variant = variant
|
| 147 |
+
self._model = None
|
| 148 |
+
self._loaded = False
|
| 149 |
+
|
| 150 |
+
def _lazy_load(self):
|
| 151 |
+
if self._loaded:
|
| 152 |
+
return
|
| 153 |
+
try:
|
| 154 |
+
from open_magvit2 import get_tokenizer
|
| 155 |
+
repo = f"TencentARC/Open-MAGVIT2-Tokenizer-{self._variant}-Video"
|
| 156 |
+
self._model = get_tokenizer(repo, device=str(self.device))
|
| 157 |
+
self._model.eval()
|
| 158 |
+
self._loaded = True
|
| 159 |
+
except ImportError:
|
| 160 |
+
raise ImportError(
|
| 161 |
+
"open_magvit2 not installed; try: pip install open-magvit2"
|
| 162 |
+
)
|
| 163 |
+
except Exception as e:
|
| 164 |
+
raise RuntimeError(f"failed to load Open-MAGVIT2: {e}")
|
| 165 |
+
|
| 166 |
+
@torch.inference_mode()
|
| 167 |
+
def encode(self, video):
|
| 168 |
+
self._lazy_load()
|
| 169 |
+
video = video.to(self.device)
|
| 170 |
+
tokens = self._model.encode(video)
|
| 171 |
+
if isinstance(tokens, (list, tuple)):
|
| 172 |
+
tokens = tokens[0]
|
| 173 |
+
return [tokens]
|
| 174 |
+
|
| 175 |
+
@torch.inference_mode()
|
| 176 |
+
def decode(self, tokens):
|
| 177 |
+
self._lazy_load()
|
| 178 |
+
recon = self._model.decode(tokens[0].to(self.device))
|
| 179 |
+
return recon
|
pipeline/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .decoder import DecoderPipeline
|
| 2 |
+
from .encoder import EncoderPipeline
|
pipeline/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (250 Bytes). View file
|
|
|
pipeline/__pycache__/decoder.cpython-313.pyc
ADDED
|
Binary file (4.99 kB). View file
|
|
|
pipeline/__pycache__/encoder.cpython-313.pyc
ADDED
|
Binary file (6.33 kB). View file
|
|
|
pipeline/decoder.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from mediatok.container.gtkv import GtkvReader, VideoTokenBlock, AudioTokenBlock
|
| 5 |
+
from mediatok.codecs.base import VideoTokenizer, AudioTokenizer
|
| 6 |
+
from mediatok.entropy import entropy_decode
|
| 7 |
+
from mediatok.container.gtkv import ENTROPY_CODEC_NAME_MAP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DecoderPipeline:
|
| 11 |
+
def __init__(self, reader: GtkvReader, video_codec: VideoTokenizer,
|
| 12 |
+
audio_codec: AudioTokenizer, device: str = "cpu"):
|
| 13 |
+
self.reader = reader
|
| 14 |
+
self.video_codec = video_codec
|
| 15 |
+
self.audio_codec = audio_codec
|
| 16 |
+
self.device = torch.device(device)
|
| 17 |
+
self.entropy_codec_id = reader.header.entropy_codec_id
|
| 18 |
+
self.layer_token_counts = reader.header.layer_token_counts
|
| 19 |
+
self.num_layers = reader.header.num_layers
|
| 20 |
+
|
| 21 |
+
def decode_chunk(self, chunk_index: int, layer_mask: int = 0b111111) -> torch.Tensor:
|
| 22 |
+
block = self.reader.read_video_block(chunk_index)
|
| 23 |
+
tokens = entropy_decode(block.entropy_payload, self.entropy_codec_id, block.token_count, bits=18)
|
| 24 |
+
|
| 25 |
+
layer_slices = []
|
| 26 |
+
offset = 0
|
| 27 |
+
for i in range(self.num_layers):
|
| 28 |
+
n = block.layer_sizes[i] if i < len(block.layer_sizes) and block.layer_sizes[i] > 0 else self.layer_token_counts[i]
|
| 29 |
+
if layer_mask & (1 << i):
|
| 30 |
+
layer_slices.append(tokens[offset:offset + n])
|
| 31 |
+
offset += n
|
| 32 |
+
|
| 33 |
+
frames_per_chunk = self.reader.header.chunk_size_frames or 1
|
| 34 |
+
layer_tensors = []
|
| 35 |
+
for i in range(self.num_layers):
|
| 36 |
+
n = block.layer_sizes[i] if i < len(block.layer_sizes) and block.layer_sizes[i] > 0 else 0
|
| 37 |
+
if n > 0 and (layer_mask & (1 << i)):
|
| 38 |
+
tpf = n // frames_per_chunk
|
| 39 |
+
arr = np.array(layer_slices.pop(0), dtype=np.int64)
|
| 40 |
+
layer_tensors.append(
|
| 41 |
+
torch.tensor(arr.reshape(1, frames_per_chunk, tpf), dtype=torch.int64, device=self.device)
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
frames = self.video_codec.decode(layer_tensors)
|
| 45 |
+
return frames
|
| 46 |
+
|
| 47 |
+
def decode_all(self, layer_mask: int = 0b111111) -> list[torch.Tensor]:
|
| 48 |
+
frames = []
|
| 49 |
+
for i in range(self.reader.num_chunks):
|
| 50 |
+
chunk = self.decode_chunk(i, layer_mask)
|
| 51 |
+
frames.append(chunk)
|
| 52 |
+
return frames
|
| 53 |
+
|
| 54 |
+
def decode_audio_chunk(self, chunk_index: int) -> torch.Tensor:
|
| 55 |
+
block = self.reader.read_audio_block(chunk_index)
|
| 56 |
+
if block is None:
|
| 57 |
+
return torch.empty(0, device=self.device)
|
| 58 |
+
tokens = entropy_decode(block.entropy_payload, self.entropy_codec_id,
|
| 59 |
+
block.codebook * block.frame_count, bits=32)
|
| 60 |
+
t = torch.tensor(tokens, dtype=torch.int64, device=self.device)
|
| 61 |
+
t = t.view(block.codebook, -1)
|
| 62 |
+
audio = self.audio_codec.decode(t)
|
| 63 |
+
return audio
|
pipeline/encoder.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import struct
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import soundfile as sf
|
| 7 |
+
|
| 8 |
+
from mediatok.container.gtkv import GtkvWriter, GtkvHeader, VideoTokenBlock, AudioTokenBlock
|
| 9 |
+
from mediatok.codecs.base import VideoTokenizer, AudioTokenizer
|
| 10 |
+
from mediatok.entropy import entropy_encode
|
| 11 |
+
from mediatok.container.gtkv import VIDEO_TOKENIZER_ID_MAP, AUDIO_TOKENIZER_ID_MAP, ENTROPY_CODEC_ID_MAP
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _choose_entropy_codec() -> int:
|
| 15 |
+
return 4
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class EncoderPipeline:
|
| 19 |
+
def __init__(self, video_codec: VideoTokenizer, audio_codec: AudioTokenizer,
|
| 20 |
+
chunk_size_frames: int = 128, entropy_codec: str = "rans"):
|
| 21 |
+
self.video_codec = video_codec
|
| 22 |
+
self.audio_codec = audio_codec
|
| 23 |
+
self.chunk_size_frames = chunk_size_frames
|
| 24 |
+
self.entropy_codec_id = ENTROPY_CODEC_ID_MAP.get(entropy_codec, 2)
|
| 25 |
+
|
| 26 |
+
def encode_file(self, video_path: str, audio_path: str, output_path: str,
|
| 27 |
+
width: int, height: int, fps: int = 30):
|
| 28 |
+
header = GtkvHeader(
|
| 29 |
+
video_tokenizer_id=VIDEO_TOKENIZER_ID_MAP.get(self.video_codec.name.split("-")[0], 0),
|
| 30 |
+
audio_tokenizer_id=AUDIO_TOKENIZER_ID_MAP.get(self.audio_codec.name.split("-")[0], 0),
|
| 31 |
+
width=width,
|
| 32 |
+
height=height,
|
| 33 |
+
fps=fps,
|
| 34 |
+
audio_sample_rate=self.audio_codec.sample_rate,
|
| 35 |
+
chunk_size_frames=self.chunk_size_frames,
|
| 36 |
+
entropy_codec_id=self.entropy_codec_id,
|
| 37 |
+
num_layers=self.video_codec.num_layers,
|
| 38 |
+
layer_token_counts=self.video_codec.layer_token_counts[:6],
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
with GtkvWriter(output_path, header) as writer:
|
| 42 |
+
self._encode_video_chunks(writer, video_path, header)
|
| 43 |
+
self._encode_audio_chunks(writer, audio_path, header)
|
| 44 |
+
|
| 45 |
+
def _encode_video_chunks(self, writer: GtkvWriter, video_path: str, header: GtkvHeader):
|
| 46 |
+
import numpy as np
|
| 47 |
+
import torch
|
| 48 |
+
|
| 49 |
+
n_frames = header.num_video_frames if header.num_video_frames else 0
|
| 50 |
+
if n_frames == 0:
|
| 51 |
+
return
|
| 52 |
+
|
| 53 |
+
for chunk_start in range(0, n_frames, self.chunk_size_frames):
|
| 54 |
+
chunk_end = min(chunk_start + self.chunk_size_frames, n_frames)
|
| 55 |
+
n = chunk_end - chunk_start
|
| 56 |
+
|
| 57 |
+
dummy_frames = torch.randn(1, 3, n, header.height, header.width)
|
| 58 |
+
tokens = self.video_codec.encode(dummy_frames)
|
| 59 |
+
|
| 60 |
+
flat_tokens = []
|
| 61 |
+
layer_sizes = [0] * 6
|
| 62 |
+
for i, t in enumerate(tokens):
|
| 63 |
+
t_np = t.cpu().numpy().ravel().astype(np.int32).tolist()
|
| 64 |
+
flat_tokens.extend(t_np)
|
| 65 |
+
if i < 6:
|
| 66 |
+
layer_sizes[i] = len(t_np)
|
| 67 |
+
|
| 68 |
+
entropy_payload = entropy_encode(flat_tokens, self.entropy_codec_id, bits=18)
|
| 69 |
+
|
| 70 |
+
block = VideoTokenBlock(
|
| 71 |
+
token_count=len(flat_tokens),
|
| 72 |
+
layer_sizes=layer_sizes,
|
| 73 |
+
tokens=flat_tokens,
|
| 74 |
+
entropy_payload=entropy_payload,
|
| 75 |
+
)
|
| 76 |
+
writer.write_chunk(block)
|
| 77 |
+
|
| 78 |
+
def _encode_audio_chunks(self, writer: GtkvWriter, audio_path: str, header: GtkvHeader):
|
| 79 |
+
import torch
|
| 80 |
+
import soundfile as sf
|
| 81 |
+
|
| 82 |
+
if not audio_path or not Path(audio_path).exists():
|
| 83 |
+
return
|
| 84 |
+
data, sr = sf.read(audio_path)
|
| 85 |
+
if sr != self.audio_codec.sample_rate:
|
| 86 |
+
import numpy as np
|
| 87 |
+
from scipy import signal
|
| 88 |
+
ratio = self.audio_codec.sample_rate / sr
|
| 89 |
+
new_len = int(len(data) * ratio)
|
| 90 |
+
data = signal.resample(data, new_len)
|
| 91 |
+
|
| 92 |
+
audio_t = torch.from_numpy(data).float()
|
| 93 |
+
if audio_t.dim() == 1:
|
| 94 |
+
audio_t = audio_t.unsqueeze(0)
|
| 95 |
+
|
| 96 |
+
tokens = self.audio_codec.encode(audio_t)
|
| 97 |
+
t_np = tokens.cpu().numpy().ravel().astype(np.int32).tolist()
|
| 98 |
+
payload = entropy_encode(t_np, self.entropy_codec_id, bits=32)
|
| 99 |
+
block = AudioTokenBlock(
|
| 100 |
+
codebook=self.audio_codec.num_codebooks,
|
| 101 |
+
frame_count=header.num_video_frames,
|
| 102 |
+
tokens=t_np,
|
| 103 |
+
entropy_payload=payload,
|
| 104 |
+
)
|
| 105 |
+
writer.write_chunk(block)
|
playback/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .engine import FrameQueue, Player
|
| 2 |
+
from .display import TkPlayer, tensor_to_image
|
playback/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (283 Bytes). View file
|
|
|
playback/__pycache__/display.cpython-313.pyc
ADDED
|
Binary file (4.45 kB). View file
|
|
|
playback/__pycache__/engine.cpython-313.pyc
ADDED
|
Binary file (7.13 kB). View file
|
|
|
playback/display.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def tensor_to_image(frame: torch.Tensor) -> Image.Image:
|
| 9 |
+
"""Convert [3, H, W] or [1, 3, H, W] float tensor to PIL Image."""
|
| 10 |
+
f = frame.detach().cpu().float()
|
| 11 |
+
if f.dim() == 4:
|
| 12 |
+
f = f.squeeze(0)
|
| 13 |
+
arr = f.permute(1, 2, 0).numpy()
|
| 14 |
+
arr = ((arr - arr.min()) / (arr.max() - arr.min() + 1e-8) * 255).astype(np.uint8)
|
| 15 |
+
return Image.fromarray(arr)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TkPlayer:
|
| 19 |
+
"""Simple tkinter video player.
|
| 20 |
+
|
| 21 |
+
Decodes chunks through the pipeline and displays frames in a
|
| 22 |
+
window at the source frame rate. Close the window to stop.
|
| 23 |
+
"""
|
| 24 |
+
def __init__(self, pipeline, layer_mask: int = 0b111111):
|
| 25 |
+
self.pipeline = pipeline
|
| 26 |
+
self.layer_mask = layer_mask
|
| 27 |
+
|
| 28 |
+
def play(self):
|
| 29 |
+
reader = self.pipeline.reader
|
| 30 |
+
fps = reader.header.fps or 30
|
| 31 |
+
frame_time = 1.0 / fps
|
| 32 |
+
|
| 33 |
+
frames = []
|
| 34 |
+
for chunk in self.pipeline.decode_all(layer_mask=self.layer_mask):
|
| 35 |
+
for t in range(chunk.shape[2]):
|
| 36 |
+
frames.append(chunk[:, :, t])
|
| 37 |
+
if not frames:
|
| 38 |
+
raise RuntimeError("no frames decoded")
|
| 39 |
+
|
| 40 |
+
import tkinter as tk
|
| 41 |
+
from PIL import ImageTk
|
| 42 |
+
|
| 43 |
+
root = tk.Tk()
|
| 44 |
+
root.title(f"MediaTok — {reader.path}")
|
| 45 |
+
label = tk.Label(root)
|
| 46 |
+
label.pack()
|
| 47 |
+
first = tensor_to_image(frames[0])
|
| 48 |
+
photo = ImageTk.PhotoImage(first)
|
| 49 |
+
label.configure(image=photo)
|
| 50 |
+
root.geometry(f"{first.width}x{first.height}")
|
| 51 |
+
|
| 52 |
+
state = {"index": 1, "photo": photo, "last": time.perf_counter()}
|
| 53 |
+
|
| 54 |
+
def advance():
|
| 55 |
+
if state["index"] >= len(frames):
|
| 56 |
+
root.destroy()
|
| 57 |
+
return
|
| 58 |
+
img = tensor_to_image(frames[state["index"]])
|
| 59 |
+
state["photo"] = ImageTk.PhotoImage(img)
|
| 60 |
+
label.configure(image=state["photo"])
|
| 61 |
+
state["index"] += 1
|
| 62 |
+
elapsed = time.perf_counter() - state["last"]
|
| 63 |
+
state["last"] = time.perf_counter()
|
| 64 |
+
delay = max(1, int((frame_time - elapsed) * 1000))
|
| 65 |
+
root.after(delay, advance)
|
| 66 |
+
|
| 67 |
+
root.after(0, advance)
|
| 68 |
+
root.mainloop()
|
playback/engine.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import threading
|
| 2 |
+
import time
|
| 3 |
+
from collections import deque
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from mediatok.pipeline.decoder import DecoderPipeline
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FrameQueue:
|
| 11 |
+
def __init__(self, maxsize: int = 4):
|
| 12 |
+
self._q: deque[torch.Tensor] = deque(maxlen=maxsize)
|
| 13 |
+
self._lock = threading.Lock()
|
| 14 |
+
self._cv = threading.Condition(self._lock)
|
| 15 |
+
self._closed = False
|
| 16 |
+
|
| 17 |
+
def put(self, frame: torch.Tensor):
|
| 18 |
+
with self._lock:
|
| 19 |
+
if self._closed:
|
| 20 |
+
return
|
| 21 |
+
if len(self._q) == self._q.maxlen:
|
| 22 |
+
self._q.popleft()
|
| 23 |
+
self._q.append(frame)
|
| 24 |
+
self._cv.notify()
|
| 25 |
+
|
| 26 |
+
def get(self, timeout: float = None) -> torch.Tensor:
|
| 27 |
+
with self._lock:
|
| 28 |
+
while not self._q and not self._closed:
|
| 29 |
+
if not self._cv.wait(timeout=timeout):
|
| 30 |
+
raise TimeoutError("frame queue timeout")
|
| 31 |
+
if self._closed and not self._q:
|
| 32 |
+
raise StopIteration()
|
| 33 |
+
return self._q.popleft()
|
| 34 |
+
|
| 35 |
+
def close(self):
|
| 36 |
+
with self._lock:
|
| 37 |
+
self._closed = True
|
| 38 |
+
self._cv.notify_all()
|
| 39 |
+
|
| 40 |
+
@property
|
| 41 |
+
def qsize(self) -> int:
|
| 42 |
+
with self._lock:
|
| 43 |
+
return len(self._q)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class Player:
|
| 47 |
+
def __init__(self, pipeline: DecoderPipeline, mode: str = "realtime",
|
| 48 |
+
layer_mask: int = 0b111111, queue_depth: int = 4):
|
| 49 |
+
self.pipeline = pipeline
|
| 50 |
+
self.mode = mode
|
| 51 |
+
self.layer_mask = layer_mask
|
| 52 |
+
self.queue = FrameQueue(maxsize=queue_depth)
|
| 53 |
+
self._decode_thread: threading.Thread = None
|
| 54 |
+
self._running = False
|
| 55 |
+
|
| 56 |
+
def start(self):
|
| 57 |
+
self._running = True
|
| 58 |
+
self._decode_thread = threading.Thread(target=self._decode_loop, daemon=True)
|
| 59 |
+
self._decode_thread.start()
|
| 60 |
+
|
| 61 |
+
def stop(self):
|
| 62 |
+
self._running = False
|
| 63 |
+
self.queue.close()
|
| 64 |
+
|
| 65 |
+
def _decode_loop(self):
|
| 66 |
+
if self.mode == "predecode":
|
| 67 |
+
all_frames = self.pipeline.decode_all(layer_mask=self.layer_mask)
|
| 68 |
+
for f in all_frames:
|
| 69 |
+
self.queue.put(f.cpu())
|
| 70 |
+
if not self._running:
|
| 71 |
+
break
|
| 72 |
+
elif self.mode == "progressive":
|
| 73 |
+
for i in range(self.pipeline.reader.num_chunks):
|
| 74 |
+
frame = self.pipeline.decode_chunk(i, layer_mask=self.layer_mask)
|
| 75 |
+
self.queue.put(frame.cpu())
|
| 76 |
+
if not self._running:
|
| 77 |
+
break
|
| 78 |
+
else:
|
| 79 |
+
fps = self.pipeline.reader.header.fps
|
| 80 |
+
frame_time = 1.0 / fps if fps > 0 else 0.033
|
| 81 |
+
for i in range(self.pipeline.reader.num_chunks):
|
| 82 |
+
t0 = time.perf_counter()
|
| 83 |
+
frame = self.pipeline.decode_chunk(i, layer_mask=self.layer_mask)
|
| 84 |
+
self.queue.put(frame.cpu())
|
| 85 |
+
elapsed = time.perf_counter() - t0
|
| 86 |
+
sleep_time = frame_time - elapsed
|
| 87 |
+
if sleep_time > 0:
|
| 88 |
+
time.sleep(sleep_time)
|
| 89 |
+
if not self._running:
|
| 90 |
+
break
|
| 91 |
+
self.queue.close()
|
| 92 |
+
|
| 93 |
+
def play(self):
|
| 94 |
+
self.start()
|
| 95 |
+
try:
|
| 96 |
+
while True:
|
| 97 |
+
frame = self.queue.get()
|
| 98 |
+
self._display_frame(frame)
|
| 99 |
+
except (StopIteration, TimeoutError):
|
| 100 |
+
pass
|
| 101 |
+
finally:
|
| 102 |
+
self.stop()
|
| 103 |
+
|
| 104 |
+
def _display_frame(self, frame: torch.Tensor):
|
| 105 |
+
pass
|