initium-deeplearning 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. initium/LNN_controller/cfc_backbone_parallel.py +265 -0
  2. initium/LNN_controller/cfc_controller.py +325 -0
  3. initium/LNN_controller/chained_controller.py +83 -0
  4. initium/LNN_controller/hybrid_controller.py +90 -0
  5. initium/MoE/README.md +1 -0
  6. initium/MoE/moe_layer.py +552 -0
  7. initium/__init__.py +1 -0
  8. initium/config/controller_config.py +86 -0
  9. initium/config/train_config.py +64 -0
  10. initium/core_training.py +2182 -0
  11. initium/data/__init__.py +0 -0
  12. initium/data/audio/__init__.py +0 -0
  13. initium/data/audio/audio_dataset.py +96 -0
  14. initium/data/base_dataset.py +75 -0
  15. initium/data/common/__init__.py +0 -0
  16. initium/data/common/chain_task.py +542 -0
  17. initium/data/common/digit_codec.py +85 -0
  18. initium/data/common/graph_io.py +123 -0
  19. initium/data/common/real_data.py +747 -0
  20. initium/data/dataset_registry.py +59 -0
  21. initium/data/graph_traversal/__init__.py +0 -0
  22. initium/data/graph_traversal/graph_traversal.py +746 -0
  23. initium/data/multimodal/__init__.py +0 -0
  24. initium/data/multimodal/multimodal_dataset.py +401 -0
  25. initium/data/text/__init__.py +0 -0
  26. initium/data/text/text_dataset.py +79 -0
  27. initium/data/video/__init__.py +0 -0
  28. initium/data/video/video_dataset.py +85 -0
  29. initium/inference/__init__.py +1 -0
  30. initium/inference/cache/__init__.py +1 -0
  31. initium/inference/cache/base_cache.py +112 -0
  32. initium/inference/cache/cache_config.py +21 -0
  33. initium/inference/cache/cache_registry.py +99 -0
  34. initium/inference/cache/cached_engine.py +264 -0
  35. initium/inference/cache/keys.py +77 -0
  36. initium/inference/cache/prefix_cache.py +59 -0
  37. initium/inference/cache/result_cache.py +57 -0
  38. initium/inference/cache/state_utils.py +74 -0
  39. initium/inference/cache/stores.py +167 -0
  40. initium/inference/capabilities.py +86 -0
  41. initium/inference/checkpoint_io.py +63 -0
  42. initium/inference/engine.py +88 -0
  43. initium/inference/inference_config.py +22 -0
  44. initium/inference/metrics.py +103 -0
  45. initium/inference/model_loader.py +270 -0
  46. initium/inference/run_inference.py +526 -0
  47. initium/inference/run_logging.py +173 -0
  48. initium/inference/tasks/__init__.py +1 -0
  49. initium/inference/tasks/audio_task.py +114 -0
  50. initium/inference/tasks/base_task.py +60 -0
  51. initium/inference/tasks/graph_traversal_task.py +235 -0
  52. initium/inference/tasks/multimodal_task.py +74 -0
  53. initium/inference/tasks/task_registry.py +68 -0
  54. initium/inference/tasks/text_task.py +114 -0
  55. initium/inference/tasks/video_task.py +114 -0
  56. initium/mamba_controller/mamba2_controller.py +433 -0
  57. initium/mamba_controller/mamba3_controller.py +242 -0
  58. initium/mamba_controller/mamba_backbone_parallel.py +291 -0
  59. initium/mamba_controller/mamba_controller.py +983 -0
  60. initium/mamba_controller/split_graph_dnc.py +623 -0
  61. initium/memory_manipulation/dynamic_memory_resize.py +157 -0
  62. initium/memory_manipulation/dynamic_n_controller.py +152 -0
  63. initium/memory_manipulation/link_matrix_ablation.py +130 -0
  64. initium/memory_manipulation/nvrtc_compat.py +143 -0
  65. initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
  66. initium/setup_wheels.py +175 -0
  67. initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
  68. initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
  69. initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
  70. initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
  71. initium_deeplearning-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,747 @@
1
+ """
2
+ file: data/common/real_data.py
3
+
4
+ Turns a modality-specific raw source (a text corpus, an audio file, a video
5
+ file) into the ONE thing every data/common/chain_task.py KVChainDataset
6
+ needs: a pool of (key, value) integer-label facts, split into a TRAIN pool
7
+ (sampled from during curriculum training, replacing the synthetic
8
+ rng.sample(range(label_range), ...) draw) and a TEST pool (disjoint keys,
9
+ never seen during training - the modality's analogue of the graph
10
+ dataset's held-out London Underground graph; see graph_io.py for that one,
11
+ which is edge-structured rather than token-structured so it doesn't fit
12
+ this module's pipeline).
13
+
14
+ Pipeline, same for every modality (this is deliberate - see the "reduce
15
+ every modality to an integer token stream first" idea from the research
16
+ notes):
17
+
18
+ raw source -> per-unit integer token id in [0, label_range)
19
+ -> consecutive-pair (bigram) counts
20
+ -> most-common value per key -> fact pool
21
+ -> disjoint train/test key split
22
+
23
+ What differs per modality is only the FIRST step (raw source -> token
24
+ stream): BPE byte-pair merges for text, quantized spectrogram-frame bins
25
+ for audio, quantized downsampled-frame intensities for video. Each
26
+ modality's dataset module (text_dataset.py / audio_dataset.py /
27
+ video_dataset.py) calls its own `*_token_stream()` function below and then
28
+ the two shared helpers (`build_kv_pool_from_tokens`, `split_train_test_facts`).
29
+
30
+ None of this is meant to be a state-of-the-art tokenizer/codec - it's the
31
+ minimum real-data pipeline that plugs into the existing digit-coded KV
32
+ chain-task machinery unmodified. Swapping in a production BPE
33
+ implementation, a real mel-spectrogram front end, or a real video encoder
34
+ later only means replacing the token-stream function; build_kv_pool_from_tokens
35
+ / split_train_test_facts and everything downstream (chain_task.py,
36
+ training, evaluation) stays the same.
37
+
38
+ Note: audio real-data loading now streams (see _iter_waveform_chunks /
39
+ audio_token_stream_chunks / build_audio_kv_pool below) -- the whole-file
40
+ torch.stft call this used to require was the actual cause of OOMs on
41
+ multi-hour sources (a 2-hour recording's single torch.stft call alone was
42
+ several GB); it is now computed chunk_seconds at a time instead.
43
+ audio_token_stream() (the original whole-file function) is kept as-is for
44
+ small files / direct use, but build_audio_kv_pool() no longer calls it.
45
+ Decoding goes through the `ffmpeg`/`ffprobe` CLI directly rather than
46
+ torchaudio's I/O backend, whose legacy info()/load() API has been removed
47
+ or changed across torchaudio releases -- see _require_ffmpeg()'s docstring.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import glob as _glob
53
+ import os
54
+ import random
55
+ from collections import Counter
56
+
57
+ LABEL_RANGE = 1000
58
+
59
+
60
+ def resolve_link_paths(link) -> list[str]:
61
+ """Expand a dataset link into a sorted, de-duplicated list of one or
62
+ more file paths. Accepts: a single file path; a directory (every
63
+ regular file directly inside it is used -- e.g. "/data/audio_clips" to
64
+ pull in a whole folder); a glob pattern (e.g. "/data/audio_clips/*.wav");
65
+ or several of these '+'-joined in one string (e.g.
66
+ "clip_a.wav+clip_b.wav"), or an actual list/tuple of specs.
67
+
68
+ Each resolved path is treated as an independent SOURCE by the
69
+ build_*_kv_pool() functions below: its own token stream/bigram counts
70
+ are folded into the SAME shared pool, but never stitched across a file
71
+ boundary the way within-file chunking is (see _accumulate_bigram_chunk)
72
+ -- two unrelated files (e.g. 5 audio clips with different frequency
73
+ content) have no real adjacency between them, so treating their
74
+ boundary as a genuine bigram would inject spurious facts. This is the
75
+ single place multi-file/"pass a whole folder" support lives; every
76
+ caller (text/audio/video real-data pools, and data/common/graph_io.py's
77
+ multi-file edge loading) goes through this function, so no per-modality
78
+ globbing logic is duplicated.
79
+
80
+ Sorted (not glob's arbitrary OS order) so the resulting pool is
81
+ reproducible across runs/machines for the same directory/pattern.
82
+ """
83
+ if isinstance(link, list | tuple):
84
+ specs = list(link)
85
+ else:
86
+ specs = str(link).split("+")
87
+
88
+ paths: list[str] = []
89
+ for spec in specs:
90
+ spec = spec.strip()
91
+ if not spec:
92
+ continue
93
+ if any(ch in spec for ch in "*?["):
94
+ matches = sorted(p for p in _glob.glob(spec) if os.path.isfile(p))
95
+ if not matches:
96
+ raise FileNotFoundError(f"resolve_link_paths: glob {spec!r} matched no files")
97
+ paths.extend(matches)
98
+ elif os.path.isdir(spec):
99
+ matches = sorted(p for p in _glob.glob(os.path.join(spec, "*")) if os.path.isfile(p))
100
+ if not matches:
101
+ raise FileNotFoundError(f"resolve_link_paths: directory {spec!r} contains no files")
102
+ paths.extend(matches)
103
+ elif os.path.isfile(spec):
104
+ paths.append(spec)
105
+ else:
106
+ raise FileNotFoundError(
107
+ f"resolve_link_paths: {spec!r} is not a file, directory, or glob match"
108
+ )
109
+
110
+ seen, deduped = set(), []
111
+ for p in paths:
112
+ if p not in seen:
113
+ seen.add(p)
114
+ deduped.append(p)
115
+ if not deduped:
116
+ raise FileNotFoundError(f"resolve_link_paths: {link!r} resolved to no files")
117
+ return deduped
118
+
119
+
120
+ import numpy as np
121
+
122
+ try:
123
+ from tokenizers import Tokenizer as _HFTokenizer
124
+ from tokenizers.models import BPE as _HFBPEModel
125
+ from tokenizers.pre_tokenizers import ByteLevel as _HFByteLevel
126
+ from tokenizers.trainers import BpeTrainer as _HFBpeTrainer
127
+
128
+ _HF_TOKENIZERS_AVAILABLE = True
129
+ except ImportError:
130
+ _HF_TOKENIZERS_AVAILABLE = False
131
+
132
+ # BPE trainer is O(corpus_len * merges) in pure python -- fine for a
133
+ # "reasonable synthetic-real" corpus, not meant for GB-scale text. Cap the
134
+ # training sample so a huge file doesn't hang the training script's startup.
135
+ _BPE_TRAIN_CHARS_CAP = 300_000
136
+
137
+
138
+ # ==========================================================================
139
+ # text: byte-level BPE -> integer token stream
140
+ # ==========================================================================
141
+ class BPETokenizer:
142
+ """Minimal byte-level BPE (Sennrich et al. 2016). Vocab starts as the
143
+ 256 raw bytes and merges the most frequent adjacent pair repeatedly
144
+ until `vocab_size` is reached. `vocab_size` defaults to LABEL_RANGE so
145
+ every emitted token id already fits DigitCodec's label_range=1000
146
+ without any further hashing/modulo -- the one thing this module's
147
+ callers rely on.
148
+ """
149
+
150
+ def __init__(self, vocab_size: int = LABEL_RANGE):
151
+ if vocab_size <= 256:
152
+ raise ValueError(
153
+ f"BPETokenizer: vocab_size must be > 256 (raw bytes), got {vocab_size}"
154
+ )
155
+ self.vocab_size = vocab_size
156
+ self.merges: list[
157
+ tuple[int, int]
158
+ ] = [] # ordered (a, b) merges, applied in order at encode time
159
+
160
+ def train(self, text: str) -> BPETokenizer:
161
+ data = text.encode("utf-8", errors="ignore")[:_BPE_TRAIN_CHARS_CAP]
162
+ tokens = list(data) # ids 0..255
163
+ next_id = 256
164
+ while next_id < self.vocab_size and len(tokens) > 1:
165
+ pairs = Counter(zip(tokens, tokens[1:]))
166
+ if not pairs:
167
+ break
168
+ (a, b), count = pairs.most_common(1)[0]
169
+ if count < 2:
170
+ break
171
+ merged = next_id
172
+ new_tokens, i = [], 0
173
+ while i < len(tokens):
174
+ if i < len(tokens) - 1 and tokens[i] == a and tokens[i + 1] == b:
175
+ new_tokens.append(merged)
176
+ i += 2
177
+ else:
178
+ new_tokens.append(tokens[i])
179
+ i += 1
180
+ tokens = new_tokens
181
+ self.merges.append((a, b))
182
+ next_id += 1
183
+ return self
184
+
185
+ def encode(self, text: str) -> list[int]:
186
+ tokens = list(text.encode("utf-8", errors="ignore"))
187
+ next_id = 256
188
+ for a, b in self.merges:
189
+ merged = next_id
190
+ new_tokens, i = [], 0
191
+ while i < len(tokens):
192
+ if i < len(tokens) - 1 and tokens[i] == a and tokens[i + 1] == b:
193
+ new_tokens.append(merged)
194
+ i += 2
195
+ else:
196
+ new_tokens.append(tokens[i])
197
+ i += 1
198
+ tokens = new_tokens
199
+ next_id += 1
200
+ return tokens
201
+
202
+
203
+ _TEXT_STREAM_CHUNK_CHARS = 1_000_000 # read/encode in ~1M-char chunks -- the corpus is
204
+ # never materialized whole as one Python str
205
+
206
+
207
+ def _iter_text_chunks(path: str, chunk_chars: int = _TEXT_STREAM_CHUNK_CHARS):
208
+ """Yield the file's text in fixed-size chunks so callers never hold more
209
+ than `chunk_chars` characters at once (the fix for f.read() loading a
210
+ multi-GB corpus into one Python str)."""
211
+ with open(path, encoding="utf-8", errors="ignore") as f:
212
+ while True:
213
+ chunk = f.read(chunk_chars)
214
+ if not chunk:
215
+ return
216
+ yield chunk
217
+
218
+
219
+ def _text_token_stream_hf(path: str, vocab_size: int):
220
+ """Rust-backed BPE (Sennrich et al. 2016) via the `tokenizers` package.
221
+ Trains directly from the file on disk (tokenizers' own file-based
222
+ train() reads it itself, rather than requiring the corpus in memory
223
+ first) and encodes the corpus in fixed-size chunks -- neither step
224
+ materializes the whole file as one Python str or one Python list of
225
+ boxed ints, which is what made the full 2GB TinyStories corpus OOM
226
+ before. Returns a numpy int64 array, not a Python list, for the same
227
+ memory reason on the output side.
228
+ """
229
+ tok = _HFTokenizer(_HFBPEModel(unk_token=None))
230
+ tok.pre_tokenizer = _HFByteLevel(add_prefix_space=False)
231
+ trainer = _HFBpeTrainer(vocab_size=vocab_size, min_frequency=2, show_progress=False)
232
+ tok.train([path], trainer=trainer) # streams the file itself; no full-corpus string needed
233
+
234
+ chunks = [
235
+ np.asarray(tok.encode(chunk_text).ids, dtype=np.int64)
236
+ for chunk_text in _iter_text_chunks(path)
237
+ ]
238
+ ids = np.concatenate(chunks) if chunks else np.empty(0, dtype=np.int64)
239
+
240
+ # ByteLevel BPE's vocabulary is capped at vocab_size by the trainer
241
+ # (alphabet + merges), so ids should already be in [0, vocab_size) --
242
+ # verify instead of silently folding out-of-range ids with modulo,
243
+ # which can collide two genuinely distinct tokens into the same id.
244
+ bad = (ids < 0) | (ids >= vocab_size)
245
+ if bad.any():
246
+ raise ValueError(
247
+ f"_text_token_stream_hf: {int(bad.sum())} token id(s) fell outside "
248
+ f"[0, {vocab_size}) -- tokenizer vocab is misconfigured; fix the "
249
+ "trainer's vocab_size instead of folding ids with modulo."
250
+ )
251
+ return ids
252
+
253
+
254
+ def _text_token_stream_hf_chunks(path: str, vocab_size: int):
255
+ """Same train step as _text_token_stream_hf, but YIELDS each encoded
256
+ chunk instead of concatenating every chunk into one corpus-length array
257
+ -- this is the actual fix for the 2GB-corpus OOM (see module docstring):
258
+ the old function's np.concatenate materialized the whole token stream
259
+ before build_kv_pool_from_tokens ever ran, on top of THAT function's own
260
+ 3-4x blowup (now also fixed, see build_kv_pool_from_tokens)."""
261
+ tok = _HFTokenizer(_HFBPEModel(unk_token=None))
262
+ tok.pre_tokenizer = _HFByteLevel(add_prefix_space=False)
263
+ trainer = _HFBpeTrainer(vocab_size=vocab_size, min_frequency=2, show_progress=False)
264
+ tok.train([path], trainer=trainer) # streams the file itself; no full-corpus string needed
265
+
266
+ for chunk_text in _iter_text_chunks(path):
267
+ ids = np.asarray(tok.encode(chunk_text).ids, dtype=np.int64)
268
+ bad = (ids < 0) | (ids >= vocab_size)
269
+ if bad.any():
270
+ raise ValueError(
271
+ f"_text_token_stream_hf_chunks: {int(bad.sum())} token id(s) fell outside "
272
+ f"[0, {vocab_size}) -- tokenizer vocab is misconfigured; fix the "
273
+ "trainer's vocab_size instead of folding ids with modulo."
274
+ )
275
+ yield ids
276
+
277
+
278
+ def text_token_stream(path: str, vocab_size: int = LABEL_RANGE) -> list[int]:
279
+ if _HF_TOKENIZERS_AVAILABLE:
280
+ with open(path, encoding="utf-8", errors="ignore") as f:
281
+ if not f.read(1):
282
+ raise ValueError(f"{path}: empty text file")
283
+ return _text_token_stream_hf(path, vocab_size)
284
+ # Fallback pure-python tokenizer only: still needs the full text in
285
+ # memory (BPETokenizer.train() itself caps at _BPE_TRAIN_CHARS_CAP).
286
+ with open(path, encoding="utf-8", errors="ignore") as f:
287
+ text = f.read()
288
+ if not text.strip():
289
+ raise ValueError(f"{path}: empty text file")
290
+ tok = BPETokenizer(vocab_size=vocab_size).train(text)
291
+ return tok.encode(text)
292
+
293
+
294
+ def build_text_kv_pool(path, label_range: int = LABEL_RANGE) -> list[tuple[int, int]]:
295
+ """Streaming AND multi-file: `path` may be a single text file, or a
296
+ directory / glob pattern / '+'-joined list of these (see
297
+ resolve_link_paths) -- e.g. an entire folder of text files, each
298
+ contributing its own bigram-derived facts to one shared pool. Each
299
+ resolved file is trained/encoded independently (the HF path streams +
300
+ trains its own BPE vocabulary per file rather than assuming unrelated
301
+ files share one byte-pair vocabulary) and folded into the same fixed
302
+ count matrix; a chunk's cross-boundary bigram is only ever stitched
303
+ WITHIN one file (see _accumulate_bigram_chunk), never across files.
304
+ Only the `tokenizers`-unavailable fallback still fully materializes
305
+ each file in memory (BPETokenizer.train() itself caps at
306
+ _BPE_TRAIN_CHARS_CAP; see text_token_stream's own docstring)."""
307
+ paths = resolve_link_paths(path)
308
+ matrix = np.zeros(label_range * label_range, dtype=np.int64)
309
+ for p in paths:
310
+ if _HF_TOKENIZERS_AVAILABLE:
311
+ with open(p, encoding="utf-8", errors="ignore") as f:
312
+ if not f.read(1):
313
+ raise ValueError(f"{p}: empty text file")
314
+ prev_last = None
315
+ for chunk in _text_token_stream_hf_chunks(p, label_range):
316
+ prev_last = _accumulate_bigram_chunk(matrix, chunk, label_range, prev_last)
317
+ else:
318
+ tokens = np.asarray(text_token_stream(p, label_range), dtype=np.int64)
319
+ _accumulate_bigram_chunk(matrix, tokens, label_range, None)
320
+ return _pool_from_bigram_matrix(matrix, label_range)
321
+
322
+
323
+ # ==========================================================================
324
+ # audio: waveform -> quantized spectrogram-frame token stream
325
+ # ==========================================================================
326
+ # Decoded via the `ffmpeg`/`ffprobe` CLI directly, not torchaudio's I/O
327
+ # backend. torchaudio's legacy info()/load() API has been unstable across
328
+ # releases (e.g. `torchaudio.info` was removed outright in some newer
329
+ # versions -- the exact failure that motivated this switch), and
330
+ # `soundfile` (a common fallback) can't demux mp4/AAC video containers at
331
+ # all, which is exactly the "audio track embedded in a video file" case
332
+ # this pipeline needs. ffmpeg handles any container/codec natively, and
333
+ # Colab ships it preinstalled.
334
+ def _require_ffmpeg() -> None:
335
+ import shutil
336
+
337
+ missing = [exe for exe in ("ffmpeg", "ffprobe") if shutil.which(exe) is None]
338
+ if missing:
339
+ raise RuntimeError(
340
+ f"audio real-data loading requires the `ffmpeg`/`ffprobe` CLI tools on PATH "
341
+ f"(missing: {missing}). Google Colab has both preinstalled; elsewhere "
342
+ "`apt-get install ffmpeg` (Debian/Ubuntu) or `conda install ffmpeg` provides both."
343
+ )
344
+
345
+
346
+ def _ffmpeg_audio_info(path: str) -> int:
347
+ """-> sample_rate of the first audio stream in `path`, via ffprobe.
348
+ Works on any container ffmpeg can demux (wav/mp3/flac/... as well as a
349
+ video file's embedded audio track, e.g. mp4/mkv)."""
350
+ import json as _json
351
+ import subprocess
352
+
353
+ _require_ffmpeg()
354
+ cmd = [
355
+ "ffprobe",
356
+ "-v",
357
+ "error",
358
+ "-select_streams",
359
+ "a:0",
360
+ "-show_entries",
361
+ "stream=sample_rate",
362
+ "-of",
363
+ "json",
364
+ path,
365
+ ]
366
+ try:
367
+ out = subprocess.run(cmd, capture_output=True, text=True, check=True)
368
+ except subprocess.CalledProcessError as e:
369
+ raise RuntimeError(f"ffprobe failed on {path!r}: {e.stderr.strip()}") from e
370
+ streams = (_json.loads(out.stdout) or {}).get("streams") or []
371
+ if not streams or not streams[0].get("sample_rate"):
372
+ raise ValueError(f"{path}: no audio stream found (or missing sample_rate)")
373
+ return int(streams[0]["sample_rate"])
374
+
375
+
376
+ def _ffmpeg_decode_mono_f32(path: str, sample_rate: int):
377
+ """Starts an ffmpeg subprocess decoding `path` to mono float32 PCM at
378
+ `sample_rate`, streamed on stdout. `-v error` keeps stderr essentially
379
+ silent in the normal case, which matters for the streaming caller
380
+ below: it only drains stderr after stdout is exhausted, and a chatty
381
+ stderr could otherwise deadlock a long-running decode by filling that
382
+ pipe's OS buffer. Caller must read stdout to EOF and check the return
383
+ code (see _load_waveform / _iter_waveform_chunks)."""
384
+ import subprocess
385
+
386
+ cmd = [
387
+ "ffmpeg",
388
+ "-v",
389
+ "error",
390
+ "-i",
391
+ path,
392
+ "-vn",
393
+ "-ac",
394
+ "1",
395
+ "-ar",
396
+ str(sample_rate),
397
+ "-f",
398
+ "f32le",
399
+ "-",
400
+ ]
401
+ return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
402
+
403
+
404
+ def _load_waveform(path: str):
405
+ """Decodes the WHOLE file to mono float32 PCM via ffmpeg. Used only by
406
+ audio_token_stream() below, the original whole-file function kept for
407
+ small files / direct use -- build_audio_kv_pool() uses the streaming
408
+ _iter_waveform_chunks() instead and never materializes a whole long
409
+ recording in memory (see that function's docstring)."""
410
+ import numpy as _np
411
+ import torch
412
+
413
+ sr = _ffmpeg_audio_info(path)
414
+ proc = _ffmpeg_decode_mono_f32(path, sr)
415
+ raw, stderr = proc.communicate()
416
+ if proc.returncode != 0:
417
+ raise RuntimeError(
418
+ f"ffmpeg failed decoding {path!r}: {stderr.decode(errors='ignore')[-2000:]}"
419
+ )
420
+ usable = len(raw) - (len(raw) % 4)
421
+ arr = _np.frombuffer(raw[:usable], dtype=_np.float32).copy()
422
+ return torch.from_numpy(arr), sr
423
+
424
+
425
+ def audio_token_stream(
426
+ path: str, label_range: int = LABEL_RANGE, n_fft: int = 400, hop_length: int = 160
427
+ ) -> list[int]:
428
+ """Each STFT frame's magnitude spectrum is collapsed to a single integer
429
+ id in [0, label_range) via its dominant-frequency-bin index, rescaled
430
+ into the label range. This is a coarse "spectral shape" codebook, not a
431
+ real acoustic-unit tokenizer -- adequate to build a real, non-synthetic
432
+ fact stream out of an actual audio file."""
433
+ import torch
434
+
435
+ waveform, _sr = _load_waveform(path)
436
+ spec = torch.stft(
437
+ waveform,
438
+ n_fft=n_fft,
439
+ hop_length=hop_length,
440
+ window=torch.hann_window(n_fft),
441
+ return_complex=True,
442
+ )
443
+ mag = spec.abs() # (freq_bins, n_frames)
444
+ freq_bins = mag.shape[0]
445
+ dominant_bin = mag.argmax(dim=0) # (n_frames,) in [0, freq_bins)
446
+ scaled = (dominant_bin.float() / max(freq_bins - 1, 1) * (label_range - 1)).round().long()
447
+ return scaled.clamp(0, label_range - 1).tolist()
448
+
449
+
450
+ _AUDIO_CHUNK_SECONDS = 30.0 # ~1.3M samples/chunk at 44.1kHz -> a stft matrix of tens of MB,
451
+ # not the multi-GB single-shot matrix a multi-hour file used to need
452
+
453
+
454
+ def _iter_waveform_chunks(path: str, chunk_seconds: float = _AUDIO_CHUNK_SECONDS):
455
+ """Streams mono float32 PCM straight from an ffmpeg subprocess pipe, in
456
+ chunk_seconds-sized pieces, so a long recording (or a video file's
457
+ audio track) is never materialized whole in memory -- the actual OOM
458
+ fix for multi-hour sources (see module docstring). Reads directly off
459
+ the pipe rather than seeking per-chunk on disk, so this is also a
460
+ single continuous decode, not one ffmpeg invocation per chunk. Yields
461
+ (mono_waveform_chunk: torch.Tensor, sample_rate)."""
462
+ import numpy as _np
463
+ import torch
464
+
465
+ sr = _ffmpeg_audio_info(path)
466
+ chunk_frames = max(1, int(chunk_seconds * sr))
467
+ bytes_per_frame = 4 # f32le, already downmixed to mono by -ac 1
468
+ chunk_bytes = chunk_frames * bytes_per_frame
469
+ proc = _ffmpeg_decode_mono_f32(path, sr)
470
+ try:
471
+ while True:
472
+ raw = proc.stdout.read(chunk_bytes) # BufferedReader.read(n) blocks for n bytes or EOF
473
+ if not raw:
474
+ break
475
+ usable = len(raw) - (len(raw) % bytes_per_frame)
476
+ if usable <= 0:
477
+ break
478
+ arr = _np.frombuffer(raw[:usable], dtype=_np.float32).copy()
479
+ yield torch.from_numpy(arr), sr
480
+ finally:
481
+ proc.stdout.close()
482
+ stderr = proc.stderr.read()
483
+ proc.stderr.close()
484
+ ret = proc.wait()
485
+ if ret != 0:
486
+ raise RuntimeError(
487
+ f"ffmpeg failed decoding {path!r} (exit {ret}): "
488
+ f"{stderr.decode(errors='ignore')[-2000:]}"
489
+ )
490
+
491
+
492
+ def audio_token_stream_chunks(
493
+ path: str,
494
+ label_range: int = LABEL_RANGE,
495
+ n_fft: int = 400,
496
+ hop_length: int = 160,
497
+ chunk_seconds: float = _AUDIO_CHUNK_SECONDS,
498
+ ):
499
+ """Streaming replacement for audio_token_stream(): computes the STFT
500
+ chunk_seconds worth of audio at a time instead of loading the whole
501
+ waveform and running one torch.stft call over it. A small per-chunk
502
+ sample overlap (n_fft - hop_length) is carried over between chunks so
503
+ STFT windows spanning a chunk boundary are still computed, and the
504
+ handful of frames derived purely from that carried-over overlap are
505
+ dropped from every chunk after the first, so tokens are not
506
+ double-counted at chunk boundaries. Uses center=False (unlike the
507
+ whole-file audio_token_stream's default-centered torch.stft) so chunk
508
+ boundaries are unambiguous; this shifts frame alignment by at most one
509
+ window and has no effect on the coarse dominant-frequency-bin codebook
510
+ this pipeline builds. Yields one int64 numpy array of token ids per
511
+ chunk (a very short final chunk may yield nothing)."""
512
+ import torch
513
+
514
+ overlap = max(0, n_fft - hop_length)
515
+ carry = None
516
+ first = True
517
+ for wf, _sr in _iter_waveform_chunks(path, chunk_seconds=chunk_seconds):
518
+ if carry is not None:
519
+ wf = torch.cat([carry, wf])
520
+ if wf.shape[0] < n_fft:
521
+ carry = wf # too short to STFT yet -- fold into the next chunk instead
522
+ continue
523
+ carry = wf[-overlap:].clone() if overlap > 0 else None
524
+ spec = torch.stft(
525
+ wf,
526
+ n_fft=n_fft,
527
+ hop_length=hop_length,
528
+ window=torch.hann_window(n_fft),
529
+ center=False,
530
+ return_complex=True,
531
+ )
532
+ mag = spec.abs() # (freq_bins, n_frames)
533
+ freq_bins = mag.shape[0]
534
+ dominant_bin = mag.argmax(dim=0)
535
+ scaled = (dominant_bin.float() / max(freq_bins - 1, 1) * (label_range - 1)).round().long()
536
+ ids = scaled.clamp(0, label_range - 1)
537
+ if not first:
538
+ n_drop = min(overlap // hop_length, ids.shape[0])
539
+ ids = ids[n_drop:]
540
+ first = False
541
+ if ids.numel():
542
+ yield ids.numpy().astype(np.int64)
543
+
544
+
545
+ def build_audio_kv_pool(
546
+ path,
547
+ label_range: int = LABEL_RANGE,
548
+ n_fft: int = 400,
549
+ hop_length: int = 160,
550
+ chunk_seconds: float = _AUDIO_CHUNK_SECONDS,
551
+ ) -> list[tuple[int, int]]:
552
+ """Multi-file AND streaming counterpart of build_text_kv_pool/
553
+ build_video_kv_pool for audio: `path` may be a single audio file, or a
554
+ directory / glob pattern / '+'-joined list of these (see
555
+ resolve_link_paths) -- e.g. an entire folder of clips with different
556
+ frequency/amplitude profiles. Each resolved file's dominant-frequency
557
+ token stream is computed and folded into the bigram matrix
558
+ chunk_seconds at a time (see audio_token_stream_chunks) rather than
559
+ loading the whole file into one torch.stft call -- this is what makes
560
+ a multi-hour recording (or a long video's audio track) safe to build a
561
+ pool from without OOMing. No bigram is stitched across a FILE boundary
562
+ (prev_last resets per file); it IS correctly stitched across a CHUNK
563
+ boundary within the same file via prev_last."""
564
+ paths = resolve_link_paths(path)
565
+ matrix = np.zeros(label_range * label_range, dtype=np.int64)
566
+ for p in paths:
567
+ prev_last = None
568
+ for chunk in audio_token_stream_chunks(
569
+ p, label_range, n_fft=n_fft, hop_length=hop_length, chunk_seconds=chunk_seconds
570
+ ):
571
+ prev_last = _accumulate_bigram_chunk(matrix, chunk, label_range, prev_last)
572
+ return _pool_from_bigram_matrix(matrix, label_range)
573
+
574
+
575
+ # ==========================================================================
576
+ # video: frames -> quantized downsampled-frame token stream
577
+ # ==========================================================================
578
+ def _iter_video_frames(path: str):
579
+ try:
580
+ import cv2
581
+ except ImportError as e:
582
+ raise ImportError(
583
+ "video real-data loading requires `opencv-python` (`pip install opencv-python`). "
584
+ f"Original import error: {e}"
585
+ ) from e
586
+ cap = cv2.VideoCapture(path)
587
+ try:
588
+ n = 0
589
+ ok, frame = cap.read()
590
+ while ok:
591
+ yield frame, cv2
592
+ n += 1
593
+ ok, frame = cap.read()
594
+ if n == 0:
595
+ raise ValueError(f"{path}: no frames read (unsupported codec / bad path?)")
596
+ finally:
597
+ cap.release()
598
+
599
+
600
+ def video_token_stream(path: str, label_range: int = LABEL_RANGE, grid: int = 8) -> list[int]:
601
+ """Each frame is downsampled to a `grid`x`grid` grayscale thumbnail and
602
+ its mean intensity quantized to [0, label_range) -- a coarse "scene
603
+ brightness/shape" codebook, analogous in spirit to audio_token_stream's
604
+ dominant-frequency-bin codebook. Adequate to build a real, non-synthetic
605
+ fact stream out of an actual video file without a full CV pipeline.
606
+
607
+ Frames are consumed and discarded one at a time (never collected into a
608
+ full-length list first) -- a long/high-resolution source would otherwise
609
+ hold every original full-size frame in RAM before any processing."""
610
+ means = []
611
+ for frame, cv2 in _iter_video_frames(path):
612
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
613
+ small = cv2.resize(gray, (grid, grid), interpolation=cv2.INTER_AREA)
614
+ means.append(float(small.mean()))
615
+ means_arr = np.asarray(means, dtype=np.float32)
616
+ scaled = np.clip(
617
+ (means_arr / 255.0 * (label_range - 1)).round().astype(np.int64), 0, label_range - 1
618
+ )
619
+ return scaled.tolist()
620
+
621
+
622
+ def build_video_kv_pool(
623
+ path, label_range: int = LABEL_RANGE, grid: int = 8, chunk_frames: int = 200_000
624
+ ) -> list[tuple[int, int]]:
625
+ """Streaming AND multi-file: `path` may be a single video file, or a
626
+ directory / glob pattern / '+'-joined list of these (see
627
+ resolve_link_paths) -- e.g. an entire folder of clips. Quantizes and
628
+ folds frames into the bigram count matrix chunk_frames at a time (never
629
+ building one `means` list for a whole video), and resets the
630
+ within-file stitching state (prev_last) at the start of EACH file, so
631
+ no bigram is stitched across a file boundary."""
632
+ paths = resolve_link_paths(path)
633
+ matrix = np.zeros(label_range * label_range, dtype=np.int64)
634
+
635
+ def _flush(buf):
636
+ arr = np.asarray(buf, dtype=np.float32)
637
+ return np.clip((arr / 255.0 * (label_range - 1)).round(), 0, label_range - 1).astype(
638
+ np.int64
639
+ )
640
+
641
+ for p in paths:
642
+ prev_last = None
643
+ buf = []
644
+ for frame, cv2 in _iter_video_frames(p):
645
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
646
+ small = cv2.resize(gray, (grid, grid), interpolation=cv2.INTER_AREA)
647
+ buf.append(float(small.mean()))
648
+ if len(buf) >= chunk_frames:
649
+ prev_last = _accumulate_bigram_chunk(matrix, _flush(buf), label_range, prev_last)
650
+ buf = []
651
+ if buf:
652
+ prev_last = _accumulate_bigram_chunk(matrix, _flush(buf), label_range, prev_last)
653
+ return _pool_from_bigram_matrix(matrix, label_range)
654
+
655
+
656
+ # ==========================================================================
657
+ # shared: token stream -> fact pool -> train/test split
658
+ # ==========================================================================
659
+ def build_kv_pool_from_tokens(token_ids, label_range: int = LABEL_RANGE) -> list[tuple[int, int]]:
660
+ """Consecutive-pair (bigram) counts -> one (key, most_common_value) fact
661
+ per distinct key seen. Accumulates into a FIXED label_range x label_range
662
+ int64 count matrix (8 MB at label_range=1000, independent of corpus
663
+ length) instead of the old np.unique/lexsort pipeline, which built 3-4
664
+ additional corpus-length arrays on top of `token_ids` itself (the modulo
665
+ copy, the packed-pair array, np.unique's own sort buffer) -- exactly
666
+ what turned a multi-GB token stream into a 10-20x-larger peak RSS and
667
+ OOM'd. `token_ids` is consumed in bounded-size slices, not concatenated
668
+ into another extra array, so even a pre-materialized huge list/array
669
+ here doesn't multiply memory the way the old pipeline did.
670
+ IDs are expected to already be valid -- out-of-range ids raise instead
671
+ of being silently folded in via modulo (a silent fold can collide two
672
+ genuinely distinct tokens into the same id).
673
+ NOTE: ties resolve to whichever value the count matrix's argmax picks
674
+ (lowest value index on a tie) -- harmless, this only seeds a coarse
675
+ synthetic/real fact pool."""
676
+ n = len(token_ids)
677
+ if n < 2:
678
+ raise ValueError(
679
+ "real-data source produced 0 distinct facts "
680
+ "(need at least 4) -- source is too short/uniform to train on."
681
+ )
682
+ matrix = np.zeros(label_range * label_range, dtype=np.int64)
683
+ prev_last = None
684
+ slice_size = 5_000_000
685
+ for start in range(0, n, slice_size):
686
+ chunk = np.asarray(token_ids[start : start + slice_size], dtype=np.int64)
687
+ prev_last = _accumulate_bigram_chunk(matrix, chunk, label_range, prev_last)
688
+ return _pool_from_bigram_matrix(matrix, label_range)
689
+
690
+
691
+ def _accumulate_bigram_chunk(
692
+ matrix_flat: np.ndarray, chunk: np.ndarray, label_range: int, prev_last
693
+ ):
694
+ """Folds one chunk's consecutive-pair counts into `matrix_flat` (a flat,
695
+ length label_range**2 int64 array) in place, and returns this chunk's
696
+ last token id so the NEXT chunk's caller can pass it back in as
697
+ `prev_last` (preserves the cross-chunk-boundary bigram that would
698
+ otherwise be lost by chunking). `chunk` must already contain only ids
699
+ in [0, label_range) -- raises rather than silently wrapping out-of-range
700
+ ids with modulo."""
701
+ if chunk.size == 0:
702
+ return prev_last
703
+ bad = (chunk < 0) | (chunk >= label_range)
704
+ if bad.any():
705
+ raise ValueError(
706
+ f"_accumulate_bigram_chunk: {int(bad.sum())} token id(s) fell "
707
+ f"outside [0, {label_range}) -- upstream tokenizer/quantizer is "
708
+ "misconfigured; ids must already be valid."
709
+ )
710
+ if prev_last is not None:
711
+ matrix_flat[prev_last * label_range + int(chunk[0])] += 1
712
+ if chunk.size >= 2:
713
+ idx = chunk[:-1] * label_range + chunk[1:]
714
+ matrix_flat += np.bincount(idx, minlength=label_range * label_range)
715
+ return int(chunk[-1])
716
+
717
+
718
+ def _pool_from_bigram_matrix(matrix_flat: np.ndarray, label_range: int) -> list[tuple[int, int]]:
719
+ matrix = matrix_flat.reshape(label_range, label_range)
720
+ row_sums = matrix.sum(axis=1)
721
+ keys_present = np.nonzero(row_sums)[0]
722
+ if keys_present.size < 4:
723
+ raise ValueError(
724
+ f"real-data source produced only {keys_present.size} distinct facts "
725
+ "(need at least 4) -- source is too short/uniform to train on."
726
+ )
727
+ vals = matrix[keys_present].argmax(axis=1)
728
+ return list(zip(keys_present.tolist(), vals.tolist()))
729
+
730
+
731
+ def split_train_test_facts(
732
+ pool: list[tuple[int, int]], test_frac: float = 0.1, seed: int = 1234
733
+ ) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
734
+ """Disjoint-by-key split so the test pool is genuinely unseen during
735
+ training -- the token-stream analogue of the graph dataset's
736
+ London-Underground-is-a-different-graph OOD guarantee."""
737
+ pool = list(pool)
738
+ rng = random.Random(seed)
739
+ rng.shuffle(pool)
740
+ n_test = max(1, int(len(pool) * test_frac))
741
+ test_pool, train_pool = pool[:n_test], pool[n_test:]
742
+ if len(train_pool) < 4:
743
+ raise ValueError(
744
+ f"real-data source only has {len(train_pool)} training facts after "
745
+ f"reserving {n_test} for the test split -- source is too short."
746
+ )
747
+ return train_pool, test_pool