@seanhogg/builderforce-memory-engine 2026.6.35 → 2026.7.11

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 (96) hide show
  1. package/README.md +30 -2
  2. package/dist/codec/evermind_video.d.ts +35 -0
  3. package/dist/codec/evermind_video.d.ts.map +1 -0
  4. package/dist/codec/evermind_video.js +37 -0
  5. package/dist/codec/evermind_video.js.map +1 -0
  6. package/dist/codec/image_rvq.d.ts +28 -0
  7. package/dist/codec/image_rvq.d.ts.map +1 -0
  8. package/dist/codec/image_rvq.js +39 -0
  9. package/dist/codec/image_rvq.js.map +1 -0
  10. package/dist/codec/index.d.ts +13 -0
  11. package/dist/codec/index.d.ts.map +1 -0
  12. package/dist/codec/index.js +11 -0
  13. package/dist/codec/index.js.map +1 -0
  14. package/dist/codec/multimodal_vocab.d.ts +79 -0
  15. package/dist/codec/multimodal_vocab.d.ts.map +1 -0
  16. package/dist/codec/multimodal_vocab.js +96 -0
  17. package/dist/codec/multimodal_vocab.js.map +1 -0
  18. package/dist/codec/video_rvq.d.ts +116 -0
  19. package/dist/codec/video_rvq.d.ts.map +1 -0
  20. package/dist/codec/video_rvq.js +450 -0
  21. package/dist/codec/video_rvq.js.map +1 -0
  22. package/dist/import/evermind.d.ts +48 -0
  23. package/dist/import/evermind.d.ts.map +1 -0
  24. package/dist/import/evermind.js +111 -0
  25. package/dist/import/evermind.js.map +1 -0
  26. package/dist/import/index.d.ts +11 -0
  27. package/dist/import/index.d.ts.map +1 -0
  28. package/dist/import/index.js +10 -0
  29. package/dist/import/index.js.map +1 -0
  30. package/dist/import/safetensors.d.ts +13 -0
  31. package/dist/import/safetensors.d.ts.map +1 -0
  32. package/dist/import/safetensors.js +64 -0
  33. package/dist/import/safetensors.js.map +1 -0
  34. package/dist/index.d.ts +8 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/kernels/selective_scan.d.ts.map +1 -1
  39. package/dist/kernels/selective_scan.js +9 -3
  40. package/dist/kernels/selective_scan.js.map +1 -1
  41. package/dist/kernels/ssd.d.ts.map +1 -1
  42. package/dist/kernels/ssd.js +8 -2
  43. package/dist/kernels/ssd.js.map +1 -1
  44. package/dist/kernels/weight_update.d.ts.map +1 -1
  45. package/dist/kernels/weight_update.js +22 -3
  46. package/dist/kernels/weight_update.js.map +1 -1
  47. package/dist/limbic/index.d.ts +2 -2
  48. package/dist/limbic/index.d.ts.map +1 -1
  49. package/dist/limbic/index.js +1 -1
  50. package/dist/limbic/index.js.map +1 -1
  51. package/dist/limbic/regions.d.ts +31 -0
  52. package/dist/limbic/regions.d.ts.map +1 -1
  53. package/dist/limbic/regions.js +34 -0
  54. package/dist/limbic/regions.js.map +1 -1
  55. package/dist/model/mamba_model.d.ts +17 -0
  56. package/dist/model/mamba_model.d.ts.map +1 -1
  57. package/dist/model/mamba_model.js +27 -0
  58. package/dist/model/mamba_model.js.map +1 -1
  59. package/dist/moe/index.d.ts +1 -1
  60. package/dist/moe/index.d.ts.map +1 -1
  61. package/dist/moe/moe_package.d.ts +40 -2
  62. package/dist/moe/moe_package.d.ts.map +1 -1
  63. package/dist/moe/moe_package.js +90 -7
  64. package/dist/moe/moe_package.js.map +1 -1
  65. package/dist/tokenizer/bpe.d.ts +37 -0
  66. package/dist/tokenizer/bpe.d.ts.map +1 -1
  67. package/dist/tokenizer/bpe.js +47 -0
  68. package/dist/tokenizer/bpe.js.map +1 -1
  69. package/dist/training/trainer.d.ts +34 -2
  70. package/dist/training/trainer.d.ts.map +1 -1
  71. package/dist/training/trainer.js +76 -26
  72. package/dist/training/trainer.js.map +1 -1
  73. package/dist/utils/quantization.d.ts.map +1 -1
  74. package/dist/utils/quantization.js +4 -1
  75. package/dist/utils/quantization.js.map +1 -1
  76. package/package.json +1 -1
  77. package/src/codec/evermind_video.ts +55 -0
  78. package/src/codec/image_rvq.ts +50 -0
  79. package/src/codec/index.ts +16 -0
  80. package/src/codec/multimodal_vocab.ts +115 -0
  81. package/src/codec/video_rvq.ts +496 -0
  82. package/src/import/evermind.ts +128 -0
  83. package/src/import/index.ts +11 -0
  84. package/src/import/safetensors.ts +67 -0
  85. package/src/index.ts +35 -0
  86. package/src/kernels/selective_scan.ts +9 -3
  87. package/src/kernels/ssd.ts +8 -2
  88. package/src/kernels/weight_update.ts +22 -3
  89. package/src/limbic/index.ts +2 -1
  90. package/src/limbic/regions.ts +60 -0
  91. package/src/model/mamba_model.ts +27 -0
  92. package/src/moe/index.ts +1 -0
  93. package/src/moe/moe_package.ts +107 -6
  94. package/src/tokenizer/bpe.ts +59 -0
  95. package/src/training/trainer.ts +92 -28
  96. package/src/utils/quantization.ts +4 -1
@@ -0,0 +1,67 @@
1
+ /**
2
+ * import/safetensors.ts — read weights FROM the HF-native `.safetensors` format.
3
+ *
4
+ * The exact inverse of {@link ../export/safetensors.ts}: parse the 8-byte
5
+ * little-endian u64 header length, the JSON header (name → {dtype, shape,
6
+ * data_offsets}), then decode each tensor's raw bytes into a Float32Array.
7
+ * F32 and F16 are supported (F16 is widened to f32 on read). This is the reader
8
+ * half of the warm-start / weight-port path — export was one-directional before.
9
+ */
10
+
11
+ import type { NamedTensor } from "../export/tensors.js";
12
+ import { fp16ToFloat } from "../utils/quantization.js";
13
+
14
+ interface HeaderEntry {
15
+ dtype: string;
16
+ shape: number[];
17
+ data_offsets: [number, number];
18
+ }
19
+
20
+ /** Decode a `.safetensors` byte buffer into named F32 tensors (header order). */
21
+ export function safetensorsToTensors(bytes: Uint8Array): NamedTensor[] {
22
+ if (bytes.length < 8) throw new Error("safetensors: buffer too short for header length");
23
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
24
+ const headerLen = Number(dv.getBigUint64(0, true));
25
+ const headerEnd = 8 + headerLen;
26
+ if (headerEnd > bytes.length) throw new Error(`safetensors: header length ${headerLen} exceeds buffer`);
27
+
28
+ const headerJson = new TextDecoder().decode(bytes.subarray(8, headerEnd));
29
+ let header: Record<string, unknown>;
30
+ try {
31
+ header = JSON.parse(headerJson) as Record<string, unknown>;
32
+ } catch (e) {
33
+ throw new Error(`safetensors: malformed JSON header (${e instanceof Error ? e.message : String(e)})`);
34
+ }
35
+
36
+ const dataBase = headerEnd;
37
+ const out: NamedTensor[] = [];
38
+ for (const [name, raw] of Object.entries(header)) {
39
+ if (name === "__metadata__") continue;
40
+ const entry = raw as HeaderEntry;
41
+ if (!entry || !Array.isArray(entry.shape) || !Array.isArray(entry.data_offsets)) {
42
+ throw new Error(`safetensors: malformed entry for "${name}"`);
43
+ }
44
+ const [start, end] = entry.data_offsets;
45
+ const numel = entry.shape.reduce((n, d) => n * d, 1);
46
+ const seg = bytes.subarray(dataBase + start, dataBase + end);
47
+ out.push({ name, shape: entry.shape, data: decodeTensor(seg, entry.dtype, numel, name) });
48
+ }
49
+ return out;
50
+ }
51
+
52
+ function decodeTensor(seg: Uint8Array, dtype: string, numel: number, name: string): Float32Array {
53
+ const dv = new DataView(seg.buffer, seg.byteOffset, seg.byteLength);
54
+ const data = new Float32Array(numel);
55
+ const d = dtype.toUpperCase();
56
+ if (d === "F32" || d === "FLOAT32") {
57
+ if (seg.length < numel * 4) throw new Error(`safetensors: "${name}" F32 bytes ${seg.length} < ${numel * 4}`);
58
+ for (let i = 0; i < numel; i++) data[i] = dv.getFloat32(i * 4, true);
59
+ return data;
60
+ }
61
+ if (d === "F16" || d === "FLOAT16") {
62
+ if (seg.length < numel * 2) throw new Error(`safetensors: "${name}" F16 bytes ${seg.length} < ${numel * 2}`);
63
+ for (let i = 0; i < numel; i++) data[i] = fp16ToFloat(dv.getUint16(i * 2, true));
64
+ return data;
65
+ }
66
+ throw new Error(`safetensors: unsupported dtype "${dtype}" for "${name}" (expected F32 or F16)`);
67
+ }
package/src/index.ts CHANGED
@@ -33,6 +33,8 @@ export type {
33
33
  MoEEpochResult,
34
34
  EvermindModelManifest,
35
35
  EvermindModelCard,
36
+ EvermindModelType,
37
+ EvermindModality,
36
38
  PackageMeta,
37
39
  ValidationResult,
38
40
  } from './moe/index.js';
@@ -60,6 +62,27 @@ export {
60
62
  // ── Tokenizer ─────────────────────────────────────────────────────────────────
61
63
 
62
64
  export { BPETokenizer } from './tokenizer/bpe.js';
65
+ export type { BPEEncodeOptions, PadSide, HuggingFaceTokenizerSpec, SpecialTokenOverrides } from './tokenizer/bpe.js';
66
+
67
+ // ── Modality codecs (media ⇄ tokens; lets EvermindLM generate video) ────────────
68
+
69
+ export {
70
+ MultimodalVocab,
71
+ VIDEO_BANK_INTRA,
72
+ VIDEO_BANK_INTER,
73
+ VideoRVQCodec,
74
+ ImageRVQCodec,
75
+ buildVideoSequence,
76
+ generateVideo,
77
+ generateImage,
78
+ } from './codec/index.js';
79
+ export type {
80
+ MultimodalVocabConfig,
81
+ TokenKind,
82
+ VideoRVQConfig,
83
+ Frame,
84
+ Video,
85
+ } from './codec/index.js';
63
86
 
64
87
  // ── Model export (the publishing step: ONNX / safetensors / GGUF / HF repo) ─────
65
88
 
@@ -90,6 +113,16 @@ export type {
90
113
  TensorSpec,
91
114
  } from './export/index.js';
92
115
 
116
+ // ── Model import (warm-start / weight-port: safetensors → EvermindLM) ────────────
117
+
118
+ export {
119
+ safetensorsToTensors,
120
+ importEvermind,
121
+ importEvermindTensors,
122
+ inferArchFromTensors,
123
+ } from './import/index.js';
124
+ export type { ImportOptions } from './import/index.js';
125
+
93
126
  // ── Benchmarking (held-out perplexity / accuracy / throughput + A/B) ──────────
94
127
 
95
128
  export {
@@ -220,6 +253,7 @@ export {
220
253
  neutralState,
221
254
  stateToRecord,
222
255
  recordToState,
256
+ personalitySetpoint,
223
257
  LimbicModel,
224
258
  DEFAULT_LIMBIC_CONFIG,
225
259
  DEFAULT_LIMBIC_SEED,
@@ -228,6 +262,7 @@ export {
228
262
  export type {
229
263
  Region,
230
264
  LimbicDimName,
265
+ PersonalityTraits,
231
266
  LimbicModelConfig,
232
267
  LimbicForward,
233
268
  LimbicParam,
@@ -60,12 +60,15 @@ fn softplus(x: f32) -> f32 {
60
60
  // B_bar = (A_bar - 1) / A * B ≈ Δ * B (first-order for simplicity)
61
61
  fn discretise_A(delta_val: f32, a_log: f32) -> f32 {
62
62
  // A is stored as -exp(a_log) to ensure A_bar < 1 (stable)
63
- let a_cont = -exp(a_log);
63
+ // Clamp log-decay so -exp(a_log) can't overflow to -Inf (A_bar→0, state
64
+ // death) nor collapse toward 0 (A_bar→1, no decay). Keeps A_bar strictly in
65
+ // (0,1) across repeated adapts. Belt-and-suspenders: WSLA also freezes A_log.
66
+ let a_cont = -exp(clamp(a_log, -10.0, 5.0));
64
67
  return exp(delta_val * a_cont);
65
68
  }
66
69
 
67
70
  fn discretise_B(delta_val: f32, a_log: f32, b_val: f32) -> f32 {
68
- let a_cont = -exp(a_log);
71
+ let a_cont = -exp(clamp(a_log, -10.0, 5.0));
69
72
  let a_bar = exp(delta_val * a_cont);
70
73
  // (A_bar - 1) / A_cont * B
71
74
  let b_bar = (a_bar - 1.0) / a_cont * b_val;
@@ -264,7 +267,10 @@ fn softplus_grad(x: f32) -> f32 {
264
267
  }
265
268
 
266
269
  fn discretise_A(delta_val: f32, a_log: f32) -> f32 {
267
- let a_cont = -exp(a_log);
270
+ // Clamp log-decay so -exp(a_log) can't overflow to -Inf (A_bar→0, state
271
+ // death) nor collapse toward 0 (A_bar→1, no decay). Keeps A_bar strictly in
272
+ // (0,1) across repeated adapts. Belt-and-suspenders: WSLA also freezes A_log.
273
+ let a_cont = -exp(clamp(a_log, -10.0, 5.0));
268
274
  return exp(delta_val * a_cont);
269
275
  }
270
276
 
@@ -50,7 +50,10 @@ struct SsdParams {
50
50
  @group(0) @binding(9) var<storage, read_write> state_carry : array<f32>; // [n_chunks+1,B,H,N,d_head]
51
51
 
52
52
  fn softplus(x: f32) -> f32 {
53
- return log(1.0 + exp(x));
53
+ // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x))
54
+ // overflows to +Inf for x ≳ 88 (f32 exp range) — and a downstream
55
+ // exp(-Inf*0) then yields NaN, poisoning the state. This form never overflows.
56
+ return max(x, 0.0) + log(1.0 + exp(-abs(x)));
54
57
  }
55
58
 
56
59
  // Workgroup: one chunk × one head × one batch item
@@ -186,7 +189,10 @@ struct SsdParams {
186
189
  @group(0) @binding(14) var<storage, read_write> dD_vec : array<f32>;
187
190
 
188
191
  fn softplus(x: f32) -> f32 {
189
- return log(1.0 + exp(x));
192
+ // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x))
193
+ // overflows to +Inf for x ≳ 88 (f32 exp range) — and a downstream
194
+ // exp(-Inf*0) then yields NaN, poisoning the state. This form never overflows.
195
+ return max(x, 0.0) + log(1.0 + exp(-abs(x)));
190
196
  }
191
197
  fn d_softplus(x: f32) -> f32 {
192
198
  return 1.0 / (1.0 + exp(-x));
@@ -19,6 +19,10 @@ struct AdamParams {
19
19
  weight_decay : f32, // default 0.01
20
20
  beta1_t : f32, // beta1^t (precomputed bias correction term)
21
21
  beta2_t : f32, // beta2^t
22
+ max_delta : f32, // trust region: max |Δθ| per step (0 ⇒ unbounded)
23
+ _pad0 : f32, // pad struct to a 16-byte multiple (uniform layout)
24
+ _pad1 : f32,
25
+ _pad2 : f32,
22
26
  };
23
27
 
24
28
  @group(0) @binding(0) var<uniform> adam : AdamParams;
@@ -52,9 +56,24 @@ fn adamw_update(
52
56
  let m_hat = m_new / (1.0 - adam.beta1_t);
53
57
  let v_hat = v_new / (1.0 - adam.beta2_t);
54
58
 
55
- // Weight decay (decoupled) + gradient step
56
- param[i] = p * (1.0 - adam.lr * adam.weight_decay) -
57
- adam.lr * m_hat / (sqrt(v_hat) + adam.eps);
59
+ // Adam step.
60
+ var step = adam.lr * m_hat / (sqrt(v_hat) + adam.eps);
61
+
62
+ // Numerical guard: never write a non-finite step into a weight. A NaN or Inf
63
+ // here (from a bad gradient, a zero v_hat, an overflow) would permanently
64
+ // poison the parameter and every future forward — the "model dies" failure.
65
+ // NaN fails self-comparison; treat ±Inf-magnitude as non-finite too.
66
+ if (step != step || step > 3.4e38 || step < -3.4e38) { step = 0.0; }
67
+
68
+ // Trust region: bound how far ONE step can move a weight. With write-through
69
+ // adaptation running repeatedly, an unbounded step (even from a noisy
70
+ // gradient) compounds across executions and blows the weights up; clamping
71
+ // the per-element delta keeps every adapt small and reversible. max_delta==0
72
+ // disables the bound (full-training callers that don't set it).
73
+ if (adam.max_delta > 0.0) { step = clamp(step, -adam.max_delta, adam.max_delta); }
74
+
75
+ // Weight decay (decoupled) + bounded gradient step
76
+ param[i] = p * (1.0 - adam.lr * adam.weight_decay) - step;
58
77
  }
59
78
  `;
60
79
 
@@ -18,8 +18,9 @@ export {
18
18
  neutralState,
19
19
  stateToRecord,
20
20
  recordToState,
21
+ personalitySetpoint,
21
22
  } from "./regions.js";
22
- export type { Region, LimbicDimName } from "./regions.js";
23
+ export type { Region, LimbicDimName, PersonalityTraits } from "./regions.js";
23
24
 
24
25
  export { LimbicModel, DEFAULT_LIMBIC_CONFIG, DEFAULT_LIMBIC_SEED } from "./limbic_model.js";
25
26
  export type { LimbicModelConfig, LimbicForward, LimbicParam } from "./limbic_model.js";
@@ -139,3 +139,63 @@ export function recordToState(rec: Partial<Record<LimbicDimName, number>>): Floa
139
139
  }
140
140
  return s;
141
141
  }
142
+
143
+ /**
144
+ * The static psychometric traits (0..100, 50 = neutral) that pull the resting
145
+ * affective setpoint away from {@link NEUTRAL_STATE}. A subset of the full
146
+ * personality vector — only the dimensions that map to a limbic drive. All
147
+ * optional; an omitted trait is treated as neutral (50).
148
+ *
149
+ * Keep the mapping in {@link personalitySetpoint} in sync with the runtime
150
+ * compiler `deriveLimbicSetpoints` in `@builderforce/agent-tools` so the on-prem
151
+ * runtime, the cloud engine, and this WebGPU trainer settle on the SAME baseline
152
+ * for the same personality. The two are coupled only by this mapping.
153
+ */
154
+ export interface PersonalityTraits {
155
+ openness?: number;
156
+ emotionality?: number;
157
+ conscientiousness?: number;
158
+ extraversion?: number;
159
+ /** Regulatory focus: 0 = prevention, 100 = promotion. */
160
+ regulatoryFocus?: number;
161
+ riskTolerance?: number;
162
+ grit?: number;
163
+ /** Schwartz "stimulation" value. */
164
+ stimulation?: number;
165
+ }
166
+
167
+ /** Signed influence of a 0..100 trait around its neutral midpoint → [-1, 1]. */
168
+ function infl(s: number | undefined): number {
169
+ const v = typeof s === "number" && !Number.isNaN(s) ? Math.max(0, Math.min(100, s)) : 50;
170
+ return (v - 50) / 50;
171
+ }
172
+
173
+ /**
174
+ * Derive the personality-conditioned resting SETPOINT from a trait vector — the
175
+ * homeostatic target the limbic dynamics relax toward and that the trainable
176
+ * affect model rides on top of ("personality = setpoints, limbic = dynamics").
177
+ * Returns a fresh clamped 8-dim state vector. A fully-neutral trait vector yields
178
+ * {@link NEUTRAL_STATE}. Mirrors `deriveLimbicSetpoints` in `@builderforce/agent-tools`.
179
+ */
180
+ export function personalitySetpoint(traits: PersonalityTraits | undefined): Float32Array {
181
+ const s = neutralState();
182
+ if (!traits) return s;
183
+ const open = infl(traits.openness);
184
+ const emo = infl(traits.emotionality);
185
+ const consc = infl(traits.conscientiousness);
186
+ const extra = infl(traits.extraversion);
187
+ const reg = infl(traits.regulatoryFocus);
188
+ const risk = infl(traits.riskTolerance);
189
+ const grit = infl(traits.grit);
190
+ const stim = infl(traits.stimulation);
191
+
192
+ s[LIMBIC_DIM.driveCuriosity] = clampDim(LIMBIC_DIM.driveCuriosity, 0.5 + 0.35 * open + 0.15 * stim);
193
+ s[LIMBIC_DIM.exploration] = clampDim(LIMBIC_DIM.exploration, 0.4 + 0.3 * open + 0.25 * risk + 0.15 * reg);
194
+ s[LIMBIC_DIM.driveCaution] = clampDim(LIMBIC_DIM.driveCaution, 0.5 + 0.3 * consc - 0.3 * risk - 0.2 * reg + 0.15 * emo);
195
+ s[LIMBIC_DIM.arousal] = clampDim(LIMBIC_DIM.arousal, 0.2 + 0.2 * emo + 0.1 * extra);
196
+ s[LIMBIC_DIM.driveSocial] = clampDim(LIMBIC_DIM.driveSocial, 0.5 + 0.35 * extra);
197
+ s[LIMBIC_DIM.driveEffort] = clampDim(LIMBIC_DIM.driveEffort, 0.8 + 0.15 * grit + 0.1 * consc);
198
+ s[LIMBIC_DIM.valence] = clampDim(LIMBIC_DIM.valence, 0.0 + 0.1 * reg - 0.1 * emo);
199
+ s[LIMBIC_DIM.attention] = clampDim(LIMBIC_DIM.attention, 0.7 + 0.1 * consc);
200
+ return s;
201
+ }
@@ -413,6 +413,33 @@ export class HybridMambaModel {
413
413
  return params;
414
414
  }
415
415
 
416
+ /**
417
+ * The parameters a `learn()` / `adapt()` step is allowed to update.
418
+ *
419
+ * Under WSLA (write-through / narrow adaptation — the Evermind "update ==
420
+ * replace" path) this is ONLY the per-layer narrow subset (the input
421
+ * projections that produce Δ/B/C). The backbone — token embedding, final
422
+ * norm, and every block's `A_log` / conv / output projection — stays FROZEN.
423
+ * Freezing `A_log` is the key stability guarantee: the state-transition
424
+ * decay can never drift into a degenerate regime (state death / no-decay)
425
+ * across repeated adapts, which is what made the model "die after several
426
+ * executions". Outside WSLA every parameter trains (full fine-tune / distill).
427
+ *
428
+ * Param names are namespaced `layer{i}.{name}` to match {@link parameters},
429
+ * so a name-keyed optimizer (see MambaTrainer) reuses the same Adam moments
430
+ * whether it's stepping the full set or just the WSLA subset.
431
+ */
432
+ getTrainableParams(): LayerParam[] {
433
+ if (!this._wslaMode) return this.parameters();
434
+ const params: LayerParam[] = [];
435
+ for (let i = 0; i < this.layers.length; i++) {
436
+ for (const p of this.layers[i]!.getTrainableParams()) {
437
+ params.push({ ...p, name: `layer${i}.${p.name}` });
438
+ }
439
+ }
440
+ return params;
441
+ }
442
+
416
443
  setWSLAMode(enabled: boolean): void {
417
444
  for (const layer of this.layers) layer.setWSLAMode(enabled);
418
445
  this._wslaMode = enabled;
package/src/moe/index.ts CHANGED
@@ -18,6 +18,7 @@ export type {
18
18
  EvermindModelManifest,
19
19
  EvermindModelCard,
20
20
  EvermindModelType,
21
+ EvermindModality,
21
22
  PackageMeta,
22
23
  ValidationResult,
23
24
  } from "./moe_package.js";
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { SharedExpertMoE, type MoEConfig } from "./moe_model.js";
15
15
  import { EvermindLM, type EvermindLMConfig } from "../lm/evermind_lm.js";
16
+ import { VideoRVQCodec } from "../codec/video_rvq.js";
16
17
 
17
18
  /** First 4 bytes of a serialised package: "EVM1". */
18
19
  const PKG_MAGIC = 0x45564d31;
@@ -21,6 +22,9 @@ const PKG_VERSION = 1;
21
22
  /** The kinds of model an `.evermind` package can carry. */
22
23
  export type EvermindModelType = "shared-expert-moe" | "evermind-lm";
23
24
 
25
+ /** Output modality. `text` (default) needs no codec; `video`/`image` bundle a VRQ codec. */
26
+ export type EvermindModality = "text" | "video" | "image";
27
+
24
28
  /** Human-facing description published with the model (the "model card"). */
25
29
  export interface EvermindModelCard {
26
30
  description: string;
@@ -38,14 +42,26 @@ export interface EvermindModelManifest {
38
42
  name: string;
39
43
  version: string;
40
44
  modelType: EvermindModelType;
45
+ /** Output modality; absent ⇒ "text". Video/image also carry a `codec` section. */
46
+ modality?: EvermindModality;
41
47
  /** Flat numeric model config (the constructor args), serialised verbatim. */
42
48
  config: Record<string, number>;
43
49
  /** Total trainable scalar parameters (for sizing / pricing / display). */
44
50
  paramCount: number;
45
51
  checkpointFormat: "MoE0" | "EVL0";
46
52
  checkpointFp16: boolean;
53
+ /**
54
+ * Byte length of the checkpoint section — REQUIRED when a codec section follows
55
+ * (so the reader knows where the checkpoint ends). Absent ⇒ checkpoint runs to
56
+ * end-of-blob (the original text-only layout, still read verbatim).
57
+ */
58
+ checkpointBytes?: number;
47
59
  /** 32-bit FNV-1a over the checkpoint bytes — integrity for download/purchase. */
48
60
  checksum: number;
61
+ /** Media codec section format (present for video/image packages). */
62
+ codecFormat?: "VRQ0";
63
+ /** 32-bit FNV-1a over the codec bytes — integrity for the bundled codec. */
64
+ codecChecksum?: number;
49
65
  card: EvermindModelCard;
50
66
  /** ISO timestamp, caller-supplied (the engine avoids Date for determinism). */
51
67
  createdAt?: string;
@@ -84,6 +100,8 @@ export class EvermindModelPackage {
84
100
  constructor(
85
101
  readonly manifest: EvermindModelManifest,
86
102
  readonly checkpoint: ArrayBuffer,
103
+ /** Serialized media codec ("VRQ0" blob), present for video/image packages. */
104
+ readonly codec?: ArrayBuffer,
87
105
  ) {}
88
106
 
89
107
  /** Package a trained model with its publishing metadata. */
@@ -126,17 +144,61 @@ export class EvermindModelPackage {
126
144
  return new EvermindModelPackage(manifest, checkpoint);
127
145
  }
128
146
 
129
- /** Serialise to a single `.evermind` blob: magic, version, manifest, checkpoint. */
147
+ /**
148
+ * Package a video/image generative model: its {@link EvermindLM} weights AND the
149
+ * {@link VideoRVQCodec} needed to turn generated tokens back into frames. Without
150
+ * the bundled codec a served media model would emit undecodable token ids — this
151
+ * is what makes an `.evermind` media artifact self-contained and servable.
152
+ */
153
+ static fromMediaLM(
154
+ lm: EvermindLM,
155
+ codec: VideoRVQCodec,
156
+ meta: PackageMeta & { modality: "video" | "image" },
157
+ ): EvermindModelPackage {
158
+ if (lm.config.vocabSize !== codec.vocabSize) {
159
+ throw new Error(
160
+ `fromMediaLM: LM vocabSize (${lm.config.vocabSize}) must equal codec.vocabSize (${codec.vocabSize})`,
161
+ );
162
+ }
163
+ const fp16 = meta.fp16 ?? false;
164
+ const checkpoint = lm.exportWeights({ fp16 });
165
+ const codecBlob = codec.serialize();
166
+ const manifest: EvermindModelManifest = {
167
+ schema: "evermind.model/1",
168
+ name: meta.name,
169
+ version: meta.version,
170
+ modelType: "evermind-lm",
171
+ modality: meta.modality,
172
+ config: lm.config as unknown as Record<string, number>,
173
+ paramCount: lm.parameters().reduce((n, p) => n + p.data.length, 0),
174
+ checkpointFormat: "EVL0",
175
+ checkpointFp16: fp16,
176
+ checkpointBytes: checkpoint.byteLength,
177
+ checksum: fnv1a(new Uint8Array(checkpoint)),
178
+ codecFormat: "VRQ0",
179
+ codecChecksum: fnv1a(new Uint8Array(codecBlob)),
180
+ card: meta.card,
181
+ ...(meta.createdAt ? { createdAt: meta.createdAt } : {}),
182
+ };
183
+ return new EvermindModelPackage(manifest, checkpoint, codecBlob);
184
+ }
185
+
186
+ /** Serialise to a single `.evermind` blob: magic, version, manifest, checkpoint[, codec]. */
130
187
  toBlob(): ArrayBuffer {
131
188
  const manifestBytes = new TextEncoder().encode(JSON.stringify(this.manifest));
132
189
  const headerBytes = 12; // magic, version, manifestLen
133
- const out = new ArrayBuffer(headerBytes + manifestBytes.byteLength + this.checkpoint.byteLength);
190
+ const codecBytes = this.codec?.byteLength ?? 0;
191
+ const out = new ArrayBuffer(headerBytes + manifestBytes.byteLength + this.checkpoint.byteLength + codecBytes);
134
192
  const head = new Uint32Array(out, 0, 3);
135
193
  head[0] = PKG_MAGIC;
136
194
  head[1] = PKG_VERSION;
137
195
  head[2] = manifestBytes.byteLength;
138
- new Uint8Array(out, headerBytes, manifestBytes.byteLength).set(manifestBytes);
139
- new Uint8Array(out, headerBytes + manifestBytes.byteLength).set(new Uint8Array(this.checkpoint));
196
+ let o = headerBytes;
197
+ new Uint8Array(out, o, manifestBytes.byteLength).set(manifestBytes);
198
+ o += manifestBytes.byteLength;
199
+ new Uint8Array(out, o, this.checkpoint.byteLength).set(new Uint8Array(this.checkpoint));
200
+ o += this.checkpoint.byteLength;
201
+ if (this.codec) new Uint8Array(out, o, codecBytes).set(new Uint8Array(this.codec));
140
202
  return out;
141
203
  }
142
204
 
@@ -152,8 +214,19 @@ export class EvermindModelPackage {
152
214
  }
153
215
  const manifestBytes = new Uint8Array(buffer, headerBytes, manifestLen);
154
216
  const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as EvermindModelManifest;
155
- const checkpoint = buffer.slice(headerBytes + manifestLen);
156
- return new EvermindModelPackage(manifest, checkpoint);
217
+ const bodyStart = headerBytes + manifestLen;
218
+ // A codec section follows the checkpoint only when the manifest fixed the
219
+ // checkpoint length; otherwise the checkpoint runs to end-of-blob (text layout).
220
+ const cpBytes = manifest.checkpointBytes;
221
+ if (cpBytes != null) {
222
+ if (bodyStart + cpBytes > buffer.byteLength) {
223
+ throw new Error("EvermindModelPackage.fromBlob: truncated (checkpointBytes exceeds blob)");
224
+ }
225
+ const checkpoint = buffer.slice(bodyStart, bodyStart + cpBytes);
226
+ const codec = bodyStart + cpBytes < buffer.byteLength ? buffer.slice(bodyStart + cpBytes) : undefined;
227
+ return new EvermindModelPackage(manifest, checkpoint, codec);
228
+ }
229
+ return new EvermindModelPackage(manifest, buffer.slice(bodyStart));
157
230
  }
158
231
 
159
232
  /** Verify integrity + structural sanity before trusting a downloaded package. */
@@ -176,6 +249,19 @@ export class EvermindModelPackage {
176
249
  if (actual !== this.manifest.checksum) {
177
250
  errors.push(`checksum mismatch (manifest ${this.manifest.checksum}, actual ${actual}) — corrupt or tampered checkpoint`);
178
251
  }
252
+ // Media packages must carry an integral codec — a served media model can't
253
+ // decode tokens → frames without it.
254
+ const modality = this.manifest.modality ?? "text";
255
+ if (modality === "video" || modality === "image") {
256
+ if (!this.codec) {
257
+ errors.push(`modality '${modality}' package is missing its codec section`);
258
+ } else {
259
+ const codecActual = fnv1a(new Uint8Array(this.codec));
260
+ if (codecActual !== this.manifest.codecChecksum) {
261
+ errors.push(`codec checksum mismatch (manifest ${this.manifest.codecChecksum}, actual ${codecActual}) — corrupt codec`);
262
+ }
263
+ }
264
+ }
179
265
  return { ok: errors.length === 0, errors };
180
266
  }
181
267
 
@@ -202,4 +288,19 @@ export class EvermindModelPackage {
202
288
  lm.loadWeights(this.checkpoint);
203
289
  return lm;
204
290
  }
291
+
292
+ /**
293
+ * Reconstruct a video/image model: the generative {@link EvermindLM} plus its
294
+ * {@link VideoRVQCodec}. Pair with `generateVideo` / `generateImage` to produce
295
+ * frames. This is what a media-serving path (gateway) loads and runs.
296
+ */
297
+ loadMediaLM(): { lm: EvermindLM; codec: VideoRVQCodec; modality: EvermindModality } {
298
+ const v = this.validate();
299
+ if (!v.ok) throw new Error(`EvermindModelPackage.loadMediaLM: ${v.errors.join("; ")}`);
300
+ const modality = this.manifest.modality ?? "text";
301
+ if (modality !== "video" && modality !== "image") {
302
+ throw new Error(`loadMediaLM: package modality is '${modality}', use loadLM()`);
303
+ }
304
+ return { lm: this.loadLM(), codec: VideoRVQCodec.deserialize(this.codec!), modality };
305
+ }
205
306
  }
@@ -9,6 +9,21 @@ export interface BPEEncodeOptions {
9
9
 
10
10
  export type PadSide = 'right' | 'left';
11
11
 
12
+ /** A Hugging Face `tokenizer.json` (or a bare `{ vocab, merges }`) for import. */
13
+ export interface HuggingFaceTokenizerSpec {
14
+ model?: { vocab?: Record<string, number>; merges?: Array<string | [string, string]> };
15
+ vocab?: Record<string, number>;
16
+ merges?: Array<string | [string, string]>;
17
+ }
18
+
19
+ /** Override the special-token strings to match an imported tokenizer. */
20
+ export interface SpecialTokenOverrides {
21
+ bos?: string;
22
+ eos?: string;
23
+ pad?: string;
24
+ unk?: string;
25
+ }
26
+
12
27
  function buildByteEncoder(): Map<number, string> {
13
28
  const enc = new Map<number, string>();
14
29
  const ranges: [number, number][] = [
@@ -177,6 +192,50 @@ export class BPETokenizer {
177
192
  this.padId = this.vocab.get(this.padToken) ?? null;
178
193
  }
179
194
 
195
+ /**
196
+ * Seed this tokenizer from an existing Hugging Face `tokenizer.json` — the
197
+ * *import-merges* path (vs `train`, which learns a fresh vocab from a corpus).
198
+ *
199
+ * BPE merges + vocab are portable DATA, not architecture: a proven code
200
+ * tokenizer (GPT-2 / Llama / StarCoder family) gives Evermind a battle-tested
201
+ * code vocabulary on day one. Those tokenizers use the same GPT-2 byte→unicode
202
+ * mapping this class already implements ({@link BYTE_ENCODER}), so the imported
203
+ * vocab is directly compatible with `encode`/`decode`/`_bpe`.
204
+ *
205
+ * Accepts the full `tokenizer.json` object (reads `.model.vocab` / `.model.merges`)
206
+ * or a bare `{ vocab, merges }`. `merges` may be the classic `"a b"` strings or
207
+ * the newer `["a", "b"]` pair arrays. Special-token strings can be overridden to
208
+ * match the source tokenizer (e.g. `<|endoftext|>`, `<s>`, `</s>`).
209
+ */
210
+ loadHuggingFace(spec: HuggingFaceTokenizerSpec, specials: SpecialTokenOverrides = {}): void {
211
+ const model = (spec.model ?? spec) as { vocab?: Record<string, number>; merges?: Array<string | [string, string]> };
212
+ const vocabObj = model.vocab ?? (spec as HuggingFaceTokenizerSpec).vocab;
213
+ const rawMerges = model.merges ?? (spec as HuggingFaceTokenizerSpec).merges;
214
+ if (!vocabObj || typeof vocabObj !== 'object') {
215
+ throw new Error('loadHuggingFace: missing model.vocab (expected a Hugging Face tokenizer.json)');
216
+ }
217
+ if (!Array.isArray(rawMerges)) {
218
+ throw new Error('loadHuggingFace: missing model.merges (expected a BPE tokenizer.json)');
219
+ }
220
+ const mergeArr = rawMerges.map((m) => (Array.isArray(m) ? `${m[0]} ${m[1]}` : String(m)));
221
+ if (specials.bos) this.bosToken = specials.bos;
222
+ if (specials.eos) this.eosToken = specials.eos;
223
+ if (specials.pad) this.padToken = specials.pad;
224
+ if (specials.unk) this.unkToken = specials.unk;
225
+ this.loadFromObjects(vocabObj, mergeArr);
226
+ }
227
+
228
+ /**
229
+ * Fetch and import a Hugging Face `tokenizer.json` by URL — the IDE's
230
+ * "import merges from a repo" path. Resolves once the tokenizer is loaded.
231
+ */
232
+ async loadHuggingFaceUrl(url: string, specials: SpecialTokenOverrides = {}): Promise<void> {
233
+ const res = await fetch(url);
234
+ if (!res.ok) throw new Error(`loadHuggingFaceUrl: ${res.status} fetching ${url}`);
235
+ const spec = (await res.json()) as HuggingFaceTokenizerSpec;
236
+ this.loadHuggingFace(spec, specials);
237
+ }
238
+
180
239
  encode(text: string, opts: BPEEncodeOptions = {}): number[] {
181
240
  const words = text.match(PRE_TOKENIZE_RE) ?? [];
182
241
  const ids: number[] = [];