Spaces:
Running on Zero
Running on Zero
Add files using upload-large-folder tool
Browse files- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/features.cpython-313.pyc +0 -0
- __pycache__/mm_grad.cpython-313.pyc +0 -0
- __pycache__/modular_mind.cpython-313.pyc +0 -0
- __pycache__/online.cpython-313.pyc +0 -0
- agents/modmind/config.py +158 -0
- agents/modmind/language/tokenizer.json +0 -0
- agents/modmind/model.py +1011 -0
- agents/modmind/moe_gradio.py +388 -0
- agents/modmind/reasoning/tokenizer.json +0 -0
- agents/modmind/registry.py +44 -0
- agents/modmind/specialist_presets.py +69 -0
- agents/modmind/spike_tokenizer.py +82 -0
- agents/panel.py +292 -0
- app.py +535 -0
- assets_data.js +0 -0
- audio/sfx/hurt3_monster.wav +0 -0
- audio/sfx/hurt_knight.wav +0 -0
- audio/sfx/hurt_monster.wav +0 -0
- audio/sfx/jump_knight.wav +0 -0
- audio/sfx/roar2_monster.wav +0 -0
- audio/sfx/roar3_monster.wav +0 -0
- audio/sfx/roar4_monster.wav +0 -0
- audio/sfx/roar5_monster.wav +0 -0
- audio/sfx/roar6_monster.wav +0 -0
- audio/sfx/roar_monster.wav +0 -0
- audio/sfx/walk_boss.wav +0 -0
- audio/sfx/walk_knight.wav +0 -0
- features.py +53 -0
- mm_grad.py +257 -0
- modular_mind.py +185 -0
- online.py +124 -0
- piano/notes.json +1 -0
- piano/piano_mind.py +168 -0
- piano/poly_mind.py +150 -0
- piano/poly_notes.json +1 -0
- piano/samples/48.mp3 +0 -0
- piano/samples/53.mp3 +0 -0
- piano/samples/60.mp3 +0 -0
- piano/samples/65.mp3 +0 -0
- piano/samples/69.mp3 +0 -0
- piano/samples/74.mp3 +0 -0
- piano/samples/79.mp3 +0 -0
- piano/samples/84.mp3 +0 -0
- piano/samples/89.mp3 +0 -0
- requirements.txt +4 -0
- web/README.md +168 -0
- web/game.css +108 -0
- web/game.js +653 -0
- web/index.html +48 -0
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (31 kB). View file
|
|
|
__pycache__/features.cpython-313.pyc
ADDED
|
Binary file (3 kB). View file
|
|
|
__pycache__/mm_grad.cpython-313.pyc
ADDED
|
Binary file (17.3 kB). View file
|
|
|
__pycache__/modular_mind.cpython-313.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
__pycache__/online.cpython-313.pyc
ADDED
|
Binary file (7.53 kB). View file
|
|
|
agents/modmind/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
config.py -- SpikeWhale: combined config from SpikeTransformer (My Project) + NanoWhale (DeepSeek-V4).
|
| 3 |
+
|
| 4 |
+
Features carried from My Project (not in NanoWhale):
|
| 5 |
+
- DERF attention: erf(alpha*score+bias)*gamma replaces softmax
|
| 6 |
+
- XSA (Exclusive Self-Attention): orthogonality correction removes self-echo from attn output
|
| 7 |
+
- Engram N-gram module: hash-table N-gram lookup with DERF gate injected into embeddings
|
| 8 |
+
- Three-tier optimizer: embed/table params trained at lower LR
|
| 9 |
+
|
| 10 |
+
Features carried from NanoWhale (not in My Project):
|
| 11 |
+
- MLA (Multi-Head Latent Attention): low-rank Q projection + direct K,V (MQA)
|
| 12 |
+
- Partial RoPE: rotary embeddings on only qk_rope_head_dim dims of Q and K
|
| 13 |
+
- Low-rank grouped output projection (o_lora_rank)
|
| 14 |
+
- Hyper-Connections: hc_mult residual streams with learned routing between layers
|
| 15 |
+
- Shared expert in MoE (always-active expert alongside routed experts)
|
| 16 |
+
- sqrtsoftplus expert scoring (vs softmax in My Project)
|
| 17 |
+
- Hash-based routing for first num_hash_layers layers
|
| 18 |
+
- norm_topk_prob + routed_scaling_factor
|
| 19 |
+
- Multi-Token Prediction (MTP): extra heads predict k steps ahead
|
| 20 |
+
- torch.compile, FineWeb-Edu streaming, Trackio, YAML configs in train.py
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from transformers import PretrainedConfig
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class SpikeWhaleConfig(PretrainedConfig):
|
| 27 |
+
model_type = "spike_whale"
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
# Standard
|
| 32 |
+
vocab_size: int = 129280,
|
| 33 |
+
hidden_size: int = 2048,
|
| 34 |
+
num_hidden_layers: int = 11,
|
| 35 |
+
max_position_embeddings: int = 8192,
|
| 36 |
+
rms_norm_eps: float = 1e-6,
|
| 37 |
+
initializer_range: float = 0.02,
|
| 38 |
+
tie_word_embeddings: bool = False,
|
| 39 |
+
hidden_dropout: float = 0.0,
|
| 40 |
+
bos_token_id: int = 0,
|
| 41 |
+
eos_token_id: int = 1,
|
| 42 |
+
# MLA Attention (NanoWhale)
|
| 43 |
+
num_attention_heads: int = 8,
|
| 44 |
+
num_key_value_heads: int = 1, # 1 = MQA; >1 = GQA
|
| 45 |
+
q_lora_rank: int = 160, # low-rank Q: hidden -> q_lora_rank -> num_heads*head_dim
|
| 46 |
+
head_dim: int = 96, # total per-head dim = nope_head_dim + qk_rope_head_dim
|
| 47 |
+
qk_rope_head_dim: int = 32, # RoPE applied only to these dims
|
| 48 |
+
o_lora_rank: int = 80, # low-rank output: num_heads*head_dim -> o_lora_rank -> hidden
|
| 49 |
+
attention_dropout: float = 0.0,
|
| 50 |
+
rope_theta: float = 10000.0,
|
| 51 |
+
# DERF + XSA (My Project)
|
| 52 |
+
use_derf: bool = True,
|
| 53 |
+
use_xsa: bool = True,
|
| 54 |
+
# MoE (combined)
|
| 55 |
+
use_moe: bool = True,
|
| 56 |
+
moe_intermediate_size: int = 640,
|
| 57 |
+
n_routed_experts: int = 4,
|
| 58 |
+
n_shared_experts: int = 1, # NanoWhale: always-active shared expert
|
| 59 |
+
num_experts_per_tok: int = 2,
|
| 60 |
+
norm_topk_prob: bool = True, # NanoWhale: normalize top-k routing weights
|
| 61 |
+
scoring_func: str = "sqrtsoftplus", # NanoWhale: sqrt(softplus(x)) vs softmax
|
| 62 |
+
routed_scaling_factor: float = 1.0, # NanoWhale: scale routed expert weights
|
| 63 |
+
num_hash_layers: int = 2, # NanoWhale: first N layers use hash routing
|
| 64 |
+
moe_aux_loss_coef: float = 0.01,
|
| 65 |
+
moe_layers: list = None,
|
| 66 |
+
# Hyper-Connections (NanoWhale)
|
| 67 |
+
use_hyper_connections: bool = True,
|
| 68 |
+
hc_mult: int = 4, # number of parallel residual streams
|
| 69 |
+
hc_sinkhorn_iters: int = 20,
|
| 70 |
+
hc_eps: float = 1e-6,
|
| 71 |
+
# Multi-Token Prediction (NanoWhale)
|
| 72 |
+
num_nextn_predict_layers: int = 1, # extra MTP heads (0 = disabled)
|
| 73 |
+
# Engram N-gram module (My Project)
|
| 74 |
+
use_engram: bool = True,
|
| 75 |
+
engram_compress_dim: int = 64,
|
| 76 |
+
engram_num_heads: int = 4,
|
| 77 |
+
engram_table_size: int = 8192,
|
| 78 |
+
engram_max_ngram: int = 3,
|
| 79 |
+
engram_gate_init_bias: float = -4.0,
|
| 80 |
+
use_hrm_refine: bool = False,
|
| 81 |
+
hrm_refine_steps: int = 3,
|
| 82 |
+
hrm_refine_dim: int = 256,
|
| 83 |
+
# --- ModularMind-on-V2 additions (off/unused unless enabled) ---
|
| 84 |
+
use_latent_io: bool = False, # add latent output head + injection input path
|
| 85 |
+
d_latent: int = 256, # RecursiveLink contract dim (fixed across chain)
|
| 86 |
+
chain_position: int = 0, # context-doubling slot: ctx & theta scale by 2^pos
|
| 87 |
+
base_context: int = 8192, # ctx at position 0 (>= training --seq-len)
|
| 88 |
+
base_rope_theta: float = 10000.0,
|
| 89 |
+
**kwargs,
|
| 90 |
+
):
|
| 91 |
+
super().__init__(
|
| 92 |
+
bos_token_id=bos_token_id,
|
| 93 |
+
eos_token_id=eos_token_id,
|
| 94 |
+
tie_word_embeddings=tie_word_embeddings,
|
| 95 |
+
**kwargs,
|
| 96 |
+
)
|
| 97 |
+
self.vocab_size = vocab_size
|
| 98 |
+
self.hidden_size = hidden_size
|
| 99 |
+
self.num_hidden_layers = num_hidden_layers
|
| 100 |
+
self.max_position_embeddings = max_position_embeddings
|
| 101 |
+
self.rms_norm_eps = rms_norm_eps
|
| 102 |
+
self.initializer_range = initializer_range
|
| 103 |
+
self.hidden_dropout = hidden_dropout
|
| 104 |
+
|
| 105 |
+
self.num_attention_heads = num_attention_heads
|
| 106 |
+
self.num_key_value_heads = num_key_value_heads
|
| 107 |
+
self.q_lora_rank = q_lora_rank
|
| 108 |
+
self.head_dim = head_dim
|
| 109 |
+
self.qk_rope_head_dim = qk_rope_head_dim
|
| 110 |
+
self.nope_head_dim = head_dim - qk_rope_head_dim
|
| 111 |
+
self.o_lora_rank = o_lora_rank
|
| 112 |
+
self.attention_dropout = attention_dropout
|
| 113 |
+
self.rope_theta = rope_theta
|
| 114 |
+
self.use_derf = use_derf
|
| 115 |
+
self.use_xsa = use_xsa
|
| 116 |
+
|
| 117 |
+
self.use_moe = use_moe
|
| 118 |
+
self.moe_intermediate_size = moe_intermediate_size
|
| 119 |
+
self.n_routed_experts = n_routed_experts
|
| 120 |
+
self.n_shared_experts = n_shared_experts
|
| 121 |
+
self.num_experts_per_tok = num_experts_per_tok
|
| 122 |
+
self.norm_topk_prob = norm_topk_prob
|
| 123 |
+
self.scoring_func = scoring_func
|
| 124 |
+
self.routed_scaling_factor = routed_scaling_factor
|
| 125 |
+
self.num_hash_layers = num_hash_layers
|
| 126 |
+
self.moe_aux_loss_coef = moe_aux_loss_coef
|
| 127 |
+
self.moe_layers = moe_layers if moe_layers is not None else list(range(num_hidden_layers))
|
| 128 |
+
|
| 129 |
+
self.use_hyper_connections = use_hyper_connections
|
| 130 |
+
self.hc_mult = hc_mult
|
| 131 |
+
self.hc_sinkhorn_iters = hc_sinkhorn_iters
|
| 132 |
+
self.hc_eps = hc_eps
|
| 133 |
+
|
| 134 |
+
self.num_nextn_predict_layers = num_nextn_predict_layers
|
| 135 |
+
|
| 136 |
+
self.use_engram = use_engram
|
| 137 |
+
self.engram_compress_dim = engram_compress_dim
|
| 138 |
+
self.engram_num_heads = engram_num_heads
|
| 139 |
+
self.engram_table_size = engram_table_size
|
| 140 |
+
self.engram_max_ngram = engram_max_ngram
|
| 141 |
+
self.engram_gate_init_bias = engram_gate_init_bias
|
| 142 |
+
self.use_hrm_refine = use_hrm_refine
|
| 143 |
+
self.hrm_refine_steps = hrm_refine_steps
|
| 144 |
+
self.hrm_refine_dim = hrm_refine_dim
|
| 145 |
+
# --- ModularMind-on-V2 additions ---
|
| 146 |
+
self.use_latent_io = use_latent_io
|
| 147 |
+
self.d_latent = d_latent
|
| 148 |
+
self.chain_position = chain_position
|
| 149 |
+
self.base_context = base_context
|
| 150 |
+
self.base_rope_theta = base_rope_theta
|
| 151 |
+
# Context-doubling: each chain slot doubles ctx and rope theta.
|
| 152 |
+
# position 0 -> (8192, 10000); position 1 -> (16384, 20000); etc.
|
| 153 |
+
# Applied only when latent IO is on (i.e. this is a ModularMind specialist),
|
| 154 |
+
# so plain V2 keeps its own max_position_embeddings/rope_theta untouched.
|
| 155 |
+
if use_latent_io:
|
| 156 |
+
scale = 2 ** chain_position
|
| 157 |
+
self.max_position_embeddings = base_context * scale
|
| 158 |
+
self.rope_theta = base_rope_theta * scale
|
agents/modmind/language/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
agents/modmind/model.py
ADDED
|
@@ -0,0 +1,1011 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
model.py -- SpikeWhaleLM: combined architecture from SpikeTransformer (My Project) + NanoWhale.
|
| 3 |
+
|
| 4 |
+
Architecture flow:
|
| 5 |
+
Embedding
|
| 6 |
+
-> Engram delta (N-gram memory, My Project)
|
| 7 |
+
-> [expand to hc_mult copies if HC enabled]
|
| 8 |
+
-> N x TransformerBlock:
|
| 9 |
+
HC pre-op (NanoWhale) -> RMSNorm -> MLA+DERF+XSA Attention (combined)
|
| 10 |
+
-> HC post-op
|
| 11 |
+
HC pre-op -> RMSNorm -> MoE FFN w/ shared expert (NanoWhale)
|
| 12 |
+
-> HC post-op
|
| 13 |
+
-> [mean-pool hc_mult copies if HC enabled]
|
| 14 |
+
-> RMSNorm
|
| 15 |
+
-> LM head + MTP heads (NanoWhale)
|
| 16 |
+
|
| 17 |
+
Component origins:
|
| 18 |
+
RMSNorm, RotaryEmbedding -- both (standard)
|
| 19 |
+
Engram / DERFContextGate -- My Project
|
| 20 |
+
MLADerfXSAAttention -- MLA from NanoWhale + DERF+XSA from My Project
|
| 21 |
+
SparseMoEFFN w/ shared expert -- NanoWhale MoE structure + My Project aux loss
|
| 22 |
+
HyperConnectionLayer -- NanoWhale
|
| 23 |
+
SpikeWhaleLM + MTP heads -- NanoWhale
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
from typing import Optional, Tuple, List
|
| 31 |
+
from transformers import PreTrainedModel
|
| 32 |
+
from transformers.modeling_outputs import CausalLMOutputWithPast
|
| 33 |
+
from torch.utils.checkpoint import checkpoint as gradient_checkpoint
|
| 34 |
+
|
| 35 |
+
from config import SpikeWhaleConfig
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
# Primitives
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
class RMSNorm(nn.Module):
|
| 43 |
+
def __init__(self, dim: int, eps: float = 1e-6):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.eps = eps
|
| 46 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 47 |
+
|
| 48 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 49 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class RotaryEmbedding(nn.Module):
|
| 53 |
+
"""RoPE for the rope partition of Q and K (qk_rope_head_dim dims only)."""
|
| 54 |
+
|
| 55 |
+
def __init__(self, dim: int, max_positions: int = 4096, theta: float = 10000.0):
|
| 56 |
+
super().__init__()
|
| 57 |
+
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
|
| 58 |
+
self.register_buffer("inv_freq", inv_freq)
|
| 59 |
+
t = torch.arange(max_positions).float()
|
| 60 |
+
freqs = torch.outer(t, inv_freq)
|
| 61 |
+
self.register_buffer("cos_cache", freqs.cos())
|
| 62 |
+
self.register_buffer("sin_cache", freqs.sin())
|
| 63 |
+
|
| 64 |
+
def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> torch.Tensor:
|
| 65 |
+
"""
|
| 66 |
+
x: [B, H, S, rope_dim]
|
| 67 |
+
position_ids: [B, S]
|
| 68 |
+
"""
|
| 69 |
+
cos = self.cos_cache[position_ids].unsqueeze(1) # [B, 1, S, rope_dim//2]
|
| 70 |
+
sin = self.sin_cache[position_ids].unsqueeze(1)
|
| 71 |
+
d = cos.shape[-1]
|
| 72 |
+
x1, x2 = x[..., :d], x[..., d:]
|
| 73 |
+
return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
# Engram: N-gram hash lookup + DERF gate (My Project, preserved)
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
|
| 80 |
+
class TokenCompressor(nn.Module):
|
| 81 |
+
def __init__(self, embed_dim: int, compress_dim: int):
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.proj = nn.Linear(embed_dim, compress_dim, bias=False)
|
| 84 |
+
nn.init.normal_(self.proj.weight, std=0.02)
|
| 85 |
+
# BUGFIX: this projection feeds ONLY the integer hash index
|
| 86 |
+
# (idx = h.abs().long() % table_size) in MultiHeadHashLookup. The .long()
|
| 87 |
+
# cast is non-differentiable, so no gradient ever reaches this weight --
|
| 88 |
+
# it can never learn. Worse, _classify_params put it in the weight-decay
|
| 89 |
+
# group, so AdamW was steadily shrinking it toward zero and degrading the
|
| 90 |
+
# hash projection over a long run. Freeze it: a fixed random projection is
|
| 91 |
+
# exactly the right behavior for an LSH-style hash, and freezing drops it
|
| 92 |
+
# from the optimizer (saves state) and from weight decay. Checkpoint-safe:
|
| 93 |
+
# the parameter still exists and is still saved/loaded in state_dict.
|
| 94 |
+
self.proj.weight.requires_grad_(False)
|
| 95 |
+
|
| 96 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 97 |
+
return self.proj(x)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class MultiHeadHashLookup(nn.Module):
|
| 101 |
+
def __init__(self, num_heads: int, table_size: int,
|
| 102 |
+
compress_dim: int, out_dim: int, max_ngram: int = 3):
|
| 103 |
+
super().__init__()
|
| 104 |
+
self.num_heads = num_heads
|
| 105 |
+
self.table_size = table_size
|
| 106 |
+
self.max_ngram = max_ngram
|
| 107 |
+
self.out_dim = out_dim
|
| 108 |
+
|
| 109 |
+
self.tables = nn.ModuleList([
|
| 110 |
+
nn.Embedding(table_size, out_dim) for _ in range(num_heads)
|
| 111 |
+
])
|
| 112 |
+
for t in self.tables:
|
| 113 |
+
nn.init.normal_(t.weight, std=0.01)
|
| 114 |
+
|
| 115 |
+
for n in range(1, max_ngram + 1):
|
| 116 |
+
for k in range(n):
|
| 117 |
+
proj = torch.randn(num_heads, compress_dim)
|
| 118 |
+
proj = proj / (proj.norm(dim=1, keepdim=True) + 1e-8)
|
| 119 |
+
self.register_buffer(f"hash_proj_n{n}_p{k}", proj)
|
| 120 |
+
|
| 121 |
+
def forward(self, compressed: torch.Tensor) -> torch.Tensor:
|
| 122 |
+
"""
|
| 123 |
+
compressed: [B, S, compress_dim]
|
| 124 |
+
returns: [B, S, out_dim]
|
| 125 |
+
|
| 126 |
+
All positions are processed in parallel. The outer loop runs max_ngram
|
| 127 |
+
times (≤3), not S times (≤2048). Each iteration is a single matmul +
|
| 128 |
+
embedding lookup across the whole sequence, making this GPU-friendly
|
| 129 |
+
and compatible with torch.compile.
|
| 130 |
+
"""
|
| 131 |
+
B, S, _ = compressed.shape
|
| 132 |
+
device = compressed.device
|
| 133 |
+
out = torch.zeros(B, S, self.out_dim, device=device, dtype=compressed.dtype)
|
| 134 |
+
# Per-position normalization: tracks how many (n-gram × head) contributions
|
| 135 |
+
# each position receives. Positions near the start get fewer contributions
|
| 136 |
+
# because shorter n-grams don't exist yet (matches original causal behavior).
|
| 137 |
+
norm = torch.zeros(S, device=device)
|
| 138 |
+
|
| 139 |
+
for n in range(1, self.max_ngram + 1):
|
| 140 |
+
if S < n:
|
| 141 |
+
continue
|
| 142 |
+
valid_len = S - n + 1 # positions [n-1 .. S-1] are valid for order-n
|
| 143 |
+
start = n - 1
|
| 144 |
+
|
| 145 |
+
# Accumulate position-k contribution to the order-n hash.
|
| 146 |
+
# compressed[:, k : k+valid_len, :] is the k-th token of every n-gram
|
| 147 |
+
# window simultaneously → [B, valid_len, num_heads] after projection.
|
| 148 |
+
h = torch.zeros(B, valid_len, self.num_heads, device=device)
|
| 149 |
+
for k in range(n):
|
| 150 |
+
proj = getattr(self, f"hash_proj_n{n}_p{k}") # [num_heads, compress_dim]
|
| 151 |
+
h = h + torch.matmul(compressed[:, k:k + valid_len, :].float(), proj.t())
|
| 152 |
+
|
| 153 |
+
idx = h.abs().long() % self.table_size # [B, valid_len, num_heads]
|
| 154 |
+
|
| 155 |
+
for head_idx, table in enumerate(self.tables):
|
| 156 |
+
out[:, start:, :] = out[:, start:, :] + table(idx[:, :, head_idx])
|
| 157 |
+
|
| 158 |
+
norm[start:] += self.num_heads
|
| 159 |
+
|
| 160 |
+
# Cast back to input dtype: the norm division promotes bf16→float32 under autocast.
|
| 161 |
+
# Keeping the output in the same dtype as the input avoids a silent dtype mismatch
|
| 162 |
+
# when EngramModule adds this result back onto the (bf16) embedding tensor.
|
| 163 |
+
return (out / norm.view(1, -1, 1).clamp(min=1)).to(compressed.dtype)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class DERFContextGate(nn.Module):
|
| 167 |
+
"""
|
| 168 |
+
DERF gate: gate = gamma * erf(alpha * proj([retrieved, x]) + bias)
|
| 169 |
+
Positive probability = (gate + 1) / 2 applied to retrieved embedding.
|
| 170 |
+
Large negative init_bias keeps gate closed at start of training.
|
| 171 |
+
"""
|
| 172 |
+
def __init__(self, obs_size: int, init_bias: float = -4.0):
|
| 173 |
+
super().__init__()
|
| 174 |
+
self.proj = nn.Linear(obs_size * 2, obs_size)
|
| 175 |
+
self.alpha = nn.Parameter(torch.ones(obs_size))
|
| 176 |
+
self.bias = nn.Parameter(torch.full((obs_size,), init_bias))
|
| 177 |
+
self.gamma = nn.Parameter(torch.ones(obs_size))
|
| 178 |
+
|
| 179 |
+
def forward(self, retrieved: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
| 180 |
+
logits = self.proj(torch.cat([retrieved, x], dim=-1))
|
| 181 |
+
gate = self.gamma * ((torch.erf(self.alpha * logits + self.bias) + 1.0) / 2.0)
|
| 182 |
+
return retrieved * gate
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class EngramModule(nn.Module):
|
| 186 |
+
"""
|
| 187 |
+
N-gram hash lookup with DERF gate (My Project), fully vectorized.
|
| 188 |
+
All S positions are processed in parallel — the sequential Python loop
|
| 189 |
+
over sequence positions has been eliminated. The lookup now accepts the
|
| 190 |
+
full [B, S, compress_dim] compressed tensor and returns [B, S, H] in one pass.
|
| 191 |
+
"""
|
| 192 |
+
def __init__(self, cfg: SpikeWhaleConfig):
|
| 193 |
+
super().__init__()
|
| 194 |
+
self.compressor = TokenCompressor(cfg.hidden_size, cfg.engram_compress_dim)
|
| 195 |
+
self.lookup = MultiHeadHashLookup(
|
| 196 |
+
cfg.engram_num_heads, cfg.engram_table_size,
|
| 197 |
+
cfg.engram_compress_dim, cfg.hidden_size, cfg.engram_max_ngram,
|
| 198 |
+
)
|
| 199 |
+
self.gate = DERFContextGate(cfg.hidden_size, cfg.engram_gate_init_bias)
|
| 200 |
+
|
| 201 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 202 |
+
"""x: [B, S, H] -> engram_delta: [B, S, H]"""
|
| 203 |
+
compressed = self.compressor(x.detach()) # [B, S, compress_dim]
|
| 204 |
+
retrieved = self.lookup(compressed) # [B, S, H]
|
| 205 |
+
return self.gate(retrieved, x) # [B, S, H]
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# ---------------------------------------------------------------------------
|
| 209 |
+
# Hyper-Connections (NanoWhale, simplified)
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
+
|
| 212 |
+
class HyperConnectionLayer(nn.Module):
|
| 213 |
+
"""
|
| 214 |
+
Simplified Hyper-Connections for one sublayer (attention or FFN).
|
| 215 |
+
|
| 216 |
+
Maintains hc_mult parallel residual streams.
|
| 217 |
+
Pre-op: learned weighted average of hc_mult copies -> single hidden state for sublayer.
|
| 218 |
+
Post-op: sublayer output added to each copy with learned per-stream weights.
|
| 219 |
+
|
| 220 |
+
Full HC uses Sinkhorn-normalized 2D routing matrices; this uses softmax-normalized
|
| 221 |
+
1D weights for pre/post routing -- captures the same multi-stream routing spirit.
|
| 222 |
+
"""
|
| 223 |
+
def __init__(self, hidden_size: int, hc_mult: int,
|
| 224 |
+
sinkhorn_iters: int = 20, eps: float = 1e-6):
|
| 225 |
+
super().__init__()
|
| 226 |
+
self.hc_mult = hc_mult
|
| 227 |
+
# pre_weight: how to mix hc_mult copies into one sublayer input
|
| 228 |
+
# post_weight: how to distribute the sublayer delta to each copy
|
| 229 |
+
#
|
| 230 |
+
# BUGFIX: these must NOT be initialized identically across streams.
|
| 231 |
+
# The model expands the hidden state into hc_mult *identical* copies.
|
| 232 |
+
# With uniform pre/post weights, pre_op produces sum_i copy_i * w_i =
|
| 233 |
+
# copy * sum(softmax)=copy (all copies equal), and post_op adds the same
|
| 234 |
+
# delta to every copy -- so the streams stay byte-for-byte identical at
|
| 235 |
+
# every layer. When all streams are equal, the softmax Jacobian applied
|
| 236 |
+
# to the (equal) per-stream gradients is exactly zero, so pre_weight and
|
| 237 |
+
# post_weight receive ZERO gradient and never move off 1/hc_mult. The HC
|
| 238 |
+
# routing then learns nothing and just burns hc_mult x memory/compute.
|
| 239 |
+
#
|
| 240 |
+
# Breaking the post_weight symmetry at init makes the streams diverge
|
| 241 |
+
# after the first sublayer, which restores gradient flow to all HC
|
| 242 |
+
# weights. We center post_weight so softmax starts near-uniform (keeps
|
| 243 |
+
# the residual baseline ~unchanged) but with a distinct value per stream.
|
| 244 |
+
self.pre_weight = nn.Parameter(
|
| 245 |
+
torch.linspace(0.5, -0.5, hc_mult) / max(hc_mult, 1)
|
| 246 |
+
)
|
| 247 |
+
self.post_weight = nn.Parameter(
|
| 248 |
+
torch.linspace(-0.5, 0.5, hc_mult) / max(hc_mult, 1)
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def pre_op(self, copies: torch.Tensor) -> torch.Tensor:
|
| 252 |
+
"""copies: [B, hc_mult, S, H] -> [B, S, H]"""
|
| 253 |
+
w = F.softmax(self.pre_weight, dim=0) # [hc_mult]
|
| 254 |
+
return (copies * w.view(1, -1, 1, 1)).sum(dim=1)
|
| 255 |
+
|
| 256 |
+
def post_op(self, copies: torch.Tensor, delta: torch.Tensor) -> torch.Tensor:
|
| 257 |
+
"""
|
| 258 |
+
copies: [B, hc_mult, S, H]
|
| 259 |
+
delta: [B, S, H]
|
| 260 |
+
Returns updated copies: [B, hc_mult, S, H]
|
| 261 |
+
"""
|
| 262 |
+
w = F.softmax(self.post_weight, dim=0) # [hc_mult]
|
| 263 |
+
return copies + delta.unsqueeze(1) * w.view(1, -1, 1, 1)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ---------------------------------------------------------------------------
|
| 267 |
+
# MLA + DERF + XSA Attention (combined)
|
| 268 |
+
# ---------------------------------------------------------------------------
|
| 269 |
+
|
| 270 |
+
class MLADerfXSAAttention(nn.Module):
|
| 271 |
+
"""
|
| 272 |
+
Multi-Head Latent Attention (NanoWhale) with DERF scores + XSA correction (My Project).
|
| 273 |
+
|
| 274 |
+
MLA (from NanoWhale):
|
| 275 |
+
Q: hidden -> q_lora_rank (RMSNorm) -> num_heads * head_dim (low-rank projection)
|
| 276 |
+
K, V: hidden -> num_kv_heads * head_dim (direct, MQA by default with num_kv_heads=1)
|
| 277 |
+
Output: num_heads * head_dim -> o_lora_rank -> hidden (low-rank output)
|
| 278 |
+
Partial RoPE: applied only to the last qk_rope_head_dim dims of Q and K
|
| 279 |
+
|
| 280 |
+
DERF (from My Project):
|
| 281 |
+
Replaces softmax: erf(alpha * scores + bias) * gamma, shifted to [0,1] then normalized.
|
| 282 |
+
Per-head learnable alpha, bias, gamma.
|
| 283 |
+
|
| 284 |
+
XSA (from My Project):
|
| 285 |
+
After computing the weighted value sum y, subtract the component of y that
|
| 286 |
+
projects onto each position's own value vector. Forces the output to carry
|
| 287 |
+
only cross-position information, not echo the current token back.
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
def __init__(self, cfg: SpikeWhaleConfig):
|
| 291 |
+
super().__init__()
|
| 292 |
+
self.num_heads = cfg.num_attention_heads
|
| 293 |
+
self.num_kv_heads = cfg.num_key_value_heads
|
| 294 |
+
self.head_dim = cfg.head_dim
|
| 295 |
+
self.qk_rope_head_dim = cfg.qk_rope_head_dim
|
| 296 |
+
self.nope_head_dim = cfg.nope_head_dim
|
| 297 |
+
self.hidden_size = cfg.hidden_size
|
| 298 |
+
self.use_derf = cfg.use_derf
|
| 299 |
+
self.use_xsa = cfg.use_xsa
|
| 300 |
+
self.dropout_p = cfg.attention_dropout
|
| 301 |
+
self.kv_groups = self.num_heads // self.num_kv_heads
|
| 302 |
+
|
| 303 |
+
# Low-rank Q projection (MLA)
|
| 304 |
+
self.q_a_proj = nn.Linear(cfg.hidden_size, cfg.q_lora_rank, bias=False)
|
| 305 |
+
self.q_a_norm = RMSNorm(cfg.q_lora_rank, cfg.rms_norm_eps)
|
| 306 |
+
self.q_b_proj = nn.Linear(cfg.q_lora_rank, self.num_heads * self.head_dim, bias=False)
|
| 307 |
+
|
| 308 |
+
# Direct K, V projections (MQA/GQA)
|
| 309 |
+
self.k_proj = nn.Linear(cfg.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
|
| 310 |
+
self.v_proj = nn.Linear(cfg.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
|
| 311 |
+
|
| 312 |
+
# Low-rank output projection (MLA)
|
| 313 |
+
self.o_a_proj = nn.Linear(self.num_heads * self.head_dim, cfg.o_lora_rank, bias=False)
|
| 314 |
+
self.o_b_proj = nn.Linear(cfg.o_lora_rank, cfg.hidden_size, bias=False)
|
| 315 |
+
|
| 316 |
+
# Partial RoPE: applied to qk_rope_head_dim dims only
|
| 317 |
+
self.rope = RotaryEmbedding(
|
| 318 |
+
self.qk_rope_head_dim,
|
| 319 |
+
max_positions=cfg.max_position_embeddings,
|
| 320 |
+
theta=cfg.rope_theta,
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# DERF parameters: one per query head (My Project)
|
| 324 |
+
if self.use_derf:
|
| 325 |
+
self.derf_alpha = nn.Parameter(torch.ones(self.num_heads))
|
| 326 |
+
self.derf_bias = nn.Parameter(torch.zeros(self.num_heads))
|
| 327 |
+
self.derf_gamma = nn.Parameter(torch.ones(self.num_heads))
|
| 328 |
+
|
| 329 |
+
nn.init.normal_(self.q_a_proj.weight, std=cfg.initializer_range)
|
| 330 |
+
nn.init.normal_(self.q_b_proj.weight, std=cfg.initializer_range)
|
| 331 |
+
nn.init.normal_(self.k_proj.weight, std=cfg.initializer_range)
|
| 332 |
+
nn.init.normal_(self.v_proj.weight, std=cfg.initializer_range)
|
| 333 |
+
nn.init.normal_(self.o_a_proj.weight, std=cfg.initializer_range)
|
| 334 |
+
nn.init.normal_(self.o_b_proj.weight, std=cfg.initializer_range)
|
| 335 |
+
|
| 336 |
+
def forward(
|
| 337 |
+
self,
|
| 338 |
+
x: torch.Tensor,
|
| 339 |
+
position_ids: torch.Tensor,
|
| 340 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 341 |
+
past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
| 342 |
+
use_cache: bool = False,
|
| 343 |
+
) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
|
| 344 |
+
B, S, _ = x.shape
|
| 345 |
+
|
| 346 |
+
# Q via low-rank projection with intermediate norm (MLA)
|
| 347 |
+
q = self.q_a_norm(self.q_a_proj(x))
|
| 348 |
+
q = self.q_b_proj(q).view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
|
| 349 |
+
# [B, num_heads, S, head_dim]
|
| 350 |
+
|
| 351 |
+
# K, V direct projections
|
| 352 |
+
k = self.k_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
|
| 353 |
+
v = self.v_proj(x).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2)
|
| 354 |
+
|
| 355 |
+
# Partial RoPE: split into nope and rope partitions, rotate only the rope part
|
| 356 |
+
q_nope = q[..., :self.nope_head_dim]
|
| 357 |
+
q_rope = q[..., self.nope_head_dim:] # qk_rope_head_dim dims
|
| 358 |
+
k_nope = k[..., :self.nope_head_dim]
|
| 359 |
+
k_rope = k[..., self.nope_head_dim:]
|
| 360 |
+
|
| 361 |
+
q_rope = self.rope(q_rope, position_ids)
|
| 362 |
+
k_rope = self.rope(k_rope, position_ids)
|
| 363 |
+
|
| 364 |
+
q = torch.cat([q_nope, q_rope], dim=-1)
|
| 365 |
+
k = torch.cat([k_nope, k_rope], dim=-1)
|
| 366 |
+
|
| 367 |
+
# KV cache for inference
|
| 368 |
+
if past_key_value is not None:
|
| 369 |
+
k = torch.cat([past_key_value[0], k], dim=2)
|
| 370 |
+
v = torch.cat([past_key_value[1], v], dim=2)
|
| 371 |
+
present = (k, v) if use_cache else None
|
| 372 |
+
N = k.shape[2] # total key positions (past + current)
|
| 373 |
+
|
| 374 |
+
# Expand KV heads for MQA/GQA
|
| 375 |
+
if self.kv_groups > 1:
|
| 376 |
+
k = k.unsqueeze(2).expand(-1, -1, self.kv_groups, -1, -1).reshape(
|
| 377 |
+
B, self.num_heads, N, self.head_dim)
|
| 378 |
+
v = v.unsqueeze(2).expand(-1, -1, self.kv_groups, -1, -1).reshape(
|
| 379 |
+
B, self.num_heads, N, self.head_dim)
|
| 380 |
+
|
| 381 |
+
# Scaled dot-product attention.
|
| 382 |
+
if self.use_derf:
|
| 383 |
+
# DERF replaces softmax with a custom erf nonlinearity, so it cannot
|
| 384 |
+
# use the fused kernel and must materialize scores explicitly.
|
| 385 |
+
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
|
| 386 |
+
|
| 387 |
+
# Build boolean mask for causality (this avoids the -inf math errors)
|
| 388 |
+
if attention_mask is None and past_key_value is None:
|
| 389 |
+
is_masked = torch.triu(torch.ones(S, N, dtype=torch.bool, device=scores.device), diagonal=N - S + 1).unsqueeze(0).unsqueeze(0)
|
| 390 |
+
else:
|
| 391 |
+
is_masked = (attention_mask < -1.0) if attention_mask is not None else torch.zeros_like(scores, dtype=torch.bool)
|
| 392 |
+
|
| 393 |
+
# FIX 2: Do NOT use float('-inf'). If alpha ever hits 0.0, 0.0 * -inf = NaN.
|
| 394 |
+
# Use a safe negative scalar (-10000.0) for masked positions.
|
| 395 |
+
safe_scores = scores.masked_fill(is_masked, -10000.0)
|
| 396 |
+
|
| 397 |
+
a = self.derf_alpha.view(1, -1, 1, 1)
|
| 398 |
+
b = self.derf_bias.view(1, -1, 1, 1)
|
| 399 |
+
g = self.derf_gamma.view(1, -1, 1, 1)
|
| 400 |
+
|
| 401 |
+
attn_weights = g * torch.erf(a * safe_scores + b) # [-gamma, gamma]
|
| 402 |
+
attn_weights = (attn_weights + g) / 2.0 # shift to [0, gamma]
|
| 403 |
+
attn_weights = attn_weights.masked_fill(is_masked, 0.0) # enforce causal mask safely
|
| 404 |
+
attn_weights = attn_weights / (attn_weights.sum(dim=-1, keepdim=True) + 1e-8)
|
| 405 |
+
|
| 406 |
+
if self.dropout_p > 0 and self.training:
|
| 407 |
+
attn_weights = F.dropout(attn_weights, p=self.dropout_p)
|
| 408 |
+
|
| 409 |
+
y = torch.matmul(attn_weights, v) # [B, num_heads, S, head_dim]
|
| 410 |
+
else:
|
| 411 |
+
# OPTIMIZATION: standard (softmax) attention goes through the fused
|
| 412 |
+
# scaled_dot_product_attention kernel (FlashAttention / mem-efficient
|
| 413 |
+
# backends). This is the hot path during pretraining (use_derf=False)
|
| 414 |
+
# and is much faster + lower memory than materializing [B,H,S,N]
|
| 415 |
+
# scores and a softmax. SDPA already scales by 1/sqrt(head_dim).
|
| 416 |
+
#
|
| 417 |
+
# CONTIGUITY FIX: with MQA/GQA, k and v above are built via
|
| 418 |
+
# .unsqueeze(2).expand(...).reshape(...). Under torch.compile, inductor
|
| 419 |
+
# can trace the broadcasted (zero-stride) view through to the fused
|
| 420 |
+
# flash-attention BACKWARD kernel, whose meta-kernel then asserts on the
|
| 421 |
+
# mismatched stride (e.g. "stride 120==245760 at dim=1") and aborts.
|
| 422 |
+
# Forcing contiguity guarantees standard strides into the fused kernel.
|
| 423 |
+
q = q.contiguous()
|
| 424 |
+
k = k.contiguous()
|
| 425 |
+
v = v.contiguous()
|
| 426 |
+
drop = self.dropout_p if self.training else 0.0
|
| 427 |
+
if past_key_value is None and attention_mask is None:
|
| 428 |
+
# Prefill / training: pure causal mask, no materialization needed.
|
| 429 |
+
y = F.scaled_dot_product_attention(q, k, v, is_causal=True, dropout_p=drop)
|
| 430 |
+
else:
|
| 431 |
+
# Incremental decode or a provided mask: pass an explicit boolean
|
| 432 |
+
# keep-mask (True = attend). SDPA fills masked positions with -inf.
|
| 433 |
+
if attention_mask is not None:
|
| 434 |
+
is_masked = (attention_mask < -1.0)
|
| 435 |
+
else:
|
| 436 |
+
is_masked = torch.triu(
|
| 437 |
+
torch.ones(S, N, dtype=torch.bool, device=q.device),
|
| 438 |
+
diagonal=N - S + 1,
|
| 439 |
+
).unsqueeze(0).unsqueeze(0)
|
| 440 |
+
y = F.scaled_dot_product_attention(
|
| 441 |
+
q, k, v, attn_mask=~is_masked, dropout_p=drop)
|
| 442 |
+
|
| 443 |
+
# XSA: remove self-projection from output (My Project)
|
| 444 |
+
# For each query position s, subtract the component of y[:,:,s,:] that
|
| 445 |
+
# projects onto the normalized value vector at the same position.
|
| 446 |
+
if self.use_xsa:
|
| 447 |
+
past_len = N - S
|
| 448 |
+
v_self = v[:, :, past_len:past_len + S, :] # [B, H, S, D]
|
| 449 |
+
vn = v_self / (v_self.norm(dim=-1, keepdim=True) + 1e-8)
|
| 450 |
+
projection = (y * vn).sum(dim=-1, keepdim=True) * vn
|
| 451 |
+
y = y - projection
|
| 452 |
+
|
| 453 |
+
# Low-rank output projection (MLA)
|
| 454 |
+
y = y.transpose(1, 2).contiguous().view(B, S, self.num_heads * self.head_dim)
|
| 455 |
+
y = self.o_b_proj(self.o_a_proj(y))
|
| 456 |
+
return y, present
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# ---------------------------------------------------------------------------
|
| 460 |
+
# MoE FFN: shared expert + sqrtsoftplus + hash routing (NanoWhale) + aux loss (My Project)
|
| 461 |
+
# ---------------------------------------------------------------------------
|
| 462 |
+
|
| 463 |
+
class ExpertFFN(nn.Module):
|
| 464 |
+
"""Single SwiGLU expert."""
|
| 465 |
+
def __init__(self, hidden_size: int, intermediate_size: int):
|
| 466 |
+
super().__init__()
|
| 467 |
+
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 468 |
+
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 469 |
+
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
|
| 470 |
+
|
| 471 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 472 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def sqrtsoftplus(x: torch.Tensor) -> torch.Tensor:
|
| 476 |
+
"""sqrt(softplus(x)) = sqrt(log(1+exp(x))). NanoWhale expert scoring."""
|
| 477 |
+
# FIX 1: Added 1e-8. If F.softplus(x) evaluates to 0.0, torch.sqrt(0) produces NaN gradients on backward pass.
|
| 478 |
+
return torch.sqrt(F.softplus(x) + 1e-8)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class SparseMoEFFN(nn.Module):
|
| 482 |
+
"""
|
| 483 |
+
Combines NanoWhale MoE structure with My Project aux loss:
|
| 484 |
+
- n_shared_experts always-active experts (NanoWhale)
|
| 485 |
+
- n_routed_experts sparse routed experts, top-k activation
|
| 486 |
+
- sqrtsoftplus scoring (NanoWhale) vs softmax
|
| 487 |
+
- hash routing for early layers (NanoWhale)
|
| 488 |
+
- norm_topk_prob + routed_scaling_factor (NanoWhale)
|
| 489 |
+
- load-balancing aux loss (My Project)
|
| 490 |
+
"""
|
| 491 |
+
def __init__(self, cfg: SpikeWhaleConfig, layer_idx: int = 0):
|
| 492 |
+
super().__init__()
|
| 493 |
+
self.n_routed_experts = cfg.n_routed_experts
|
| 494 |
+
self.n_shared_experts = cfg.n_shared_experts
|
| 495 |
+
self.num_experts_per_tok = cfg.num_experts_per_tok
|
| 496 |
+
self.norm_topk_prob = cfg.norm_topk_prob
|
| 497 |
+
self.scoring_func = cfg.scoring_func
|
| 498 |
+
self.routed_scaling_factor = cfg.routed_scaling_factor
|
| 499 |
+
self.use_hash_routing = layer_idx < cfg.num_hash_layers
|
| 500 |
+
self.aux_loss_coef = cfg.moe_aux_loss_coef
|
| 501 |
+
|
| 502 |
+
self.router = nn.Linear(cfg.hidden_size, cfg.n_routed_experts, bias=False)
|
| 503 |
+
self.experts = nn.ModuleList([
|
| 504 |
+
ExpertFFN(cfg.hidden_size, cfg.moe_intermediate_size)
|
| 505 |
+
for _ in range(cfg.n_routed_experts)
|
| 506 |
+
])
|
| 507 |
+
self.shared_experts = nn.ModuleList([
|
| 508 |
+
ExpertFFN(cfg.hidden_size, cfg.moe_intermediate_size)
|
| 509 |
+
for _ in range(cfg.n_shared_experts)
|
| 510 |
+
]) if cfg.n_shared_experts > 0 else None
|
| 511 |
+
|
| 512 |
+
self._last_aux_loss: Optional[torch.Tensor] = None
|
| 513 |
+
|
| 514 |
+
def forward(self, x: torch.Tensor, position_ids: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 515 |
+
B, S, H = x.shape
|
| 516 |
+
x_flat = x.view(B * S, H)
|
| 517 |
+
T = B * S
|
| 518 |
+
|
| 519 |
+
# Shared experts: always active (NanoWhale)
|
| 520 |
+
shared_out = torch.zeros_like(x_flat)
|
| 521 |
+
if self.shared_experts:
|
| 522 |
+
for expert in self.shared_experts:
|
| 523 |
+
shared_out = shared_out + expert(x_flat)
|
| 524 |
+
if len(self.shared_experts) > 1:
|
| 525 |
+
shared_out = shared_out / len(self.shared_experts)
|
| 526 |
+
|
| 527 |
+
# Router
|
| 528 |
+
if self.use_hash_routing:
|
| 529 |
+
# Hash routing: deterministic assignment without learned router (NanoWhale).
|
| 530 |
+
# Assign each of the num_experts_per_tok slots a DISTINCT expert by cycling:
|
| 531 |
+
# token at absolute position p -> experts [p%n, (p+1)%n, ..., (p+k-1)%n].
|
| 532 |
+
#
|
| 533 |
+
# BUGFIX: the assignment must key off the token's ABSOLUTE sequence
|
| 534 |
+
# position, not torch.arange(T) (its index in the current flattened
|
| 535 |
+
# batch). With arange(T), incremental KV-cache decoding (S=1) always
|
| 536 |
+
# sees index 0 and routes every token to expert 0, so generation used
|
| 537 |
+
# a different expert assignment than training and silently diverged.
|
| 538 |
+
# Using position_ids makes prefill, full-sequence training, and
|
| 539 |
+
# step-by-step generation all agree. (For S divisible by n_experts,
|
| 540 |
+
# this matches the previous training-time behavior exactly, so existing
|
| 541 |
+
# checkpoints stay valid.)
|
| 542 |
+
if position_ids is not None:
|
| 543 |
+
base = (position_ids.reshape(T, 1) % self.n_routed_experts).long()
|
| 544 |
+
else:
|
| 545 |
+
base = (torch.arange(T, device=x.device) % self.n_routed_experts).unsqueeze(1)
|
| 546 |
+
offsets = torch.arange(self.num_experts_per_tok, device=x.device) # [k]
|
| 547 |
+
top_k_indices = (base + offsets.unsqueeze(0)) % self.n_routed_experts # [T, k]
|
| 548 |
+
top_k_weights = torch.ones(T, self.num_experts_per_tok, device=x.device) / self.num_experts_per_tok
|
| 549 |
+
self._last_aux_loss = None
|
| 550 |
+
else:
|
| 551 |
+
router_logits = self.router(x_flat)
|
| 552 |
+
|
| 553 |
+
if self.scoring_func == "sqrtsoftplus":
|
| 554 |
+
routing_scores = sqrtsoftplus(router_logits)
|
| 555 |
+
else:
|
| 556 |
+
routing_scores = F.softmax(router_logits, dim=-1)
|
| 557 |
+
|
| 558 |
+
top_k_scores, top_k_indices = torch.topk(routing_scores, self.num_experts_per_tok, dim=-1)
|
| 559 |
+
|
| 560 |
+
if self.norm_topk_prob:
|
| 561 |
+
top_k_weights = top_k_scores / (top_k_scores.sum(dim=-1, keepdim=True) + 1e-8)
|
| 562 |
+
else:
|
| 563 |
+
top_k_weights = top_k_scores
|
| 564 |
+
top_k_weights = top_k_weights * self.routed_scaling_factor
|
| 565 |
+
|
| 566 |
+
# Load-balancing aux loss (My Project)
|
| 567 |
+
softmax_probs = F.softmax(router_logits, dim=-1)
|
| 568 |
+
expert_mask = torch.zeros_like(softmax_probs)
|
| 569 |
+
expert_mask.scatter_(1, top_k_indices, 1.0)
|
| 570 |
+
f_e = expert_mask.mean(0)
|
| 571 |
+
p_e = softmax_probs.mean(0)
|
| 572 |
+
self._last_aux_loss = self.n_routed_experts * (f_e * p_e).sum() * self.aux_loss_coef
|
| 573 |
+
|
| 574 |
+
# Dispatch tokens to routed experts
|
| 575 |
+
out = torch.zeros_like(x_flat)
|
| 576 |
+
for expert_idx, expert in enumerate(self.experts):
|
| 577 |
+
token_mask = (top_k_indices == expert_idx).any(dim=-1)
|
| 578 |
+
if not token_mask.any():
|
| 579 |
+
continue
|
| 580 |
+
expert_input = x_flat[token_mask]
|
| 581 |
+
expert_output = expert(expert_input)
|
| 582 |
+
k_pos = (top_k_indices[token_mask] == expert_idx).nonzero(as_tuple=False)
|
| 583 |
+
weights = top_k_weights[token_mask][k_pos[:, 0], k_pos[:, 1]].unsqueeze(-1)
|
| 584 |
+
out[token_mask] = out[token_mask] + expert_output * weights
|
| 585 |
+
|
| 586 |
+
out = out + shared_out
|
| 587 |
+
return out.view(B, S, H)
|
| 588 |
+
|
| 589 |
+
def get_aux_loss(self) -> Optional[torch.Tensor]:
|
| 590 |
+
# Return None when hash routing (no aux loss) or when forward hasn't run yet.
|
| 591 |
+
# Returning torch.tensor(0.0) here would be a CPU tensor and cause a device
|
| 592 |
+
# mismatch when added to the CUDA total_aux_loss in SpikeWhaleModel.
|
| 593 |
+
return self._last_aux_loss
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
class DenseFFN(nn.Module):
|
| 597 |
+
"""Dense SwiGLU FFN for non-MoE layers."""
|
| 598 |
+
def __init__(self, cfg: SpikeWhaleConfig):
|
| 599 |
+
super().__init__()
|
| 600 |
+
self.gate_proj = nn.Linear(cfg.hidden_size, cfg.moe_intermediate_size, bias=False)
|
| 601 |
+
self.up_proj = nn.Linear(cfg.hidden_size, cfg.moe_intermediate_size, bias=False)
|
| 602 |
+
self.down_proj = nn.Linear(cfg.moe_intermediate_size, cfg.hidden_size, bias=False)
|
| 603 |
+
|
| 604 |
+
def forward(self, x: torch.Tensor, position_ids: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 605 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 606 |
+
|
| 607 |
+
def get_aux_loss(self) -> Optional[torch.Tensor]:
|
| 608 |
+
return None # dense layers have no aux loss; None avoids CPU-tensor device mismatch
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
# ---------------------------------------------------------------------------
|
| 612 |
+
# Transformer block with Hyper-Connections
|
| 613 |
+
# ---------------------------------------------------------------------------
|
| 614 |
+
|
| 615 |
+
class TransformerBlock(nn.Module):
|
| 616 |
+
"""
|
| 617 |
+
Transformer block combining all features:
|
| 618 |
+
- Hyper-Connections: pre/post routing through hc_mult streams (NanoWhale)
|
| 619 |
+
- MLA + DERF + XSA attention (combined)
|
| 620 |
+
- MoE FFN with shared expert (NanoWhale) + aux loss (My Project)
|
| 621 |
+
"""
|
| 622 |
+
def __init__(self, cfg: SpikeWhaleConfig, layer_idx: int):
|
| 623 |
+
super().__init__()
|
| 624 |
+
self.use_hc = cfg.use_hyper_connections
|
| 625 |
+
self.hidden_dropout = cfg.hidden_dropout
|
| 626 |
+
|
| 627 |
+
self.attn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 628 |
+
self.attn = MLADerfXSAAttention(cfg)
|
| 629 |
+
self.ffn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 630 |
+
|
| 631 |
+
if cfg.use_moe and layer_idx in cfg.moe_layers:
|
| 632 |
+
self.ffn = SparseMoEFFN(cfg, layer_idx)
|
| 633 |
+
self.is_moe = True
|
| 634 |
+
else:
|
| 635 |
+
self.ffn = DenseFFN(cfg)
|
| 636 |
+
self.is_moe = False
|
| 637 |
+
|
| 638 |
+
if self.use_hc:
|
| 639 |
+
self.hc_attn = HyperConnectionLayer(cfg.hidden_size, cfg.hc_mult,
|
| 640 |
+
cfg.hc_sinkhorn_iters, cfg.hc_eps)
|
| 641 |
+
self.hc_ffn = HyperConnectionLayer(cfg.hidden_size, cfg.hc_mult,
|
| 642 |
+
cfg.hc_sinkhorn_iters, cfg.hc_eps)
|
| 643 |
+
|
| 644 |
+
def forward(
|
| 645 |
+
self,
|
| 646 |
+
x: torch.Tensor, # [B, hc_mult, S, H] if HC else [B, S, H]
|
| 647 |
+
position_ids: torch.Tensor,
|
| 648 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 649 |
+
past_key_value: Optional[Tuple] = None,
|
| 650 |
+
use_cache: bool = False,
|
| 651 |
+
) -> Tuple[torch.Tensor, Optional[Tuple], Optional[torch.Tensor]]:
|
| 652 |
+
|
| 653 |
+
# --- Attention sub-layer ---
|
| 654 |
+
if self.use_hc:
|
| 655 |
+
h = self.hc_attn.pre_op(x) # [B, S, H]
|
| 656 |
+
else:
|
| 657 |
+
h = x
|
| 658 |
+
|
| 659 |
+
attn_out, present = self.attn(
|
| 660 |
+
self.attn_norm(h), position_ids, attention_mask, past_key_value, use_cache
|
| 661 |
+
)
|
| 662 |
+
attn_out = F.dropout(attn_out, p=self.hidden_dropout, training=self.training)
|
| 663 |
+
|
| 664 |
+
if self.use_hc:
|
| 665 |
+
x = self.hc_attn.post_op(x, attn_out)
|
| 666 |
+
h = self.hc_ffn.pre_op(x) # [B, S, H]
|
| 667 |
+
else:
|
| 668 |
+
h = h + attn_out
|
| 669 |
+
|
| 670 |
+
# --- FFN sub-layer ---
|
| 671 |
+
ffn_out = self.ffn(self.ffn_norm(h), position_ids)
|
| 672 |
+
ffn_out = F.dropout(ffn_out, p=self.hidden_dropout, training=self.training)
|
| 673 |
+
|
| 674 |
+
if self.use_hc:
|
| 675 |
+
x = self.hc_ffn.post_op(x, ffn_out)
|
| 676 |
+
else:
|
| 677 |
+
x = h + ffn_out
|
| 678 |
+
|
| 679 |
+
return x, present, self.ffn.get_aux_loss()
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
# ---------------------------------------------------------------------------
|
| 683 |
+
# Full model
|
| 684 |
+
# ---------------------------------------------------------------------------
|
| 685 |
+
|
| 686 |
+
class HRMRefinementBlock(nn.Module):
|
| 687 |
+
"""
|
| 688 |
+
HRM-INSPIRED iterative refinement (EXPERIMENTAL, off by default). NOT the full
|
| 689 |
+
Hierarchical Reasoning Model -- only the iterative-refinement mechanism that the
|
| 690 |
+
independent ARC-Prize ablation found carried most of HRM's benefit, adapted to a
|
| 691 |
+
causal LM's final hidden state.
|
| 692 |
+
|
| 693 |
+
Runs N inner steps; each computes a small gated update conditioned on the current
|
| 694 |
+
state AND the original ('anchor') input. Per-step gate inits at 0 and up.weight is
|
| 695 |
+
zero-init -> the block is an EXACT identity at init, so enabling it cannot hurt a
|
| 696 |
+
fresh model; it only contributes if training opens the gate. Pointwise over
|
| 697 |
+
positions -> causal-safe (no future-token leakage). In/out [B,S,H].
|
| 698 |
+
"""
|
| 699 |
+
def __init__(self, hidden_size: int, refine_dim: int, steps: int, eps: float = 1e-6):
|
| 700 |
+
super().__init__()
|
| 701 |
+
self.steps = steps
|
| 702 |
+
self.norm = RMSNorm(hidden_size, eps)
|
| 703 |
+
self.down = nn.Linear(hidden_size * 2, refine_dim, bias=False)
|
| 704 |
+
self.up = nn.Linear(refine_dim, hidden_size, bias=False)
|
| 705 |
+
self.gate = nn.Parameter(torch.zeros(steps))
|
| 706 |
+
nn.init.normal_(self.down.weight, std=0.02)
|
| 707 |
+
nn.init.zeros_(self.up.weight)
|
| 708 |
+
|
| 709 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 710 |
+
anchor = x
|
| 711 |
+
h = x
|
| 712 |
+
for t in range(self.steps):
|
| 713 |
+
inp = torch.cat([self.norm(h), anchor], dim=-1)
|
| 714 |
+
update = self.up(F.silu(self.down(inp)))
|
| 715 |
+
h = h + torch.tanh(self.gate[t]) * update
|
| 716 |
+
return h
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
class LatentProjection(nn.Module):
|
| 720 |
+
"""ModularMind-on-V2: pool final hidden state -> d_latent output vector.
|
| 721 |
+
Mirrors ModularMind's contract: mean-pool over sequence, ReLU^2 activation
|
| 722 |
+
(sparse latent codes), Xavier init (NOT zero) so the latent carries signal
|
| 723 |
+
from step 1 — zero-init would make the chain unable to bootstrap."""
|
| 724 |
+
def __init__(self, hidden_size: int, d_latent: int, eps: float = 1e-6):
|
| 725 |
+
super().__init__()
|
| 726 |
+
self.proj1 = nn.Linear(hidden_size, hidden_size, bias=False)
|
| 727 |
+
self.proj2 = nn.Linear(hidden_size, d_latent, bias=False)
|
| 728 |
+
self.norm = RMSNorm(d_latent, eps)
|
| 729 |
+
nn.init.xavier_uniform_(self.proj1.weight)
|
| 730 |
+
nn.init.xavier_uniform_(self.proj2.weight)
|
| 731 |
+
|
| 732 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 733 |
+
pooled = x.mean(dim=1) # [B, S, H] -> [B, H]
|
| 734 |
+
h = torch.relu(self.proj1(pooled)) ** 2
|
| 735 |
+
return self.norm(self.proj2(h)) # [B, d_latent]
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
class LatentInjection(nn.Module):
|
| 739 |
+
"""ModularMind-on-V2: fold an incoming d_latent vector into embeddings.
|
| 740 |
+
Broadcast across positions, ReGLU-gated add. Gate starts SMALL (not exactly
|
| 741 |
+
zero): the injection is near-identity at init (stable) while still passing a
|
| 742 |
+
little gradient, so the upstream RecursiveLink + specialist can bootstrap from
|
| 743 |
+
step 1. (Exact-zero gate would block all gradient to the link -- the
|
| 744 |
+
bootstrapping problem ModularMind's LatentProjection docstring warns about.)
|
| 745 |
+
This is the INPUT side of RecursiveLink (the prev specialist's latent)."""
|
| 746 |
+
def __init__(self, hidden_size: int, d_latent: int, eps: float = 1e-6,
|
| 747 |
+
gate_init: float = 1e-3):
|
| 748 |
+
super().__init__()
|
| 749 |
+
self.up = nn.Linear(d_latent, hidden_size, bias=False)
|
| 750 |
+
self.norm = RMSNorm(hidden_size, eps)
|
| 751 |
+
self.value_proj = nn.Linear(hidden_size, hidden_size, bias=False)
|
| 752 |
+
self.gate_proj = nn.Linear(hidden_size, hidden_size, bias=False)
|
| 753 |
+
self.gate_init = gate_init
|
| 754 |
+
nn.init.xavier_uniform_(self.up.weight)
|
| 755 |
+
nn.init.xavier_uniform_(self.value_proj.weight)
|
| 756 |
+
nn.init.normal_(self.gate_proj.weight, std=gate_init) # small, not zero
|
| 757 |
+
|
| 758 |
+
def forward(self, x: torch.Tensor, latent: torch.Tensor) -> torch.Tensor:
|
| 759 |
+
# x: [B, S, H], latent: [B, d_latent]
|
| 760 |
+
inj = self.norm(self.up(latent)).unsqueeze(1) # [B, 1, H] broadcast over S
|
| 761 |
+
value = self.value_proj(inj)
|
| 762 |
+
gate = torch.relu(self.gate_proj(inj))
|
| 763 |
+
return x + value * gate
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
class RecursiveLink(nn.Module):
|
| 767 |
+
"""ModularMind cross-specialist bridge, V2 build. Converts one specialist's
|
| 768 |
+
output latent into the next specialist's input latent. ReGLU + residual,
|
| 769 |
+
single shared module reused for every hop. Fully differentiable."""
|
| 770 |
+
def __init__(self, d_latent: int = 256, expansion: float = 2.0):
|
| 771 |
+
super().__init__()
|
| 772 |
+
d_hidden = int(d_latent * expansion)
|
| 773 |
+
self.norm = nn.LayerNorm(d_latent)
|
| 774 |
+
self.value_proj = nn.Linear(d_latent, d_hidden, bias=False)
|
| 775 |
+
self.gate_proj = nn.Linear(d_latent, d_hidden, bias=False)
|
| 776 |
+
self.down = nn.Linear(d_hidden, d_latent, bias=False)
|
| 777 |
+
self.residual_gate = nn.Parameter(torch.ones(1))
|
| 778 |
+
nn.init.xavier_uniform_(self.value_proj.weight)
|
| 779 |
+
nn.init.xavier_uniform_(self.gate_proj.weight)
|
| 780 |
+
nn.init.xavier_uniform_(self.down.weight)
|
| 781 |
+
|
| 782 |
+
def forward(self, z: torch.Tensor) -> torch.Tensor:
|
| 783 |
+
n = self.norm(z)
|
| 784 |
+
h = self.value_proj(n) * torch.relu(self.gate_proj(n))
|
| 785 |
+
return z + self.residual_gate * self.down(h)
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
class SpikeWhaleModel(nn.Module):
|
| 789 |
+
"""Decoder stack without LM head."""
|
| 790 |
+
|
| 791 |
+
def __init__(self, cfg: SpikeWhaleConfig):
|
| 792 |
+
super().__init__()
|
| 793 |
+
self.cfg = cfg
|
| 794 |
+
self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
|
| 795 |
+
nn.init.normal_(self.embed_tokens.weight, std=cfg.initializer_range)
|
| 796 |
+
|
| 797 |
+
self.engram = EngramModule(cfg) if cfg.use_engram else None
|
| 798 |
+
self.layers = nn.ModuleList([
|
| 799 |
+
TransformerBlock(cfg, layer_idx=i)
|
| 800 |
+
for i in range(cfg.num_hidden_layers)
|
| 801 |
+
])
|
| 802 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 803 |
+
self.hrm_refine = (
|
| 804 |
+
HRMRefinementBlock(cfg.hidden_size, cfg.hrm_refine_dim,
|
| 805 |
+
cfg.hrm_refine_steps, cfg.rms_norm_eps)
|
| 806 |
+
if getattr(cfg, "use_hrm_refine", False) else None
|
| 807 |
+
)
|
| 808 |
+
# ModularMind-on-V2: latent input/output (off unless use_latent_io)
|
| 809 |
+
if getattr(cfg, "use_latent_io", False):
|
| 810 |
+
self.latent_inject = LatentInjection(cfg.hidden_size, cfg.d_latent, cfg.rms_norm_eps)
|
| 811 |
+
self.latent_out = LatentProjection(cfg.hidden_size, cfg.d_latent, cfg.rms_norm_eps)
|
| 812 |
+
else:
|
| 813 |
+
self.latent_inject = None
|
| 814 |
+
self.latent_out = None
|
| 815 |
+
self.gradient_checkpointing = False
|
| 816 |
+
|
| 817 |
+
def reset_latent_gate(self):
|
| 818 |
+
"""Re-init the injection gate SMALL (not zero). Must be called AFTER any HF
|
| 819 |
+
post_init/_init_weights pass, which otherwise re-randomizes the gate to full
|
| 820 |
+
scale. Small-but-nonzero keeps injection near-identity at start while letting
|
| 821 |
+
gradient reach the upstream RecursiveLink (so the chain can bootstrap)."""
|
| 822 |
+
if self.latent_inject is not None:
|
| 823 |
+
nn.init.normal_(self.latent_inject.gate_proj.weight,
|
| 824 |
+
std=self.latent_inject.gate_init)
|
| 825 |
+
|
| 826 |
+
def forward(
|
| 827 |
+
self,
|
| 828 |
+
input_ids: torch.Tensor,
|
| 829 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 830 |
+
position_ids: Optional[torch.Tensor] = None,
|
| 831 |
+
past_key_values: Optional[List[Tuple]] = None,
|
| 832 |
+
use_cache: bool = False,
|
| 833 |
+
inject_latent: Optional[torch.Tensor] = None,
|
| 834 |
+
) -> Tuple[torch.Tensor, Optional[List[Tuple]], torch.Tensor]:
|
| 835 |
+
B, S = input_ids.shape
|
| 836 |
+
device = input_ids.device
|
| 837 |
+
|
| 838 |
+
if position_ids is None:
|
| 839 |
+
past_len = past_key_values[0][0].shape[2] if past_key_values else 0
|
| 840 |
+
position_ids = torch.arange(
|
| 841 |
+
past_len, past_len + S, device=device
|
| 842 |
+
).unsqueeze(0).expand(B, -1)
|
| 843 |
+
|
| 844 |
+
# Token embedding
|
| 845 |
+
x = self.embed_tokens(input_ids) # [B, S, H]
|
| 846 |
+
|
| 847 |
+
# Engram N-gram delta (My Project)
|
| 848 |
+
if self.engram is not None:
|
| 849 |
+
x = x + self.engram(x)
|
| 850 |
+
|
| 851 |
+
# ModularMind-on-V2: inject the previous specialist's latent (broadcast
|
| 852 |
+
# across positions, ReGLU-gated). No-op at init (gate zero) and skipped
|
| 853 |
+
# entirely if no latent is passed.
|
| 854 |
+
if self.latent_inject is not None and inject_latent is not None:
|
| 855 |
+
x = self.latent_inject(x, inject_latent)
|
| 856 |
+
|
| 857 |
+
# Expand to hc_mult streams for Hyper-Connections (NanoWhale)
|
| 858 |
+
if self.cfg.use_hyper_connections:
|
| 859 |
+
x = x.unsqueeze(1).expand(-1, self.cfg.hc_mult, -1, -1).clone()
|
| 860 |
+
# [B, hc_mult, S, H]
|
| 861 |
+
|
| 862 |
+
present_key_values = [] if use_cache else None
|
| 863 |
+
total_aux_loss = torch.tensor(0.0, device=device)
|
| 864 |
+
|
| 865 |
+
for layer_idx, layer in enumerate(self.layers):
|
| 866 |
+
pkv = past_key_values[layer_idx] if past_key_values else None
|
| 867 |
+
|
| 868 |
+
if self.gradient_checkpointing and self.training:
|
| 869 |
+
# Gradient checkpointing with use_reentrant=False (NanoWhale)
|
| 870 |
+
x, present, aux_loss = gradient_checkpoint(
|
| 871 |
+
layer, x, position_ids, attention_mask, None, False,
|
| 872 |
+
use_reentrant=False,
|
| 873 |
+
)
|
| 874 |
+
else:
|
| 875 |
+
x, present, aux_loss = layer(x, position_ids, attention_mask, pkv, use_cache)
|
| 876 |
+
|
| 877 |
+
if use_cache:
|
| 878 |
+
present_key_values.append(present)
|
| 879 |
+
if aux_loss is not None:
|
| 880 |
+
total_aux_loss = total_aux_loss + aux_loss
|
| 881 |
+
|
| 882 |
+
# Reduce HC streams to single hidden state
|
| 883 |
+
if self.cfg.use_hyper_connections:
|
| 884 |
+
x = x.mean(dim=1) # [B, S, H]
|
| 885 |
+
|
| 886 |
+
if self.hrm_refine is not None:
|
| 887 |
+
x = self.hrm_refine(x)
|
| 888 |
+
|
| 889 |
+
x = self.norm(x)
|
| 890 |
+
|
| 891 |
+
# ModularMind-on-V2: emit this specialist's output latent (for RecursiveLink).
|
| 892 |
+
out_latent = self.latent_out(x) if self.latent_out is not None else None
|
| 893 |
+
return x, present_key_values, total_aux_loss, out_latent
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
class SpikeWhaleLM(PreTrainedModel):
|
| 897 |
+
"""
|
| 898 |
+
Full causal LM combining all SpikeTransformer + NanoWhale features.
|
| 899 |
+
|
| 900 |
+
Training (forward with labels):
|
| 901 |
+
out = model(input_ids=ids, labels=ids)
|
| 902 |
+
loss = out.loss # CE + MTP loss + MoE aux loss
|
| 903 |
+
|
| 904 |
+
Generation:
|
| 905 |
+
out = model(input_ids=ids, use_cache=True)
|
| 906 |
+
past = out.past_key_values
|
| 907 |
+
out2 = model(input_ids=next_id, past_key_values=past, use_cache=True)
|
| 908 |
+
"""
|
| 909 |
+
config_class = SpikeWhaleConfig
|
| 910 |
+
base_model_prefix = "model"
|
| 911 |
+
supports_gradient_checkpointing = True
|
| 912 |
+
_no_split_modules = ["TransformerBlock"]
|
| 913 |
+
|
| 914 |
+
def __init__(self, cfg: SpikeWhaleConfig):
|
| 915 |
+
super().__init__(cfg)
|
| 916 |
+
self.model = SpikeWhaleModel(cfg)
|
| 917 |
+
self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
|
| 918 |
+
nn.init.normal_(self.lm_head.weight, std=cfg.initializer_range)
|
| 919 |
+
|
| 920 |
+
if cfg.tie_word_embeddings:
|
| 921 |
+
self.lm_head.weight = self.model.embed_tokens.weight
|
| 922 |
+
|
| 923 |
+
# Multi-Token Prediction heads (NanoWhale): predict token at position+k
|
| 924 |
+
self.mtp_heads = nn.ModuleList([
|
| 925 |
+
nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
|
| 926 |
+
for _ in range(cfg.num_nextn_predict_layers)
|
| 927 |
+
]) if cfg.num_nextn_predict_layers > 0 else None
|
| 928 |
+
|
| 929 |
+
self.post_init()
|
| 930 |
+
# HF post_init re-randomizes Linear weights, clobbering the zero-init
|
| 931 |
+
# injection gate. Restore it so the latent injection is identity-at-start.
|
| 932 |
+
self.model.reset_latent_gate()
|
| 933 |
+
|
| 934 |
+
def get_input_embeddings(self):
|
| 935 |
+
return self.model.embed_tokens
|
| 936 |
+
|
| 937 |
+
def set_input_embeddings(self, value):
|
| 938 |
+
self.model.embed_tokens = value
|
| 939 |
+
|
| 940 |
+
def get_output_embeddings(self):
|
| 941 |
+
return self.lm_head
|
| 942 |
+
|
| 943 |
+
def set_output_embeddings(self, new_embeddings):
|
| 944 |
+
self.lm_head = new_embeddings
|
| 945 |
+
|
| 946 |
+
def _set_gradient_checkpointing(self, module, value=False):
|
| 947 |
+
if isinstance(module, SpikeWhaleModel):
|
| 948 |
+
module.gradient_checkpointing = value
|
| 949 |
+
|
| 950 |
+
def forward(
|
| 951 |
+
self,
|
| 952 |
+
input_ids: Optional[torch.Tensor] = None,
|
| 953 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 954 |
+
position_ids: Optional[torch.Tensor] = None,
|
| 955 |
+
past_key_values: Optional[List[Tuple]] = None,
|
| 956 |
+
labels: Optional[torch.Tensor] = None,
|
| 957 |
+
use_cache: bool = False,
|
| 958 |
+
inject_latent: Optional[torch.Tensor] = None,
|
| 959 |
+
**kwargs,
|
| 960 |
+
) -> CausalLMOutputWithPast:
|
| 961 |
+
hidden, present_kvs, aux_loss, out_latent = self.model(
|
| 962 |
+
input_ids=input_ids,
|
| 963 |
+
attention_mask=attention_mask,
|
| 964 |
+
position_ids=position_ids,
|
| 965 |
+
past_key_values=past_key_values,
|
| 966 |
+
use_cache=use_cache,
|
| 967 |
+
inject_latent=inject_latent,
|
| 968 |
+
)
|
| 969 |
+
|
| 970 |
+
logits = self.lm_head(hidden)
|
| 971 |
+
loss = None
|
| 972 |
+
|
| 973 |
+
if labels is not None:
|
| 974 |
+
# Standard next-token CE loss (shifted by 1)
|
| 975 |
+
shift_logits = logits[..., :-1, :].contiguous()
|
| 976 |
+
shift_labels = labels[..., 1:].contiguous()
|
| 977 |
+
loss = F.cross_entropy(
|
| 978 |
+
shift_logits.view(-1, shift_logits.size(-1)),
|
| 979 |
+
shift_labels.view(-1),
|
| 980 |
+
ignore_index=-100,
|
| 981 |
+
)
|
| 982 |
+
|
| 983 |
+
# Multi-Token Prediction loss (NanoWhale)
|
| 984 |
+
# Each MTP head k predicts token at position + k+1 (beyond the standard +1)
|
| 985 |
+
if self.mtp_heads is not None:
|
| 986 |
+
mtp_total = torch.tensor(0.0, device=loss.device)
|
| 987 |
+
for k, head in enumerate(self.mtp_heads, start=1):
|
| 988 |
+
offset = k + 1 # predicts position + offset
|
| 989 |
+
if hidden.size(1) > offset:
|
| 990 |
+
mtp_logits = head(hidden[..., :-offset, :].contiguous())
|
| 991 |
+
mtp_labels = labels[..., offset:].contiguous()
|
| 992 |
+
mtp_total = mtp_total + F.cross_entropy(
|
| 993 |
+
mtp_logits.view(-1, mtp_logits.size(-1)),
|
| 994 |
+
mtp_labels.view(-1),
|
| 995 |
+
ignore_index=-100,
|
| 996 |
+
)
|
| 997 |
+
loss = loss + mtp_total / max(len(self.mtp_heads), 1)
|
| 998 |
+
|
| 999 |
+
# MoE load-balancing aux loss (My Project)
|
| 1000 |
+
loss = loss + aux_loss
|
| 1001 |
+
|
| 1002 |
+
out = CausalLMOutputWithPast(
|
| 1003 |
+
loss=loss,
|
| 1004 |
+
logits=logits,
|
| 1005 |
+
past_key_values=present_kvs,
|
| 1006 |
+
)
|
| 1007 |
+
out.latent = out_latent # ModularMind-on-V2: this specialist's output latent
|
| 1008 |
+
return out
|
| 1009 |
+
|
| 1010 |
+
def count_parameters(self) -> int:
|
| 1011 |
+
return sum(p.numel() for p in self.parameters())
|
agents/modmind/moe_gradio.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
moe_gradio.py -- adapter that drives the agents/ Gradio MoE panel with the ModMind
|
| 3 |
+
SpikeWhale specialists instead of the byte-level ByteGPT experts.
|
| 4 |
+
|
| 5 |
+
It exposes the SAME public API as agents/orchestrator.py:
|
| 6 |
+
get_moe(device) -> obj with .available() .reload() .route() .shared_latent()
|
| 7 |
+
.generate() .run()
|
| 8 |
+
and .run() returns the SAME dict shape panel.py consumes:
|
| 9 |
+
{winner, weights, bits_per_byte, steps, generation, shared_latent}
|
| 10 |
+
so agents/panel.py can use it as a drop-in backend (see the MM_MOE_BACKEND switch there).
|
| 11 |
+
|
| 12 |
+
What this adapter handles (the real differences vs the byte-level experts):
|
| 13 |
+
* Each specialist has its OWN SpikeTokenizer (different vocab), so per-TOKEN NLL is
|
| 14 |
+
not comparable across experts. Routing is therefore by BITS-PER-BYTE -- total NLL
|
| 15 |
+
divided by the query's raw UTF-8 byte count -- which IS comparable across tokenizers.
|
| 16 |
+
* d_latent=256 (vs 64); the shared RecursiveLink is built at 256, and every ModMind
|
| 17 |
+
specialist already shares that width, so latent fusion works natively.
|
| 18 |
+
* Checkpoints are ModMind format ({"model_state": ...}) under
|
| 19 |
+
<MM_CKPT_ROOT or repo>/<domain>/checkpoints/step_*.pt, loaded exactly like
|
| 20 |
+
train_link.py does, and hot-reloaded when newer ones appear.
|
| 21 |
+
|
| 22 |
+
Display mapping (ModMind domain -> the panel's expert slot/emoji):
|
| 23 |
+
language -> language (the panel shows 📖), reasoning -> math (➗), tool_use -> tool (🛠️)
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import glob
|
| 28 |
+
import math
|
| 29 |
+
import os
|
| 30 |
+
import random
|
| 31 |
+
import string
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
|
| 34 |
+
import torch
|
| 35 |
+
import torch.nn.functional as F
|
| 36 |
+
|
| 37 |
+
from model import RecursiveLink, SpikeWhaleLM
|
| 38 |
+
from specialist_presets import specialist_config
|
| 39 |
+
from spike_tokenizer import SpikeTokenizer
|
| 40 |
+
|
| 41 |
+
HERE = Path(__file__).parent
|
| 42 |
+
# checkpoints may live on a Modal Volume; tokenizers stay with the code (same rule as train_link.py)
|
| 43 |
+
CKPT_ROOT = Path(os.environ.get("MM_CKPT_ROOT", str(HERE)))
|
| 44 |
+
D_LATENT = 256
|
| 45 |
+
MAX_CTX = 512 # cap prompt/scoring length so CPU routing + generation stay snappy
|
| 46 |
+
|
| 47 |
+
# (modmind_domain, panel_slot). reasoning trains on FineMath, so it IS the "math" expert.
|
| 48 |
+
SLOTS = [("language", "language"), ("reasoning", "math"), ("tool_use", "tool")]
|
| 49 |
+
DOMAIN2SLOT = {d: s for d, s in SLOTS}
|
| 50 |
+
# trained bridges to surface in the panel as (asker_domain, consultant_domain) "consult" demos
|
| 51 |
+
LINKS = [("language", "reasoning")]
|
| 52 |
+
KEY_CHARS = string.ascii_letters + string.digits # must match train_link.py
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _latest_ckpt(domain: str):
|
| 56 |
+
cks = sorted(glob.glob(str(CKPT_ROOT / domain / "checkpoints" / "step_*.pt")))
|
| 57 |
+
return cks[-1] if cks else None
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class SpikeWhaleMoE:
|
| 61 |
+
def __init__(self, device: str = "cpu"):
|
| 62 |
+
self.device = device
|
| 63 |
+
self.models, self.toks, self.steps, self._mtime = {}, {}, {}, {}
|
| 64 |
+
self.link = RecursiveLink(d_latent=D_LATENT).to(device).eval()
|
| 65 |
+
self.trained_link = None # the TRAINED bridge (train_link.py output), for the consult demo
|
| 66 |
+
self.bridge_asker = None # the FULL fine-tuned asker, for reproducible key-recall
|
| 67 |
+
self.link_meta = None
|
| 68 |
+
self.reload()
|
| 69 |
+
|
| 70 |
+
def reload(self):
|
| 71 |
+
"""Load/refresh any specialist whose checkpoint exists or changed on disk."""
|
| 72 |
+
for domain, slot in SLOTS:
|
| 73 |
+
ck = _latest_ckpt(domain)
|
| 74 |
+
tok_path = HERE / domain / "tokenizer.json"
|
| 75 |
+
if ck is None or not tok_path.exists():
|
| 76 |
+
continue
|
| 77 |
+
mt = os.path.getmtime(ck)
|
| 78 |
+
if self._mtime.get(slot) == mt:
|
| 79 |
+
continue
|
| 80 |
+
cfg = specialist_config(domain)
|
| 81 |
+
m = SpikeWhaleLM(cfg).to(self.device).eval()
|
| 82 |
+
st = torch.load(ck, map_location=self.device, weights_only=False)
|
| 83 |
+
m.load_state_dict(st["model_state"])
|
| 84 |
+
self.models[slot] = m
|
| 85 |
+
self.toks[slot] = SpikeTokenizer(vocab_file=str(tok_path))
|
| 86 |
+
self.steps[slot] = int(st.get("step", 0))
|
| 87 |
+
self._mtime[slot] = mt
|
| 88 |
+
self._load_links()
|
| 89 |
+
return list(self.models)
|
| 90 |
+
|
| 91 |
+
def available(self):
|
| 92 |
+
return list(self.models)
|
| 93 |
+
|
| 94 |
+
def _load_links(self):
|
| 95 |
+
"""Load a TRAINED RecursiveLink bridge (train_link.py output) from
|
| 96 |
+
links/<asker>__from__<consultant>.pt and overlay its bridge-trained injection path
|
| 97 |
+
onto the asker. The injection only fires when we pass inject_latent (the consult demo),
|
| 98 |
+
so normal routing/generation is unaffected."""
|
| 99 |
+
self.trained_link = None
|
| 100 |
+
self.bridge_asker = None
|
| 101 |
+
self.link_meta = None
|
| 102 |
+
for asker_domain, consultant_domain in LINKS:
|
| 103 |
+
a, c = DOMAIN2SLOT.get(asker_domain), DOMAIN2SLOT.get(consultant_domain)
|
| 104 |
+
if a not in self.models or c not in self.models:
|
| 105 |
+
continue
|
| 106 |
+
lp = CKPT_ROOT / "links" / f"{asker_domain}__from__{consultant_domain}.pt"
|
| 107 |
+
if not lp.exists():
|
| 108 |
+
continue
|
| 109 |
+
st = torch.load(lp, map_location=self.device, weights_only=False)
|
| 110 |
+
link = RecursiveLink(d_latent=D_LATENT).to(self.device).eval()
|
| 111 |
+
link.load_state_dict(st["link"])
|
| 112 |
+
try:
|
| 113 |
+
self.models[a].model.latent_inject.load_state_dict(st["asker_latent_inject"])
|
| 114 |
+
except Exception:
|
| 115 |
+
pass
|
| 116 |
+
# FULL fine-tuned asker, if this bridge was saved with the updated train_link.py.
|
| 117 |
+
# Without it we can show latent *influence* (consult) but not reproduce key-recall.
|
| 118 |
+
if "asker_state" in st:
|
| 119 |
+
ba = SpikeWhaleLM(specialist_config(asker_domain)).to(self.device).eval()
|
| 120 |
+
ba.load_state_dict(st["asker_state"])
|
| 121 |
+
self.bridge_asker = ba
|
| 122 |
+
self.trained_link = link
|
| 123 |
+
self.link_meta = {
|
| 124 |
+
"asker": a, "consultant": c,
|
| 125 |
+
"key_len": int(st.get("key_len", 6)), "prompt": st.get("prompt", "KEY> "),
|
| 126 |
+
"with_latent": float(st.get("with_latent", float("nan"))),
|
| 127 |
+
"without_latent": float(st.get("without_latent", float("nan"))),
|
| 128 |
+
}
|
| 129 |
+
return # one bridge is enough for the panel demo
|
| 130 |
+
|
| 131 |
+
def key_recall_available(self):
|
| 132 |
+
return self.bridge_asker is not None and self.trained_link is not None
|
| 133 |
+
|
| 134 |
+
@torch.no_grad()
|
| 135 |
+
def key_recall(self, n: int = 8, ablate: bool = False):
|
| 136 |
+
"""Reproduce the train_link.py forcing task with the FULL trained asker: a random key
|
| 137 |
+
is shown ONLY to the consultant; the asker must emit it from the latent alone.
|
| 138 |
+
Returns {acc, examples:[(key, recovered, ok)]}. ablate=True zeros the latent."""
|
| 139 |
+
if not self.key_recall_available():
|
| 140 |
+
return None
|
| 141 |
+
a, c = self.link_meta["asker"], self.link_meta["consultant"]
|
| 142 |
+
a_tok, c_tok = self.toks[a], self.toks[c]
|
| 143 |
+
key_len = self.link_meta.get("key_len", 6)
|
| 144 |
+
prompt = self.link_meta.get("prompt", "KEY> ")
|
| 145 |
+
plen = len(a_tok.encode(prompt, add_special_tokens=False))
|
| 146 |
+
keys = ["".join(random.choice(KEY_CHARS) for _ in range(key_len)) for _ in range(n)]
|
| 147 |
+
examples, correct = [], 0
|
| 148 |
+
for k in keys:
|
| 149 |
+
c_ids = torch.tensor([c_tok.encode(k, add_special_tokens=False)], device=self.device)
|
| 150 |
+
a_ids = torch.tensor([a_tok.encode(prompt, add_special_tokens=False)
|
| 151 |
+
+ a_tok.encode(k, add_special_tokens=False)], device=self.device)
|
| 152 |
+
latent = self.models[c](input_ids=c_ids).latent
|
| 153 |
+
inj = torch.zeros_like(self.trained_link(latent)) if ablate else self.trained_link(latent)
|
| 154 |
+
logits = self.bridge_asker(input_ids=a_ids, inject_latent=inj).logits
|
| 155 |
+
pred = logits[:, plen - 1:plen - 1 + key_len, :].argmax(-1)[0]
|
| 156 |
+
out = a_tok.decode(pred.tolist())[:len(k)]
|
| 157 |
+
ok = (out == k)
|
| 158 |
+
correct += int(ok)
|
| 159 |
+
examples.append((k, out, ok))
|
| 160 |
+
return {"acc": correct / max(1, n), "examples": examples}
|
| 161 |
+
|
| 162 |
+
def consult_available(self):
|
| 163 |
+
return self.trained_link is not None
|
| 164 |
+
|
| 165 |
+
def consult_meta(self):
|
| 166 |
+
return dict(self.link_meta) if self.link_meta else None
|
| 167 |
+
|
| 168 |
+
@torch.no_grad()
|
| 169 |
+
def consult(self, query: str, max_new: int = 120, temperature: float = 0.8,
|
| 170 |
+
top_k: int = 40, ablate: bool = False):
|
| 171 |
+
"""The asker generates while reading the consultant's latent through the TRAINED
|
| 172 |
+
RecursiveLink. ablate=True zeros the latent (the truth-test: output should lose the
|
| 173 |
+
consultant's influence)."""
|
| 174 |
+
if not self.consult_available():
|
| 175 |
+
return ""
|
| 176 |
+
a, c = self.link_meta["asker"], self.link_meta["consultant"]
|
| 177 |
+
latent = self.models[c](input_ids=self._ids(c, query)).latent # [1, 256]
|
| 178 |
+
inj = self.trained_link(latent)
|
| 179 |
+
if ablate:
|
| 180 |
+
inj = torch.zeros_like(inj)
|
| 181 |
+
m, tok = self.models[a], self.toks[a]
|
| 182 |
+
ids = self._ids(a, query); start = ids.shape[1]
|
| 183 |
+
ctx_max = int(getattr(m.config, "max_position_embeddings", 2048))
|
| 184 |
+
for _ in range(max_new):
|
| 185 |
+
logits = m(input_ids=ids[:, -ctx_max:], inject_latent=inj).logits[:, -1, :] / max(1e-5, temperature)
|
| 186 |
+
if top_k:
|
| 187 |
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 188 |
+
logits[logits < v[:, [-1]]] = -float("inf")
|
| 189 |
+
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
| 190 |
+
ids = torch.cat([ids, nxt], dim=1)
|
| 191 |
+
if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
|
| 192 |
+
break
|
| 193 |
+
return tok.decode(ids[0, start:].tolist())
|
| 194 |
+
|
| 195 |
+
def combine_available(self):
|
| 196 |
+
return "language" in self.models and "math" in self.models
|
| 197 |
+
|
| 198 |
+
@torch.no_grad()
|
| 199 |
+
def combine(self, query: str, max_new: int = 60, blend: float = 0.5,
|
| 200 |
+
temperature: float = 0.8, top_k: int = 40, consult: bool = False):
|
| 201 |
+
"""Token-level MoE: blend BOTH specialists' next-token distributions every step.
|
| 202 |
+
Possible because they share the 16k tokenizer (same vocab). blend = weight on Math
|
| 203 |
+
(0 -> pure Language, 1 -> pure Math, 0.5 -> equal mix). consult=True also injects Math's
|
| 204 |
+
latent into Language through the trained RecursiveLink during the blend."""
|
| 205 |
+
if not self.combine_available():
|
| 206 |
+
return ""
|
| 207 |
+
lang, math_ = self.models["language"], self.models["math"]
|
| 208 |
+
tok = self.toks["language"] # shared tokenizer (identical for both)
|
| 209 |
+
ids = self._ids("language", query); start = ids.shape[1]
|
| 210 |
+
cl = int(getattr(lang.config, "max_position_embeddings", 2048))
|
| 211 |
+
cm = int(getattr(math_.config, "max_position_embeddings", 2048))
|
| 212 |
+
inj = None
|
| 213 |
+
if consult and self.trained_link is not None:
|
| 214 |
+
latent = math_(input_ids=self._ids("math", query)).latent
|
| 215 |
+
inj = self.trained_link(latent)
|
| 216 |
+
t = max(1e-5, temperature)
|
| 217 |
+
for _ in range(max_new):
|
| 218 |
+
pl = F.softmax(lang(input_ids=ids[:, -cl:], inject_latent=inj).logits[:, -1, :] / t, dim=-1)
|
| 219 |
+
pm = F.softmax(math_(input_ids=ids[:, -cm:]).logits[:, -1, :] / t, dim=-1)
|
| 220 |
+
probs = (1.0 - blend) * pl + blend * pm # mixture of the two experts' distributions
|
| 221 |
+
if top_k:
|
| 222 |
+
v, _ = torch.topk(probs, min(top_k, probs.size(-1)))
|
| 223 |
+
probs = torch.where(probs < v[:, [-1]], torch.zeros_like(probs), probs)
|
| 224 |
+
probs = probs / probs.sum(dim=-1, keepdim=True)
|
| 225 |
+
nxt = torch.multinomial(probs, 1)
|
| 226 |
+
ids = torch.cat([ids, nxt], dim=1)
|
| 227 |
+
if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
|
| 228 |
+
break
|
| 229 |
+
return tok.decode(ids[0, start:].tolist())
|
| 230 |
+
|
| 231 |
+
def _ids(self, slot: str, text: str):
|
| 232 |
+
tok = self.toks[slot]
|
| 233 |
+
ids = tok.encode(text, add_special_tokens=False)[:MAX_CTX] or [tok.pad_token_id or 0]
|
| 234 |
+
return torch.tensor([ids], dtype=torch.long, device=self.device)
|
| 235 |
+
|
| 236 |
+
@torch.no_grad()
|
| 237 |
+
def _bits_per_byte(self, slot: str, text: str) -> float:
|
| 238 |
+
"""Total next-token NLL of `text` under this specialist, per RAW UTF-8 byte.
|
| 239 |
+
Byte-normalisation makes the score comparable across the experts' different
|
| 240 |
+
tokenizers (a smaller vocab spends more tokens but each is cheaper)."""
|
| 241 |
+
nbytes = max(1, len(text.encode("utf-8")))
|
| 242 |
+
ids = self._ids(slot, text)
|
| 243 |
+
if ids.shape[1] < 2:
|
| 244 |
+
return float("inf")
|
| 245 |
+
logits = self.models[slot](input_ids=ids).logits
|
| 246 |
+
ce = F.cross_entropy(logits[:, :-1, :].reshape(-1, logits.size(-1)),
|
| 247 |
+
ids[:, 1:].reshape(-1), reduction="sum")
|
| 248 |
+
return (ce.item() / math.log(2)) / nbytes
|
| 249 |
+
|
| 250 |
+
@torch.no_grad()
|
| 251 |
+
def route(self, query: str, temp: float = 0.5):
|
| 252 |
+
"""Per-expert bits/byte + softmax routing weights + the winner (lowest bits/byte)."""
|
| 253 |
+
bits = {slot: self._bits_per_byte(slot, query) for slot in self.models}
|
| 254 |
+
names = list(bits)
|
| 255 |
+
logits = torch.tensor([-bits[n] / temp for n in names])
|
| 256 |
+
w = F.softmax(logits, dim=0).tolist()
|
| 257 |
+
weights = {n: round(wi, 3) for n, wi in zip(names, w)}
|
| 258 |
+
winner = min(bits, key=bits.get)
|
| 259 |
+
return winner, weights, {n: round(b, 3) for n, b in bits.items()}
|
| 260 |
+
|
| 261 |
+
@torch.no_grad()
|
| 262 |
+
def shared_latent(self, query: str):
|
| 263 |
+
"""Each expert's output latent -> sum -> RecursiveLink -> one shared latent (the bus)."""
|
| 264 |
+
lats = {slot: self.models[slot](input_ids=self._ids(slot, query)).latent
|
| 265 |
+
for slot in self.models}
|
| 266 |
+
z = torch.stack([lats[s] for s in lats], 0).sum(0) # [1, 256]
|
| 267 |
+
shared = self.link(z)[0]
|
| 268 |
+
return {s: lats[s][0].tolist() for s in lats}, shared.tolist()
|
| 269 |
+
|
| 270 |
+
@torch.no_grad()
|
| 271 |
+
def generate(self, query: str, expert: str | None = None, max_new: int = 160,
|
| 272 |
+
temperature: float = 0.8, top_k: int = 40):
|
| 273 |
+
if expert is None:
|
| 274 |
+
expert, _, _ = self.route(query)
|
| 275 |
+
m, tok = self.models[expert], self.toks[expert]
|
| 276 |
+
ids = self._ids(expert, query)
|
| 277 |
+
start = ids.shape[1]
|
| 278 |
+
ctx_max = int(getattr(m.config, "max_position_embeddings", 2048))
|
| 279 |
+
for _ in range(max_new):
|
| 280 |
+
logits = m(input_ids=ids[:, -ctx_max:]).logits[:, -1, :] / max(1e-5, temperature)
|
| 281 |
+
if top_k:
|
| 282 |
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 283 |
+
logits[logits < v[:, [-1]]] = -float("inf")
|
| 284 |
+
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
| 285 |
+
ids = torch.cat([ids, nxt], dim=1)
|
| 286 |
+
if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
|
| 287 |
+
break
|
| 288 |
+
return expert, tok.decode(ids[0, start:].tolist())
|
| 289 |
+
|
| 290 |
+
@torch.no_grad()
|
| 291 |
+
def run(self, query: str, max_new: int = 160, temperature: float = 0.8):
|
| 292 |
+
"""Full pass: route -> fuse latents -> generate from the winner."""
|
| 293 |
+
if not self.models:
|
| 294 |
+
return {"error": "no experts trained yet"}
|
| 295 |
+
winner, weights, bits = self.route(query)
|
| 296 |
+
_, shared = self.shared_latent(query)
|
| 297 |
+
_, gen = self.generate(query, winner, max_new, temperature)
|
| 298 |
+
return {
|
| 299 |
+
"winner": winner, "weights": weights, "bits_per_byte": bits,
|
| 300 |
+
"steps": {n: self.steps.get(n, 0) for n in self.models},
|
| 301 |
+
"generation": gen, "shared_latent": [round(x, 3) for x in shared],
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
# ------------------------------------------------------------------ #
|
| 305 |
+
# WEIGHT-MERGE section (self-contained; does not touch anything above).
|
| 306 |
+
# Both specialists are the identical dense architecture, so their weights
|
| 307 |
+
# can be linearly merged into ONE model: W = (1-alpha)*Language + alpha*Math.
|
| 308 |
+
# This is a real merged model (one forward pass), not an output ensemble.
|
| 309 |
+
# ------------------------------------------------------------------ #
|
| 310 |
+
def merge_available(self):
|
| 311 |
+
return "language" in self.models and "math" in self.models
|
| 312 |
+
|
| 313 |
+
def _merged_model(self, alpha: float):
|
| 314 |
+
"""Build (and cache the most recent) single weight-merged model at ratio alpha."""
|
| 315 |
+
alpha = round(float(alpha), 2)
|
| 316 |
+
cache = getattr(self, "_merge_cache", None)
|
| 317 |
+
if cache is not None and abs(cache[0] - alpha) < 1e-6:
|
| 318 |
+
return cache[1]
|
| 319 |
+
lsd = self.models["language"].state_dict()
|
| 320 |
+
msd = self.models["math"].state_dict()
|
| 321 |
+
merged = {}
|
| 322 |
+
for k in lsd:
|
| 323 |
+
if lsd[k].is_floating_point():
|
| 324 |
+
merged[k] = ((1.0 - alpha) * lsd[k].float() + alpha * msd[k].float()).to(lsd[k].dtype)
|
| 325 |
+
else:
|
| 326 |
+
merged[k] = lsd[k].clone() # integer/bool buffers: copy as-is
|
| 327 |
+
m = SpikeWhaleLM(specialist_config("language")).to(self.device).eval()
|
| 328 |
+
m.load_state_dict(merged)
|
| 329 |
+
# make it bridge-ready: re-load the bridge-trained injection weights straight from the
|
| 330 |
+
# link file (self-contained). The injection only fires when we pass inject_latent.
|
| 331 |
+
try:
|
| 332 |
+
lp = CKPT_ROOT / "links" / "language__from__reasoning.pt"
|
| 333 |
+
if lp.exists():
|
| 334 |
+
st = torch.load(lp, map_location=self.device, weights_only=False)
|
| 335 |
+
m.model.latent_inject.load_state_dict(st["asker_latent_inject"])
|
| 336 |
+
except Exception:
|
| 337 |
+
pass
|
| 338 |
+
self._merge_cache = (alpha, m)
|
| 339 |
+
return m
|
| 340 |
+
|
| 341 |
+
@torch.no_grad()
|
| 342 |
+
def merge_generate(self, query: str, alpha: float = 0.5, max_new: int = 60,
|
| 343 |
+
temperature: float = 0.8, top_k: int = 40, consult: bool = False):
|
| 344 |
+
"""Generate from the WEIGHT-MERGED single model. consult=True injects Math's latent
|
| 345 |
+
into the merged model through the trained RecursiveLink."""
|
| 346 |
+
if not self.merge_available():
|
| 347 |
+
return ""
|
| 348 |
+
m = self._merged_model(alpha)
|
| 349 |
+
tok = self.toks["language"] # shared tokenizer
|
| 350 |
+
ids = self._ids("language", query); start = ids.shape[1]
|
| 351 |
+
ctx = int(getattr(m.config, "max_position_embeddings", 2048))
|
| 352 |
+
inj = None
|
| 353 |
+
if consult and getattr(self, "trained_link", None) is not None:
|
| 354 |
+
latent = self.models["math"](input_ids=self._ids("math", query)).latent
|
| 355 |
+
inj = self.trained_link(latent)
|
| 356 |
+
t = max(1e-5, temperature)
|
| 357 |
+
for _ in range(max_new):
|
| 358 |
+
logits = m(input_ids=ids[:, -ctx:], inject_latent=inj).logits[:, -1, :] / t
|
| 359 |
+
if top_k:
|
| 360 |
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 361 |
+
logits[logits < v[:, [-1]]] = -float("inf")
|
| 362 |
+
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
| 363 |
+
ids = torch.cat([ids, nxt], dim=1)
|
| 364 |
+
if tok.eos_token_id is not None and int(nxt.item()) == tok.eos_token_id:
|
| 365 |
+
break
|
| 366 |
+
return tok.decode(ids[0, start:].tolist())
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
_MOE = None
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def get_moe(device: str = "cpu"):
|
| 373 |
+
global _MOE
|
| 374 |
+
if _MOE is None:
|
| 375 |
+
_MOE = SpikeWhaleMoE(device=device)
|
| 376 |
+
else:
|
| 377 |
+
_MOE.reload()
|
| 378 |
+
return _MOE
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
if __name__ == "__main__":
|
| 382 |
+
moe = get_moe()
|
| 383 |
+
print("experts:", moe.available() or "(none trained yet)")
|
| 384 |
+
if moe.available():
|
| 385 |
+
for q in ["The mitochondria is the", "Solve for x: 2x + 3 =", "Book a flight to"]:
|
| 386 |
+
r = moe.run(q, max_new=60)
|
| 387 |
+
print(f"\nQ: {q!r}\n routed-> {r['winner']} bits/byte {r['bits_per_byte']} weights {r['weights']}")
|
| 388 |
+
print(f" gen: {r['generation']!r}")
|
agents/modmind/reasoning/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
agents/modmind/registry.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
registry.py -- the SINGLE SOURCE OF TRUTH for specialists.
|
| 3 |
+
|
| 4 |
+
To add a new specialist later, add ONE entry here, then:
|
| 5 |
+
python train_tokenizer.py --domains <name>
|
| 6 |
+
python train_specialist.py --domain <name> --muon --normuon --dean-schedule --compile
|
| 7 |
+
python train_link.py --asker language --consultant <name> # so it can be consulted
|
| 8 |
+
|
| 9 |
+
Every other script (tokenizer, data prep, model sizing, link trainer) reads from
|
| 10 |
+
here, so nothing else needs editing. `position` is the chain slot (context-doubling
|
| 11 |
+
/ chain ordering in the V2 config); give each new specialist the next index.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
# vocab=16384: the shared length-max tokenizer (covers prose + math; all 256 bytes). Both
|
| 15 |
+
# active specialists use the SAME tokenizer -> identical vocab makes routing directly comparable.
|
| 16 |
+
# PURE specialization (language=100% FineWeb-Edu, reasoning=100% FineMath) -> crisp routing.
|
| 17 |
+
SPECIALISTS = {
|
| 18 |
+
"language": dict(dataset="HuggingFaceFW/fineweb-edu", config="sample-100BT",
|
| 19 |
+
field="text", vocab=16384, position=0),
|
| 20 |
+
"reasoning": dict(dataset="HuggingFaceTB/finemath", config="finemath-3plus",
|
| 21 |
+
field="text", vocab=16384, position=1),
|
| 22 |
+
# dormant -- not in ACTIVE; ignore for now (no tool calling this round)
|
| 23 |
+
"tool_use": dict(dataset="glaiveai/glaive-function-calling-v2", config=None,
|
| 24 |
+
field=("system", "chat"), vocab=16384, position=2),
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
# which specialists you are actively training right now (the "foundation" set).
|
| 28 |
+
ACTIVE = ["language", "reasoning"]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def spec(name):
|
| 32 |
+
if name not in SPECIALISTS:
|
| 33 |
+
raise KeyError(f"unknown specialist {name!r}; add it to registry.SPECIALISTS. "
|
| 34 |
+
f"known: {list(SPECIALISTS)}")
|
| 35 |
+
return SPECIALISTS[name]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def text_of(name_or_spec, ex):
|
| 39 |
+
"""Extract the training text from a streamed example (handles multi-field specs)."""
|
| 40 |
+
s = name_or_spec if isinstance(name_or_spec, dict) else spec(name_or_spec)
|
| 41 |
+
f = s["field"]
|
| 42 |
+
if isinstance(f, (tuple, list)):
|
| 43 |
+
return "\n".join(str(ex.get(k, "") or "") for k in f)
|
| 44 |
+
return ex.get(f, "") or ""
|
agents/modmind/specialist_presets.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
specialist_presets.py -- ModularMind-on-V2 specialist sizing.
|
| 3 |
+
|
| 4 |
+
DENSE ~80M specialists (Supra-50M-style, scaled up): a dense Llama-ish transformer with
|
| 5 |
+
NO MoE / Engram / Hyper-Connections / HRM, so the parameters go into language modeling
|
| 6 |
+
instead of machinery -> coherent generation (the lesson from SupraLabs/Supra-50M-Base,
|
| 7 |
+
a dense model that produces coherent multi-paragraph text on FineWeb-Edu).
|
| 8 |
+
|
| 9 |
+
Shape (shared across domains; only the vocab differs, read from registry.py):
|
| 10 |
+
hidden 640, 16 layers, 10 heads / 5 KV (GQA), dense FFN 1728, ctx 1024, d_latent 256.
|
| 11 |
+
-> ~81.5M params at vocab 16384 (the shared length-max tokenizer).
|
| 12 |
+
|
| 13 |
+
The bridge bus (d_latent=256) and latent IO are kept, so train_link.py / the Gradio
|
| 14 |
+
adapter still work after retraining.
|
| 15 |
+
"""
|
| 16 |
+
from config import SpikeWhaleConfig
|
| 17 |
+
from registry import spec
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _dense_80m(vocab_size: int) -> SpikeWhaleConfig:
|
| 21 |
+
"""A dense ~80M specialist for the given vocab."""
|
| 22 |
+
return SpikeWhaleConfig(
|
| 23 |
+
vocab_size=vocab_size,
|
| 24 |
+
hidden_size=640,
|
| 25 |
+
num_hidden_layers=16,
|
| 26 |
+
num_attention_heads=10,
|
| 27 |
+
num_key_value_heads=5, # GQA
|
| 28 |
+
head_dim=64,
|
| 29 |
+
qk_rope_head_dim=16,
|
| 30 |
+
q_lora_rank=320,
|
| 31 |
+
o_lora_rank=160,
|
| 32 |
+
tie_word_embeddings=True,
|
| 33 |
+
# DENSE: no MoE. moe_intermediate_size still sizes the DenseFFN (model.py).
|
| 34 |
+
use_moe=False,
|
| 35 |
+
moe_intermediate_size=1728,
|
| 36 |
+
# strip the heavy extras -> params go to the LM, not machinery
|
| 37 |
+
use_engram=False,
|
| 38 |
+
use_hyper_connections=False,
|
| 39 |
+
hc_mult=1,
|
| 40 |
+
use_hrm_refine=False,
|
| 41 |
+
num_nextn_predict_layers=0,
|
| 42 |
+
use_derf=False,
|
| 43 |
+
use_xsa=True,
|
| 44 |
+
# keep the ModularMind bridge bus so train_link.py / the adapter still work
|
| 45 |
+
use_latent_io=True,
|
| 46 |
+
d_latent=256,
|
| 47 |
+
# uniform 1024 context (Supra used 1024). base_context MUST be >= training --seq-len.
|
| 48 |
+
chain_position=0,
|
| 49 |
+
base_context=4096,
|
| 50 |
+
base_rope_theta=10000.0,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def specialist_config(domain: str = "language", position: int = 0) -> SpikeWhaleConfig:
|
| 55 |
+
"""A dense ~80M specialist; vocab comes from registry.py (single source of truth)."""
|
| 56 |
+
return _dense_80m(spec(domain)["vocab"])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def generic_specialist_config(vocab_size: int, position: int = 0) -> SpikeWhaleConfig:
|
| 60 |
+
"""Same dense ~80M shape for an arbitrary vocab (new domains 'just work')."""
|
| 61 |
+
return _dense_80m(vocab_size)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Foundation chain ordering (derived from the registry, so it grows automatically)
|
| 65 |
+
try:
|
| 66 |
+
from registry import SPECIALISTS as _REG
|
| 67 |
+
FOUNDATION_ORDER = {v["position"]: k for k, v in _REG.items()}
|
| 68 |
+
except Exception:
|
| 69 |
+
FOUNDATION_ORDER = {0: "language", 1: "reasoning", 2: "tool_use"}
|
agents/modmind/spike_tokenizer.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
spike_tokenizer.py -- HuggingFace-compatible wrapper for the custom
|
| 3 |
+
byte-level "length-max" (greedy longest-match) tokenizer in tokenizer.json.
|
| 4 |
+
|
| 5 |
+
The raw tokenizer.json is NOT a HuggingFace `tokenizers` file; it is a plain
|
| 6 |
+
dict {vocab, vocab_size, max_token_len, algorithm:"length-max"}. This wrapper
|
| 7 |
+
makes it loadable by AutoTokenizer.from_pretrained / save_pretrained and
|
| 8 |
+
exposes encode/decode + the bos/eos/pad/unk ids the training scripts expect.
|
| 9 |
+
|
| 10 |
+
Encoding scheme (verified): byte-level. Text is UTF-8 encoded, each byte mapped
|
| 11 |
+
to its latin-1 character, then greedily matched against the vocab using the
|
| 12 |
+
longest key that matches at each position (max key length = max_token_len).
|
| 13 |
+
"""
|
| 14 |
+
import json, os
|
| 15 |
+
from typing import List, Optional
|
| 16 |
+
from transformers import PreTrainedTokenizer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SpikeTokenizer(PreTrainedTokenizer):
|
| 20 |
+
vocab_files_names = {"vocab_file": "tokenizer.json"}
|
| 21 |
+
model_input_names = ["input_ids"]
|
| 22 |
+
|
| 23 |
+
def __init__(self, vocab_file=None, **kwargs):
|
| 24 |
+
with open(vocab_file, "r", encoding="utf-8") as f:
|
| 25 |
+
data = json.load(f)
|
| 26 |
+
self._vocab = data["vocab"] # str -> id
|
| 27 |
+
self._ids_to_tokens = {i: t for t, i in self._vocab.items()}
|
| 28 |
+
self.max_token_len = int(data.get("max_token_len", 24))
|
| 29 |
+
# length-bucketed keys for fast greedy match (longest length first)
|
| 30 |
+
self._lengths = sorted({len(k) for k in self._vocab}, reverse=True)
|
| 31 |
+
|
| 32 |
+
kwargs.setdefault("bos_token", "<bos>")
|
| 33 |
+
kwargs.setdefault("eos_token", "<eos>")
|
| 34 |
+
kwargs.setdefault("unk_token", "<unk>")
|
| 35 |
+
kwargs.setdefault("pad_token", "<pad>")
|
| 36 |
+
super().__init__(**kwargs)
|
| 37 |
+
|
| 38 |
+
@property
|
| 39 |
+
def vocab_size(self) -> int:
|
| 40 |
+
return len(self._vocab)
|
| 41 |
+
|
| 42 |
+
def get_vocab(self):
|
| 43 |
+
return dict(self._vocab)
|
| 44 |
+
|
| 45 |
+
# --- core byte-level greedy tokenization ---
|
| 46 |
+
def _tokenize(self, text: str) -> List[str]:
|
| 47 |
+
s = text.encode("utf-8").decode("latin-1") # one char per byte
|
| 48 |
+
out, i, n = [], 0, len(s)
|
| 49 |
+
while i < n:
|
| 50 |
+
matched = None
|
| 51 |
+
hi = min(self.max_token_len, n - i)
|
| 52 |
+
for L in range(hi, 0, -1):
|
| 53 |
+
sub = s[i:i + L]
|
| 54 |
+
if sub in self._vocab:
|
| 55 |
+
matched = sub
|
| 56 |
+
break
|
| 57 |
+
if matched is None: # single byte always exists in vocab
|
| 58 |
+
matched = s[i]
|
| 59 |
+
out.append(matched)
|
| 60 |
+
i += len(matched)
|
| 61 |
+
return out
|
| 62 |
+
|
| 63 |
+
def _convert_token_to_id(self, token: str) -> int:
|
| 64 |
+
return self._vocab.get(token, self._vocab["<unk>"])
|
| 65 |
+
|
| 66 |
+
def _convert_id_to_token(self, index: int) -> str:
|
| 67 |
+
return self._ids_to_tokens.get(index, "<unk>")
|
| 68 |
+
|
| 69 |
+
def convert_tokens_to_string(self, tokens: List[str]) -> str:
|
| 70 |
+
specials = {"<pad>", "<unk>", "<bos>", "<eos>"}
|
| 71 |
+
byte_str = "".join(t for t in tokens if t not in specials)
|
| 72 |
+
return byte_str.encode("latin-1").decode("utf-8", errors="replace")
|
| 73 |
+
|
| 74 |
+
def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None):
|
| 75 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 76 |
+
fn = (filename_prefix + "-" if filename_prefix else "") + "tokenizer.json"
|
| 77 |
+
path = os.path.join(save_directory, fn)
|
| 78 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 79 |
+
json.dump({"vocab": self._vocab, "vocab_size": self.vocab_size,
|
| 80 |
+
"max_token_len": self.max_token_len,
|
| 81 |
+
"algorithm": "length-max"}, f, ensure_ascii=False)
|
| 82 |
+
return (path,)
|
agents/panel.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
panel.py -- the Gradio section for the bottom of the boss app: a live demo of the
|
| 3 |
+
Modular-Mind mixture-of-experts.
|
| 4 |
+
|
| 5 |
+
For the SpikeWhale backend it leads with the *latent bridge* (the real result) and
|
| 6 |
+
organizes the three demos into tabs. Every handler is a generator that yields an
|
| 7 |
+
instant "loading/generating" message first, so the first run never looks frozen while
|
| 8 |
+
the ~80M models lazy-load. Hot-reloads checkpoints.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
import gradio as gr
|
| 16 |
+
|
| 17 |
+
EMOJI = {"language": "📖 Language", "math": "➗ Math", "tool": "🛠️ Tool-use"}
|
| 18 |
+
DEVICE = os.environ.get("MM_AGENTS_DEVICE", "cpu")
|
| 19 |
+
# Self-contained SpikeWhale bundle that ships next to this file (agents/modmind/: the 80M
|
| 20 |
+
# specialists + bridge + inference code). If it's present we default to the SpikeWhale backend
|
| 21 |
+
# so the HuggingFace Space "just works" with no env config. Env vars still override.
|
| 22 |
+
_BUNDLED_MODMIND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modmind")
|
| 23 |
+
_DEFAULT_BACKEND = "spikewhale" if os.path.isdir(_BUNDLED_MODMIND) else "bytegpt"
|
| 24 |
+
_SPIKEWHALE = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower() in ("spikewhale", "modmind")
|
| 25 |
+
_WARMED = {"done": False} # so the "loading the models" notice only shows on the first run
|
| 26 |
+
|
| 27 |
+
_FOOTER = (
|
| 28 |
+
"Two ~80M dense specialists — 📖 Language (FineWeb-Edu) and ➗ Math (FineMath) — sharing a "
|
| 29 |
+
"16k length-max tokenizer. A coordinator routes by bits-per-byte, and a trained RecursiveLink "
|
| 30 |
+
"lets them communicate in latent space (proven in the Bridge tab). Hot-reloads checkpoints."
|
| 31 |
+
if _SPIKEWHALE else
|
| 32 |
+
"Three byte-level ~10M specialists, streamed-trained on FineWeb-Edu / FineMath / "
|
| 33 |
+
"glaive-function-calling. Tiny + early-trained, so generations are rough — the routing "
|
| 34 |
+
"(which expert is most confident) is the point. It hot-reloads as training continues."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _get_moe():
|
| 39 |
+
"""Pick the MoE backend. Defaults to the bundled SpikeWhale 80M specialists
|
| 40 |
+
(agents/modmind/) when present, else the byte-level ByteGPT experts. MM_MOE_BACKEND
|
| 41 |
+
and MODMIND_DIR override."""
|
| 42 |
+
backend = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower()
|
| 43 |
+
if backend in ("spikewhale", "modmind"):
|
| 44 |
+
mm_dir = os.environ.get("MODMIND_DIR", _BUNDLED_MODMIND)
|
| 45 |
+
if mm_dir and mm_dir not in sys.path:
|
| 46 |
+
sys.path.insert(0, mm_dir) # front: ModMind's model.py wins over agents/model.py
|
| 47 |
+
from moe_gradio import get_moe
|
| 48 |
+
return get_moe
|
| 49 |
+
from orchestrator import get_moe
|
| 50 |
+
return get_moe
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _loading_notice(action="Generating"):
|
| 54 |
+
"""First-run popup + in-place message so nothing ever looks frozen."""
|
| 55 |
+
if not _WARMED["done"]:
|
| 56 |
+
gr.Info("First run — loading the models (~20–40s on CPU). After this, it's quick.")
|
| 57 |
+
return (f"### ⏳ Loading the models + {action.lower()}…\n"
|
| 58 |
+
"*First run loads the ~80M specialists into memory — this can take ~20–40s on CPU. "
|
| 59 |
+
"Every run after this is fast.*")
|
| 60 |
+
return f"### ⏳ {action}…"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _bar(frac, n=18):
|
| 64 |
+
f = max(0.0, min(1.0, frac))
|
| 65 |
+
return "█" * round(f * n) + "·" * (n - round(f * n))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def moe_run(query, max_new):
|
| 69 |
+
yield _loading_notice("Routing & generating")
|
| 70 |
+
moe = _get_moe()(DEVICE)
|
| 71 |
+
if not moe.available():
|
| 72 |
+
if _SPIKEWHALE:
|
| 73 |
+
yield ("### ⏳ No SpikeWhale experts found\nSet `MODMIND_DIR` to your ModMind folder "
|
| 74 |
+
"and make sure `<domain>/checkpoints/step_*.pt` exist (the panel hot-reloads them).")
|
| 75 |
+
else:
|
| 76 |
+
yield ("### ⏳ No experts trained yet\nRun `python agents/train.py --expert language` "
|
| 77 |
+
"(and `math`, `tool`).")
|
| 78 |
+
return
|
| 79 |
+
q = (query or "").strip() or "The"
|
| 80 |
+
r = moe.run(q, max_new=int(max_new))
|
| 81 |
+
_WARMED["done"] = True
|
| 82 |
+
w = r["weights"]; bits = r["bits_per_byte"]; steps = r["steps"]
|
| 83 |
+
rows = "\n".join(
|
| 84 |
+
f"| {EMOJI.get(n, n)} | {steps.get(n,0):,} | {bits[n]:.2f} | `{_bar(w[n])}` {w[n]*100:4.1f}% |"
|
| 85 |
+
+ (" ⬅ **routed**" if n == r["winner"] else "")
|
| 86 |
+
for n in w
|
| 87 |
+
)
|
| 88 |
+
lat = r["shared_latent"][:16]
|
| 89 |
+
spark = "".join("▁▂▃▄▅▆▇█"[min(7, int((abs(v)) / (max(1e-3, max(abs(x) for x in lat))) * 7))] for v in lat)
|
| 90 |
+
yield f"""### Routed to {EMOJI.get(r['winner'], r['winner'])}
|
| 91 |
+
The expert with the **lowest bits/byte** (most fluent on your text) wins the route.
|
| 92 |
+
|
| 93 |
+
| expert | train steps | bits/byte | routing weight |
|
| 94 |
+
|---|---|---|---|
|
| 95 |
+
{rows}
|
| 96 |
+
|
| 97 |
+
**{EMOJI.get(r['winner'], r['winner'])} continues your prompt:**
|
| 98 |
+
> {q}**{r['generation']}**
|
| 99 |
+
|
| 100 |
+
**Fused latent** (both experts' output latents combined — a glimpse of the shared bus): `{spark}`
|
| 101 |
+
|
| 102 |
+
<sub>{_FOOTER}</sub>"""
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def moe_key_recall(n):
|
| 106 |
+
"""THE PROOF: a random key shown only to the consultant; the asker reproduces it from the
|
| 107 |
+
latent alone (with) vs ablated (without)."""
|
| 108 |
+
yield _loading_notice("Running the proof")
|
| 109 |
+
moe = _get_moe()(DEVICE)
|
| 110 |
+
if not getattr(moe, "key_recall_available", lambda: False)():
|
| 111 |
+
yield ("### 🔑 Bridge unavailable\nNeeds the **SpikeWhale** backend and a trained "
|
| 112 |
+
"`links/<asker>__from__<consultant>.pt` saved with the full asker.")
|
| 113 |
+
return
|
| 114 |
+
meta = moe.consult_meta()
|
| 115 |
+
a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
|
| 116 |
+
wr = moe.key_recall(n=int(n), ablate=False)
|
| 117 |
+
ar = moe.key_recall(n=int(n), ablate=True)
|
| 118 |
+
_WARMED["done"] = True
|
| 119 |
+
rows = "\n".join(f"| `{k}` | `{rec}` | {'✅' if ok else '❌'} |" for k, rec, ok in wr["examples"])
|
| 120 |
+
return_ok = wr['acc'] * 100
|
| 121 |
+
return_no = ar['acc'] * 100
|
| 122 |
+
yield f"""### 🔑 {a} read {c}'s mind — {return_ok:.0f}% with the latent, {return_no:.0f}% without
|
| 123 |
+
|
| 124 |
+
A random key is shown **only to {c}**. {a} never sees it — yet by reading {c}'s latent through the
|
| 125 |
+
trained RecursiveLink, it reproduces the key:
|
| 126 |
+
|
| 127 |
+
| secret key (only {c} saw it) | {a} recovered it | |
|
| 128 |
+
|---|---|---|
|
| 129 |
+
{rows}
|
| 130 |
+
|
| 131 |
+
## ✅ With the latent: {return_ok:.0f}% → ❌ Cut the latent: {return_no:.0f}%
|
| 132 |
+
|
| 133 |
+
<sub>That collapse to chance when the latent is removed is the whole point: the information is genuinely
|
| 134 |
+
crossing the latent bridge between two models that were trained **separately, on different data**.
|
| 135 |
+
This is the result — routing and generation are the supporting act.</sub>"""
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def moe_consult(query, max_new):
|
| 139 |
+
"""Latent-influence demo: inject the consultant's latent into the asker (WITH vs WITHOUT).
|
| 140 |
+
The bridge was trained on key-recall, so the latent *steers* the output rather than answering."""
|
| 141 |
+
yield _loading_notice("Injecting latent")
|
| 142 |
+
moe = _get_moe()(DEVICE)
|
| 143 |
+
if not getattr(moe, "consult_available", lambda: False)():
|
| 144 |
+
yield ("### 🔗 Latent bridge unavailable\nNeeds the **SpikeWhale** backend and a trained bridge.")
|
| 145 |
+
return
|
| 146 |
+
q = (query or "").strip() or "natural selection"
|
| 147 |
+
meta = moe.consult_meta()
|
| 148 |
+
a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
|
| 149 |
+
with_gen = moe.consult(q, max_new=int(max_new), ablate=False)
|
| 150 |
+
abl_gen = moe.consult(q, max_new=int(max_new), ablate=True)
|
| 151 |
+
_WARMED["done"] = True
|
| 152 |
+
wl, nl = meta["with_latent"], meta["without_latent"]
|
| 153 |
+
yield f"""### 🔗 Latent influence — {c}'s latent injected into {a}
|
| 154 |
+
|
| 155 |
+
**WITH {c}'s latent (through the trained RecursiveLink):**
|
| 156 |
+
> {q}**{with_gen}**
|
| 157 |
+
|
| 158 |
+
**WITHOUT it (latent ablated to zero):**
|
| 159 |
+
> {q}**{abl_gen}**
|
| 160 |
+
|
| 161 |
+
<sub>This shows the latent's raw *effect* on generation, not a Q&A answer. The bridge was trained on
|
| 162 |
+
key-recall (ablation with={wl:.3f} vs without={nl:.4f}), so the injected latent pushes the asker toward
|
| 163 |
+
that learned behavior — which is why WITH and WITHOUT differ so sharply. The clean proof the latent
|
| 164 |
+
carries real information is the **Bridge** tab.</sub>"""
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def moe_combine(query, max_new, blend, consult):
|
| 168 |
+
"""Two blends compared at the same mix ratio: a real WEIGHT-MERGE (one merged model) vs an
|
| 169 |
+
OUTPUT-BLEND (two models run separately, distributions averaged)."""
|
| 170 |
+
yield _loading_notice("Building merge + blending")
|
| 171 |
+
moe = _get_moe()(DEVICE)
|
| 172 |
+
if not getattr(moe, "merge_available", lambda: False)():
|
| 173 |
+
yield "### 🧬 Needs both specialists loaded."
|
| 174 |
+
return
|
| 175 |
+
q = (query or "").strip() or "The water cycle works by"
|
| 176 |
+
a = float(blend)
|
| 177 |
+
merged_gen = moe.merge_generate(q, alpha=a, max_new=int(max_new), consult=bool(consult))
|
| 178 |
+
blend_gen = moe.combine(q, max_new=int(max_new), blend=a, consult=bool(consult))
|
| 179 |
+
_WARMED["done"] = True
|
| 180 |
+
mix = f"{int(round((1-a)*100))}% 📖 Language / {int(round(a*100))}% ➗ Math"
|
| 181 |
+
extra = " · +Reasoning's latent (consult)" if consult else ""
|
| 182 |
+
yield f"""### 🧬 MoE Modular Minds — two ways to blend · {mix}{extra}
|
| 183 |
+
|
| 184 |
+
**① Weight merge** — a *single* model whose weights are `(1-α)·Language + α·Math` — one network, one forward pass:
|
| 185 |
+
> {q}**{merged_gen}**
|
| 186 |
+
|
| 187 |
+
**② Output blend** — both models run separately, their next-token distributions averaged each step:
|
| 188 |
+
> {q}**{blend_gen}**
|
| 189 |
+
|
| 190 |
+
<sub>Same mix ratio, two different mechanisms. **Weight merge** fuses the actual parameters into one model
|
| 191 |
+
(only possible because they're the identical dense architecture); **output blend** is an inference-time
|
| 192 |
+
ensemble of two separate models (only possible because they share the 16k tokenizer). Tick *consult* to also
|
| 193 |
+
route Reasoning's latent into each through the trained bridge. Exploratory — generations are rough at this scale.</sub>"""
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
HERO = """# 🧩 Modular Mind — two specialists that talk in latent space
|
| 197 |
+
**Two ~80M models trained completely separately** — 📖 **Language** on FineWeb-Edu, ➗ **Math** on
|
| 198 |
+
FineMath — that never saw each other's data. A coordinator **routes** your query to the right one,
|
| 199 |
+
and a trained **RecursiveLink** lets them **communicate through latent space**: Language can read
|
| 200 |
+
information straight out of Math's "mind." The **🔑 Bridge** tab proves it.
|
| 201 |
+
|
| 202 |
+
> ℹ️ *These specialists were trained only to demonstrate a **verifiable result** — clean routing and a
|
| 203 |
+
> provable latent-bridge ablation — **not** for production-quality output. The generated text is
|
| 204 |
+
> intentionally rough at this scale; the mechanism is the point.*"""
|
| 205 |
+
|
| 206 |
+
BRIDGE_INTRO = """### The proof: two independent models, one latent channel
|
| 207 |
+
A random secret key is shown **only to ➗ Math**. 📖 Language never sees it — but by reading Math's
|
| 208 |
+
latent through the trained RecursiveLink, it **reproduces the key**. Zero out the latent and it
|
| 209 |
+
collapses to chance. That gap *is* the result: real information crossing between two models that
|
| 210 |
+
were trained on different data and never met. **Hit the button.**"""
|
| 211 |
+
|
| 212 |
+
INTRO_BYTE = """## 🧩 Experiment — Modular Mind as a Mixture of Experts
|
| 213 |
+
Three tiny ~10M byte-level specialists (language, math, tool-use), each streamed-trained on its own
|
| 214 |
+
dataset. A coordinator **routes** your query to whichever expert is most fluent (perplexity-based MoE)
|
| 215 |
+
and fuses their latents through a **RecursiveLink**. Try a math problem vs. a sentence."""
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _routing_block():
|
| 219 |
+
with gr.Row():
|
| 220 |
+
q = gr.Textbox(label="Your prompt", value="Solve for x: 2x + 3 = 11",
|
| 221 |
+
scale=4, placeholder="a sentence or a math problem…")
|
| 222 |
+
n = gr.Slider(40, 300, value=80, step=20, label="generate tokens", scale=1)
|
| 223 |
+
btn = gr.Button("🧭 Route & generate", variant="primary")
|
| 224 |
+
out = gr.Markdown()
|
| 225 |
+
btn.click(moe_run, [q, n], out)
|
| 226 |
+
gr.Examples(examples=[["The theory of evolution explains", 80],
|
| 227 |
+
["Compute the derivative of x^2 + 3x", 80],
|
| 228 |
+
["The history of the Roman Empire began", 80]],
|
| 229 |
+
inputs=[q, n])
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def build_moe_panel():
|
| 233 |
+
"""Create the MoE demo components inside the current gr.Blocks context."""
|
| 234 |
+
if not _SPIKEWHALE:
|
| 235 |
+
with gr.Accordion("🧩 Experiment: Modular Mind = Mixture of Experts (3 specialists)", open=False):
|
| 236 |
+
gr.Markdown(INTRO_BYTE)
|
| 237 |
+
_routing_block()
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
with gr.Accordion("🧩 Modular Mind — independent specialists communicating in latent space", open=True):
|
| 241 |
+
gr.Markdown(HERO)
|
| 242 |
+
with gr.Tabs():
|
| 243 |
+
# The headline result, FIRST.
|
| 244 |
+
with gr.Tab("🔑 The latent bridge — the proof"):
|
| 245 |
+
gr.Markdown(BRIDGE_INTRO)
|
| 246 |
+
with gr.Row():
|
| 247 |
+
kn = gr.Slider(4, 16, value=8, step=1, label="keys to test", scale=3)
|
| 248 |
+
kbtn = gr.Button("🔑 Run the proof", variant="primary", scale=1)
|
| 249 |
+
kout = gr.Markdown()
|
| 250 |
+
kbtn.click(moe_key_recall, [kn], kout)
|
| 251 |
+
|
| 252 |
+
# Routing — the supporting act.
|
| 253 |
+
with gr.Tab("🧭 Routing & generation"):
|
| 254 |
+
gr.Markdown("Type a math problem vs. a sentence and watch the **route flip** — each "
|
| 255 |
+
"expert is most fluent (lowest bits/byte) on its own domain.")
|
| 256 |
+
_routing_block()
|
| 257 |
+
|
| 258 |
+
# Latent influence — honest "with vs without".
|
| 259 |
+
with gr.Tab("🔗 Latent influence"):
|
| 260 |
+
gr.Markdown(
|
| 261 |
+
"Inject ➗ Math's latent into 📖 Language through the **trained RecursiveLink** and "
|
| 262 |
+
"see generation **with vs without** it. The bridge was trained on key-recall, so the "
|
| 263 |
+
"latent *steers* the output (it doesn't answer the question) — the point is how "
|
| 264 |
+
"strongly it changes generation. The clean proof is the **Bridge** tab.")
|
| 265 |
+
with gr.Row():
|
| 266 |
+
cq = gr.Textbox(label="Text to encode (Math's latent → injected into Language)",
|
| 267 |
+
value="natural selection", scale=4)
|
| 268 |
+
cn = gr.Slider(40, 200, value=80, step=20, label="generate tokens", scale=1)
|
| 269 |
+
cbtn = gr.Button("🔗 Show latent influence (with vs without)", variant="secondary")
|
| 270 |
+
cout = gr.Markdown()
|
| 271 |
+
cbtn.click(moe_consult, [cq, cn], cout)
|
| 272 |
+
|
| 273 |
+
# MoE Modular Minds — TWO ways to blend the specialists, compared side by side.
|
| 274 |
+
with gr.Tab("🧬 MoE Modular Minds"):
|
| 275 |
+
gr.Markdown(
|
| 276 |
+
"**Two ways to blend the two specialists**, shown side by side at the same mix ratio:\n"
|
| 277 |
+
"- **① Weight merge** — fuse the *parameters* into one model `(1-α)·Language + α·Math` "
|
| 278 |
+
"(works because they're the identical dense architecture).\n"
|
| 279 |
+
"- **② Output blend** — run both models separately and average their next-token "
|
| 280 |
+
"distributions (works because they share the 16k tokenizer).\n\n"
|
| 281 |
+
"Slide the mix, and tick *consult* to also route Reasoning's latent into each through the "
|
| 282 |
+
"trained bridge.")
|
| 283 |
+
with gr.Row():
|
| 284 |
+
mq = gr.Textbox(label="Prompt", value="The water cycle works by", scale=4)
|
| 285 |
+
mn = gr.Slider(40, 160, value=70, step=10, label="generate tokens", scale=1)
|
| 286 |
+
with gr.Row():
|
| 287 |
+
mblend = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
|
| 288 |
+
label="mix α: 0 = 📖 Language ⟷ 1 = ➗ Math", scale=3)
|
| 289 |
+
mconsult = gr.Checkbox(value=False, label="consult (inject Reasoning's latent)", scale=1)
|
| 290 |
+
mbtn = gr.Button("🧬 Blend both ways (weight-merge vs output-blend)", variant="primary")
|
| 291 |
+
mout = gr.Markdown()
|
| 292 |
+
mbtn.click(moe_combine, [mq, mn, mblend, mconsult], mout)
|
app.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py -- Modular Mind: Boss Fight (HuggingFace Space entry point).
|
| 3 |
+
|
| 4 |
+
A 2D Dark-Souls-style duel. The boss (Demon Slime) is driven by a tiny Modular
|
| 5 |
+
Mind: six specialist networks emit latents that a RecursiveLink merges into one
|
| 6 |
+
shared latent, and a coordinator reads it to pick the boss's next move. The brain
|
| 7 |
+
was trained by self-play reinforcement learning (see train.py / duel_sim.py).
|
| 8 |
+
|
| 9 |
+
The browser renders the fight at 60fps; at each decision point it calls the Python
|
| 10 |
+
brain through this app's /decide endpoint and shows the Modular Mind deciding live.
|
| 11 |
+
"""
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
from urllib.parse import quote
|
| 16 |
+
|
| 17 |
+
import gradio as gr
|
| 18 |
+
|
| 19 |
+
import modular_mind
|
| 20 |
+
import online
|
| 21 |
+
|
| 22 |
+
# the MoE-experts experiment lives in ./agents (added to the bottom of the page)
|
| 23 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "agents"))
|
| 24 |
+
try:
|
| 25 |
+
from panel import build_moe_panel
|
| 26 |
+
except Exception as _e: # agents optional -> game still runs without it
|
| 27 |
+
build_moe_panel = None
|
| 28 |
+
print(f"[app] MoE experiment panel unavailable ({_e})")
|
| 29 |
+
|
| 30 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 31 |
+
|
| 32 |
+
# the self-playing piano (a Modular Mind trained on a song) lives in ./piano
|
| 33 |
+
sys.path.insert(0, os.path.join(HERE, "piano"))
|
| 34 |
+
_PIANO = {"player": None, "tried": False}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _get_piano():
|
| 38 |
+
if not _PIANO["tried"]:
|
| 39 |
+
_PIANO["tried"] = True
|
| 40 |
+
try:
|
| 41 |
+
from poly_mind import PolyPlayer
|
| 42 |
+
_PIANO["player"] = PolyPlayer()
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"[app] piano Modular Mind unavailable ({e})")
|
| 45 |
+
return _PIANO["player"]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
_pmeta = json.load(open(os.path.join(HERE, "piano", "poly_notes.json")))
|
| 50 |
+
PIANO_LO, PIANO_HI, PIANO_FPS = _pmeta["midi_lo"], _pmeta["midi_hi"], _pmeta.get("fps", 8)
|
| 51 |
+
except Exception:
|
| 52 |
+
PIANO_LO, PIANO_HI, PIANO_FPS = 56, 86, 8
|
| 53 |
+
|
| 54 |
+
_get_piano() # warm the piano Modular Mind at app startup (so the first play is instant)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _read(path):
|
| 58 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 59 |
+
return f.read()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
CSS = _read(os.path.join(HERE, "web", "game.css"))
|
| 63 |
+
GAME_JS = _read(os.path.join(HERE, "web", "game.js"))
|
| 64 |
+
ASSETS_JS = _read(os.path.join(HERE, "assets_data.js"))
|
| 65 |
+
INDEX_HTML = _read(os.path.join(HERE, "web", "index.html"))
|
| 66 |
+
|
| 67 |
+
# music/sfx are served as static files by Gradio (allowed_paths below); the game
|
| 68 |
+
# builds audio URLs from this base.
|
| 69 |
+
AUDIO_DIR = os.path.join(HERE, "audio")
|
| 70 |
+
# URL-encode the absolute path (it may contain spaces) but keep "/" and the drive ":"
|
| 71 |
+
AUDIO_BASE_URL = (
|
| 72 |
+
"/gradio_api/file=" + quote(AUDIO_DIR.replace(os.sep, "/"), safe="/:") + "/"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# real acoustic-grand-piano note samples (served static; the piano plays the nearest
|
| 76 |
+
# sample pitch-shifted to each note, for a real piano sound instead of an oscillator).
|
| 77 |
+
PIANO_SAMPLES_DIR = os.path.join(HERE, "piano", "samples")
|
| 78 |
+
try:
|
| 79 |
+
PIANO_SAMPLE_MIDIS = sorted(int(f[:-4]) for f in os.listdir(PIANO_SAMPLES_DIR) if f.endswith(".mp3"))
|
| 80 |
+
except Exception:
|
| 81 |
+
PIANO_SAMPLE_MIDIS = []
|
| 82 |
+
PIANO_SAMPLE_BASE = "/gradio_api/file=" + quote(PIANO_SAMPLES_DIR.replace(os.sep, "/"), safe="/:") + "/"
|
| 83 |
+
|
| 84 |
+
# warm the default brain
|
| 85 |
+
modular_mind.get_mind("hard")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def decide(state_json: str) -> str:
|
| 89 |
+
"""Called by the browser at each boss decision point. In: game-state JSON
|
| 90 |
+
(includes a "difficulty" tier). Out: chosen action + telemetry, as JSON."""
|
| 91 |
+
try:
|
| 92 |
+
state = json.loads(state_json)
|
| 93 |
+
except Exception:
|
| 94 |
+
state = {}
|
| 95 |
+
return json.dumps(modular_mind.decide(state))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def learn(traj_json: str) -> str:
|
| 99 |
+
"""Called by the browser at the end of a fight with the full decision trajectory
|
| 100 |
+
+ outcome. Buffers it and periodically finetunes the HARD brain (REINFORCE)."""
|
| 101 |
+
try:
|
| 102 |
+
traj = json.loads(traj_json)
|
| 103 |
+
except Exception:
|
| 104 |
+
return json.dumps({"error": "bad json"})
|
| 105 |
+
return json.dumps(online.record_fight(traj))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def piano(payload_json: str) -> str:
|
| 109 |
+
"""Called by the browser's self-playing piano: in = {history:[tokens], n}, out =
|
| 110 |
+
{notes:[midi...], history:[...]}. The Modular Mind autoregressively generates the
|
| 111 |
+
next `n` notes from the recent history (server-side; history kept client-side)."""
|
| 112 |
+
try:
|
| 113 |
+
req = json.loads(payload_json)
|
| 114 |
+
except Exception:
|
| 115 |
+
req = {}
|
| 116 |
+
player = _get_piano()
|
| 117 |
+
hist = list(req.get("history") or [])
|
| 118 |
+
n = max(1, min(64, int(req.get("n", 32))))
|
| 119 |
+
if player is None:
|
| 120 |
+
return json.dumps({"notes": [], "history": hist, "error": "piano unavailable"})
|
| 121 |
+
if not hist:
|
| 122 |
+
hist = [list(f) for f in player.seed]
|
| 123 |
+
frames, telem = [], []
|
| 124 |
+
for _ in range(n):
|
| 125 |
+
toks, midis, tl = player.next_frame(hist)
|
| 126 |
+
hist.append(toks); frames.append([int(x) for x in midis]); telem.append(tl)
|
| 127 |
+
return json.dumps({"frames": frames, "telem": telem,
|
| 128 |
+
"history": [list(map(int, f)) for f in hist[-player.K:]]})
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# Bootstrap (runs in the browser): wire window.MM_DECIDE to this app's /decide
|
| 132 |
+
# endpoint via Gradio's REST API (no external CDN), then boot the game once the
|
| 133 |
+
# gr.HTML canvas is in the DOM.
|
| 134 |
+
BOOTSTRAP_JS = """
|
| 135 |
+
(function () {
|
| 136 |
+
// route each boss decision to the Python Modular Mind through /gradio_api/call
|
| 137 |
+
window.MM_DECIDE = async (state) => {
|
| 138 |
+
const post = await fetch('/gradio_api/call/decide', {
|
| 139 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 140 |
+
body: JSON.stringify({data: [JSON.stringify(state)]}),
|
| 141 |
+
});
|
| 142 |
+
const j = await post.json();
|
| 143 |
+
const res = await fetch('/gradio_api/call/decide/' + j.event_id);
|
| 144 |
+
const text = await res.text();
|
| 145 |
+
const line = text.split('\\n').filter(l => l.startsWith('data:')).pop();
|
| 146 |
+
const arr = JSON.parse(line.slice(5).trim());
|
| 147 |
+
return JSON.parse(arr[0]);
|
| 148 |
+
};
|
| 149 |
+
// send a finished fight's trajectory to the online learner (fire-and-forget)
|
| 150 |
+
window.MM_LEARN = async (traj) => {
|
| 151 |
+
try {
|
| 152 |
+
const post = await fetch('/gradio_api/call/learn', {
|
| 153 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 154 |
+
body: JSON.stringify({data: [JSON.stringify(traj)]}),
|
| 155 |
+
});
|
| 156 |
+
const j = await post.json();
|
| 157 |
+
await fetch('/gradio_api/call/learn/' + j.event_id);
|
| 158 |
+
} catch (e) { /* learning is best-effort */ }
|
| 159 |
+
};
|
| 160 |
+
const tryBoot = () => {
|
| 161 |
+
if (document.getElementById('mm-canvas') && window.__mmBoot) window.__mmBoot();
|
| 162 |
+
else setTimeout(tryBoot, 80);
|
| 163 |
+
};
|
| 164 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryBoot);
|
| 165 |
+
else tryBoot();
|
| 166 |
+
})();
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
# Force Gradio dark mode (matches the dark game) regardless of the visitor's browser
|
| 170 |
+
# setting, by ensuring the ?__theme=dark URL param is present before the app renders.
|
| 171 |
+
FORCE_DARK_JS = """
|
| 172 |
+
(function () {
|
| 173 |
+
try {
|
| 174 |
+
var p = new URLSearchParams(window.location.search);
|
| 175 |
+
if (p.get('__theme') !== 'dark') {
|
| 176 |
+
p.set('__theme', 'dark');
|
| 177 |
+
window.location.replace(window.location.pathname + '?' + p.toString() + window.location.hash);
|
| 178 |
+
}
|
| 179 |
+
} catch (e) {}
|
| 180 |
+
})();
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
# ---- self-playing piano (a Modular Mind trained on the song) -----------------
|
| 184 |
+
PIANO_CSS = """
|
| 185 |
+
#mm-piano-wrap{max-width:920px;margin:6px auto 2px;font-family:system-ui,sans-serif}
|
| 186 |
+
#mm-piano{display:flex;align-items:flex-end;justify-content:center;gap:2px;height:120px;
|
| 187 |
+
background:#13131a;border:1px solid #2a2a35;border-radius:8px;padding:10px 8px;overflow-x:auto}
|
| 188 |
+
.pk{box-sizing:border-box;border:1px solid #05050a;border-radius:0 0 3px 3px;flex:0 0 auto}
|
| 189 |
+
.pk.white{width:20px;height:100px;background:#e9e9ee}
|
| 190 |
+
.pk.black{width:14px;height:62px;background:#2b2b33}
|
| 191 |
+
.pk.on.white{background:#7ad1ff;box-shadow:0 0 14px #7ad1ff}
|
| 192 |
+
.pk.on.black{background:#3ba0d8;box-shadow:0 0 14px #3ba0d8}
|
| 193 |
+
#mm-piano-ctrl{display:flex;gap:14px;align-items:center;justify-content:center;margin:8px auto 4px}
|
| 194 |
+
#mm-piano-btn{cursor:pointer;background:#2a9d6a;color:#fff;border:none;border-radius:6px;
|
| 195 |
+
padding:9px 18px;font-weight:700;font-size:14px}
|
| 196 |
+
#mm-piano-btn:hover{background:#33b87c}
|
| 197 |
+
#mm-piano-note{color:#9bd;font-size:13px;min-width:96px;text-align:left}
|
| 198 |
+
#mm-piano-specs{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:8px auto 2px;max-width:800px}
|
| 199 |
+
.psp{width:110px;background:#16161e;border:1px solid #2a2a35;border-radius:6px;padding:6px 9px}
|
| 200 |
+
.psp .nm{font-weight:700;font-size:12px}
|
| 201 |
+
.psp .ow{opacity:.55;font-size:10px;color:#aaa;margin-top:1px}
|
| 202 |
+
.psp .bar{height:7px;background:#2a2a33;border-radius:4px;margin-top:6px;overflow:hidden}
|
| 203 |
+
.psp .fill{height:100%;width:4%;border-radius:4px;transition:width .12s ease}
|
| 204 |
+
#mm-piano-lbl{text-align:center;color:#888;font-size:11px;margin-top:8px}
|
| 205 |
+
#mm-piano-latent{display:flex;gap:3px;justify-content:center;align-items:flex-end;height:22px;margin:5px auto}
|
| 206 |
+
#mm-piano-latent .lc{width:8px;height:3px;border-radius:2px;background:#3a3a48;transition:height .12s ease}
|
| 207 |
+
"""
|
| 208 |
+
PIANO_GLOBALS = (f"window.MM_PIANO_LO={PIANO_LO};window.MM_PIANO_HI={PIANO_HI};"
|
| 209 |
+
f"window.MM_PIANO_FPS={PIANO_FPS};"
|
| 210 |
+
f"window.MM_PIANO_SAMPLE_BASE={json.dumps(PIANO_SAMPLE_BASE)};"
|
| 211 |
+
f"window.MM_PIANO_SAMPLE_MIDIS={json.dumps(PIANO_SAMPLE_MIDIS)};")
|
| 212 |
+
PIANO_JS = r"""
|
| 213 |
+
(function(){
|
| 214 |
+
var SPC={Bass:'#4da6ff',Tenor:'#2ecc71',Soprano:'#ff6b9d',Sustain:'#1abc9c',Rest:'#95a5a6',Onset:'#e67e22',Phrase:'#9b59b6'};
|
| 215 |
+
window.__pianoBoot = function(){
|
| 216 |
+
var wrap=document.getElementById('mm-piano');
|
| 217 |
+
if(!wrap || wrap.dataset.built) return; wrap.dataset.built='1';
|
| 218 |
+
var LO=window.MM_PIANO_LO||56, HI=window.MM_PIANO_HI||86, BLACK={1:1,3:1,6:1,8:1,10:1};
|
| 219 |
+
for(var m=LO;m<=HI;m++){var k=document.createElement('div');
|
| 220 |
+
k.className='pk '+(BLACK[m%12]?'black':'white'); k.id='pk-'+m; wrap.appendChild(k);}
|
| 221 |
+
var audio=null, playing=false, queue=[], history=[], fetching=false, timer=null, voices={};
|
| 222 |
+
var specFills={}, built=false, buffers={}, loaded=false;
|
| 223 |
+
var PLAY_MS=Math.round(1000/(window.MM_PIANO_FPS||8))+58; // a touch slower = calmer feel
|
| 224 |
+
var noteEl=document.getElementById('mm-piano-note'), btn=document.getElementById('mm-piano-btn');
|
| 225 |
+
var specBox=document.getElementById('mm-piano-specs'), latBox=document.getElementById('mm-piano-latent');
|
| 226 |
+
var NN=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
| 227 |
+
function name(m){return NN[m%12]+(Math.floor(m/12)-1);}
|
| 228 |
+
function buildSpecs(telem){
|
| 229 |
+
if(built || !specBox || !telem) return; built=true;
|
| 230 |
+
telem.spec.forEach(function(s){
|
| 231 |
+
var c=SPC[s.name]||'#888', card=document.createElement('div'); card.className='psp';
|
| 232 |
+
card.innerHTML='<div class="nm" style="color:'+c+'">'+s.name+'</div>'+
|
| 233 |
+
'<div class="ow">'+(s.owns?('owns '+s.owns):'modulator')+'</div>'+
|
| 234 |
+
'<div class="bar"><div class="fill" style="background:'+c+'"></div></div>';
|
| 235 |
+
specBox.appendChild(card); specFills[s.name]=card.querySelector('.fill');
|
| 236 |
+
});
|
| 237 |
+
if(latBox){ for(var i=0;i<8;i++){var lc=document.createElement('div'); lc.className='lc'; latBox.appendChild(lc);} }
|
| 238 |
+
}
|
| 239 |
+
function updateSpecs(telem){
|
| 240 |
+
if(!telem) return; buildSpecs(telem);
|
| 241 |
+
telem.spec.forEach(function(s){
|
| 242 |
+
var h;
|
| 243 |
+
if(s.owns!=null && s.drive!=null){ h=Math.abs(s.drive)/4.0*100; } // owners: by drive
|
| 244 |
+
else { h=(s.act-16.0)/10.0*100; } // modulators: by latent pulse
|
| 245 |
+
h=Math.max(4,Math.min(100,h));
|
| 246 |
+
if(specFills[s.name]) specFills[s.name].style.width=h+'%';
|
| 247 |
+
});
|
| 248 |
+
if(latBox && telem.shared){ var lc=latBox.children;
|
| 249 |
+
for(var i=0;i<lc.length && i<telem.shared.length;i++){
|
| 250 |
+
lc[i].style.height=Math.max(2,Math.min(20,Math.abs(telem.shared[i])*9))+'px';
|
| 251 |
+
lc[i].style.background=telem.shared[i]>=0?'#5bbcdf':'#df7a5b';
|
| 252 |
+
} }
|
| 253 |
+
}
|
| 254 |
+
async function fetchPhrase(){
|
| 255 |
+
if(fetching) return; fetching=true;
|
| 256 |
+
try{
|
| 257 |
+
var post=await fetch('/gradio_api/call/piano',{method:'POST',
|
| 258 |
+
headers:{'Content-Type':'application/json'},
|
| 259 |
+
body:JSON.stringify({data:[JSON.stringify({history:history,n:32})]})});
|
| 260 |
+
var j=await post.json();
|
| 261 |
+
var res=await fetch('/gradio_api/call/piano/'+j.event_id);
|
| 262 |
+
var text=await res.text();
|
| 263 |
+
var line=text.split('\n').filter(function(l){return l.indexOf('data:')===0;}).pop();
|
| 264 |
+
var out=JSON.parse(JSON.parse(line.slice(5).trim())[0]);
|
| 265 |
+
history=out.history||history;
|
| 266 |
+
var fr=out.frames||[]; for(var i=0;i<fr.length;i++) queue.push({f:fr[i], t:(out.telem&&out.telem[i])||null});
|
| 267 |
+
}catch(e){}
|
| 268 |
+
fetching=false;
|
| 269 |
+
}
|
| 270 |
+
function loadSamples(){
|
| 271 |
+
if(loaded || !audio) return; loaded=true; // background load; play() upgrades to samples as they arrive
|
| 272 |
+
var ms=window.MM_PIANO_SAMPLE_MIDIS||[], base=window.MM_PIANO_SAMPLE_BASE||'';
|
| 273 |
+
ms.forEach(function(sm){
|
| 274 |
+
var ctl=('AbortController' in window)?new AbortController():null;
|
| 275 |
+
var to=ctl?setTimeout(function(){ctl.abort();},8000):0;
|
| 276 |
+
fetch(base+sm+'.mp3', ctl?{signal:ctl.signal}:{}).then(function(r){return r.arrayBuffer();})
|
| 277 |
+
.then(function(ab){audio.decodeAudioData(ab,function(buf){buffers[sm]=buf;},function(){});})
|
| 278 |
+
.catch(function(){}).finally(function(){if(to)clearTimeout(to);});
|
| 279 |
+
});
|
| 280 |
+
}
|
| 281 |
+
function nearest(m){ var ks=Object.keys(buffers); if(!ks.length) return null;
|
| 282 |
+
return ks.map(Number).reduce(function(a,b){return Math.abs(b-m)<Math.abs(a-m)?b:a;}); }
|
| 283 |
+
function voice(m, vol){ // real sample if it's loaded, else an oscillator -> ALWAYS audible
|
| 284 |
+
if(!audio) return null;
|
| 285 |
+
var sm=nearest(m), t=audio.currentTime;
|
| 286 |
+
if(sm!=null && buffers[sm]){
|
| 287 |
+
var src=audio.createBufferSource(); src.buffer=buffers[sm];
|
| 288 |
+
src.playbackRate.value=Math.pow(2,(m-sm)/12);
|
| 289 |
+
var g=audio.createGain(); g.gain.value=vol;
|
| 290 |
+
src.connect(g); g.connect(audio.destination); src.start(t);
|
| 291 |
+
return {src:src, gain:g};
|
| 292 |
+
}
|
| 293 |
+
var f=440*Math.pow(2,(m-69)/12);
|
| 294 |
+
var o1=audio.createOscillator(); o1.type='triangle'; o1.frequency.value=f;
|
| 295 |
+
var o2=audio.createOscillator(); o2.type='sine'; o2.frequency.value=f*2;
|
| 296 |
+
var g2=audio.createGain(); g2.gain.value=0.18;
|
| 297 |
+
var lp=audio.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=2600;
|
| 298 |
+
var g=audio.createGain();
|
| 299 |
+
g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol,t+0.014);
|
| 300 |
+
g.gain.exponentialRampToValueAtTime(Math.max(0.0001,vol*0.3),t+1.6);
|
| 301 |
+
o1.connect(lp); o2.connect(g2); g2.connect(lp); lp.connect(g); g.connect(audio.destination);
|
| 302 |
+
o1.start(t); o2.start(t);
|
| 303 |
+
return {oscs:[o1,o2], gain:g};
|
| 304 |
+
}
|
| 305 |
+
function releaseNode(nd){
|
| 306 |
+
if(!nd || !audio) return; var t=audio.currentTime;
|
| 307 |
+
try{ nd.gain.gain.cancelScheduledValues(t);
|
| 308 |
+
nd.gain.gain.setValueAtTime(Math.max(nd.gain.gain.value,0.0001),t);
|
| 309 |
+
nd.gain.gain.linearRampToValueAtTime(0.0001,t+0.10);
|
| 310 |
+
if(nd.src) nd.src.stop(t+0.13); if(nd.oscs) nd.oscs.forEach(function(o){o.stop(t+0.13);});
|
| 311 |
+
}catch(e){}
|
| 312 |
+
}
|
| 313 |
+
function releaseAll(){
|
| 314 |
+
for(var mk in voices){ releaseNode(voices[mk]); var pe=document.getElementById('pk-'+mk); if(pe)pe.classList.remove('on'); }
|
| 315 |
+
voices={};
|
| 316 |
+
}
|
| 317 |
+
function playFrame(midis){ // polyphony: strike new notes, hold sustained ones, release dropped ones
|
| 318 |
+
var nw={}; (midis||[]).forEach(function(m){ if(m>0) nw[m]=1; });
|
| 319 |
+
for(var mk in voices){ if(!nw[mk]){ releaseNode(voices[mk]);
|
| 320 |
+
var pe=document.getElementById('pk-'+mk); if(pe)pe.classList.remove('on'); delete voices[mk]; } }
|
| 321 |
+
var on=Object.keys(nw), vol=on.length>2?0.5:0.65;
|
| 322 |
+
on.forEach(function(ms){ var m=+ms; if(!voices[m]){ var v=voice(m,vol); if(v) voices[m]=v;
|
| 323 |
+
var el=document.getElementById('pk-'+m); if(el)el.classList.add('on'); } });
|
| 324 |
+
if(noteEl){ noteEl.textContent= on.length ? ('♪ '+on.map(function(ms){return name(+ms);}).join(' ')) : '♪ (rest)'; }
|
| 325 |
+
}
|
| 326 |
+
function tick(){ if(!playing) return; if(queue.length<10 && !fetching) fetchPhrase();
|
| 327 |
+
if(queue.length>0){ var it=queue.shift(); playFrame(it.f); updateSpecs(it.t); } }
|
| 328 |
+
function start(){
|
| 329 |
+
if(!audio) audio=new (window.AudioContext||window.webkitAudioContext)();
|
| 330 |
+
if(audio.state==='suspended'){ try{audio.resume();}catch(e){} }
|
| 331 |
+
loadSamples(); // real piano loads in background; oscillator plays until then
|
| 332 |
+
playing=true; btn.textContent='⏸ Pause';
|
| 333 |
+
if(queue.length===0) fetchPhrase();
|
| 334 |
+
if(!timer) timer=setInterval(tick, PLAY_MS);
|
| 335 |
+
}
|
| 336 |
+
function stop(){ playing=false; btn.textContent='▶ Let the Modular Mind play'; releaseAll(); }
|
| 337 |
+
btn.onclick=function(){ playing?stop():start(); };
|
| 338 |
+
};
|
| 339 |
+
var t=function(){ if(document.getElementById('mm-piano')) window.__pianoBoot(); else setTimeout(t,120); };
|
| 340 |
+
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',t); else t();
|
| 341 |
+
})();
|
| 342 |
+
"""
|
| 343 |
+
PIANO_HTML = """
|
| 344 |
+
<div id="mm-piano-wrap">
|
| 345 |
+
<div id="mm-piano"></div>
|
| 346 |
+
<div id="mm-piano-ctrl">
|
| 347 |
+
<button id="mm-piano-btn">▶ Let the Modular Mind play</button>
|
| 348 |
+
<span id="mm-piano-note">♪</span>
|
| 349 |
+
</div>
|
| 350 |
+
<div id="mm-piano-lbl">specialists firing for each chord — Bass / Tenor / Soprano own a register; Sustain / Onset / Phrase are modulators that only write to the shared latent</div>
|
| 351 |
+
<div id="mm-piano-specs"></div>
|
| 352 |
+
<div id="mm-piano-latent" title="RecursiveLink shared latent"></div>
|
| 353 |
+
</div>
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
# Injected verbatim into the page <head>: dark-mode forcer, stylesheet, embedded sprite
|
| 357 |
+
# atlases, the game engine, the piano engine, and the bootstrap. (Inline <script> in <head>
|
| 358 |
+
# runs reliably; gr.HTML's innerHTML scripts do not.)
|
| 359 |
+
HEAD = (
|
| 360 |
+
f"<script>{FORCE_DARK_JS}</script>\n"
|
| 361 |
+
f"<style>{CSS}</style>\n"
|
| 362 |
+
f"<style>{PIANO_CSS}</style>\n"
|
| 363 |
+
f"<script>{ASSETS_JS}</script>\n"
|
| 364 |
+
f"<script>window.MM_AUDIO_BASE = {json.dumps(AUDIO_BASE_URL)};</script>\n"
|
| 365 |
+
f"<script>{PIANO_GLOBALS}</script>\n"
|
| 366 |
+
f"<script>{GAME_JS}</script>\n"
|
| 367 |
+
f"<script>{PIANO_JS}</script>\n"
|
| 368 |
+
f"<script>{BOOTSTRAP_JS}</script>\n"
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
INTRO = """
|
| 372 |
+
# 🍄 Quazim0t0's Thousand Token Wood Entry
|
| 373 |
+
A mini **Dark-Souls-style** duel where the boss is controlled by a **Modular Mind** — six tiny
|
| 374 |
+
specialist networks that communicate through a **shared latent** (RecursiveLink) and a coordinator
|
| 375 |
+
that picks each move. The brain was **trained by self-play reinforcement learning**, not scripted.
|
| 376 |
+
Watch the right-hand panel: every boss decision shows which specialists fired and how the modulators
|
| 377 |
+
steer the fight through the latent. **Click *Enter the Fog* and click the game once to focus, then play.**
|
| 378 |
+
"""
|
| 379 |
+
|
| 380 |
+
# ---- placeholder repo link (replace REPO_URL with your real GitHub URL) ------
|
| 381 |
+
REPO_URL = "https://github.com/your-username/ModularMind" # TODO: replace with the real repo
|
| 382 |
+
REPO_MD = f"""
|
| 383 |
+
### 📦 Get the full Modular Mind project
|
| 384 |
+
This Space runs a **tiny, specialist-scale** version of the architecture. The full,
|
| 385 |
+
production-scale **Modular Mind** — transformer specialists (MLA / MoE / Hyper-Connections),
|
| 386 |
+
the `RecursiveLink` latent bridge, context-doubling and the residual latent highway — lives on GitHub:
|
| 387 |
+
|
| 388 |
+
### 👉 [**Download / view the Modular Mind repo on GitHub**]({REPO_URL})
|
| 389 |
+
*(placeholder link — swap `{REPO_URL}` for your actual repository)*
|
| 390 |
+
"""
|
| 391 |
+
|
| 392 |
+
TECH_MD = r"""
|
| 393 |
+
## 🧠 How the model works (technical breakdown)
|
| 394 |
+
|
| 395 |
+
The boss brain is a faithful, **specialist-scale** implementation of the Modular Mind
|
| 396 |
+
architecture — small enough (~**4,500 parameters**, pure-NumPy inference) to decide in
|
| 397 |
+
well under a millisecond on a free CPU, yet structurally identical to the big idea:
|
| 398 |
+
**many small domain specialists that communicate through one shared latent.**
|
| 399 |
+
|
| 400 |
+
### The pieces
|
| 401 |
+
- **7 specialists** (tiny 2-layer MLPs). Five *own an action* and two *modulators* own none:
|
| 402 |
+
| Specialist | Owns | Role |
|
| 403 |
+
|---|---|---|
|
| 404 |
+
| **Aggressor** | `CLEAVE` | attack when in range |
|
| 405 |
+
| **Stalker** | `APPROACH` | close the distance |
|
| 406 |
+
| **Survivor** | `RETREAT` | reset spacing when it can't swing |
|
| 407 |
+
| **Baiter** | `IDLE` | wait / bait a whiff |
|
| 408 |
+
| **Defender** | `BLOCK` | guard the player's melee when it can't punish |
|
| 409 |
+
| **Punisher** | — *(modulator)* | detects "the player is open / recovering" |
|
| 410 |
+
| **Enrage** | — *(modulator)* | detects "we're low on HP → go berserk" |
|
| 411 |
+
- **`RecursiveLink`** — a ReGLU + residual block that merges the six latents into **one shared latent** (the "bridge").
|
| 412 |
+
- **Coordinator** — a linear read-out of the shared latent that nudges every action's score.
|
| 413 |
+
|
| 414 |
+
### What every specialist is doing *at one moment* (a single decision tick)
|
| 415 |
+
A souls boss commits to one move at a time, so the brain only fires when the boss is free
|
| 416 |
+
(~2–4 times/second). In that one forward pass, **all six specialists run in parallel**:
|
| 417 |
+
|
| 418 |
+
1. **Perceive** — the live game state is compressed to a **10-D feature vector** (distance, in-range?, boss HP, player HP, cooldown ready?, is the player attacking / recovering / blocking?).
|
| 419 |
+
2. **Specialise** — each specialist computes `h = tanh(W₁·features)` and emits a **latent vector** `zᵢ` (its "opinion"); the four action-owners also emit a scalar **drive** for their move.
|
| 420 |
+
3. **Communicate** — the six latents are summed and pushed through the **`RecursiveLink`** to form the **shared latent** `s`. This is the only channel the **modulators** have: *Punisher* writes "player is open" and *Enrage* writes "HP is low" into `s` — they cast no direct vote.
|
| 421 |
+
4. **Coordinate** — the **coordinator** reads `s` and produces a **modulation** added to each action's score. So `score(action) = (owner's drive) + (coordinator modulation)`. This is where "the player is open" turns *Aggressor's* CLEAVE up, or "we're low HP" makes the boss commit harder.
|
| 422 |
+
5. **Act** — the boss takes the top-scoring legal action (CLEAVE is masked while on cooldown). A small per-difficulty *mistake rate* adds the easy/normal/hard feel.
|
| 423 |
+
|
| 424 |
+
That whole loop is the **4-bar specialist panel + shared-latent strip** you see updating in the game — a live X-ray of the model thinking.
|
| 425 |
+
|
| 426 |
+
### How it learned
|
| 427 |
+
Trained by **self-play REINFORCE** (policy gradient + value baseline) in a headless duel
|
| 428 |
+
simulator: reward = *damage dealt − damage taken*, plus shaping that rewards pressuring in
|
| 429 |
+
range and punishes stalling. Over ~700 batches the win-rate climbed against a near-optimal
|
| 430 |
+
dodging opponent and the tactics — spacing, punishing recovery frames, blocking your punish,
|
| 431 |
+
enraging at low HP — **emerged**; none of it is hand-scripted. The **difficulty tiers are the
|
| 432 |
+
same trained brain at different decision-noise levels** (Easy makes more exploitable mistakes,
|
| 433 |
+
Hard plays sharp ≈0.95 win vs the dodger).
|
| 434 |
+
|
| 435 |
+
### Why the structure matters
|
| 436 |
+
- **Modular** — you can retrain or swap one specialist without touching the others (e.g. the **Defender/BLOCK** specialist was added later and the rest were untouched).
|
| 437 |
+
- **Explainable** — at any instant you can read *which* specialist drove the decision and how the modulators bent it.
|
| 438 |
+
- **Cheap** — specialists are small and run in parallel; the latent bridge is one tiny matmul.
|
| 439 |
+
|
| 440 |
+
### It finetunes from *your* fights (online learning)
|
| 441 |
+
Because the model is tiny, a gradient step is microseconds — so the boss can learn
|
| 442 |
+
from real play **on this CPU**. Every HARD-tier fight is logged (state, action, HP per
|
| 443 |
+
decision) and sent to a `/learn` endpoint; we rebuild the per-decision rewards (damage
|
| 444 |
+
dealt − taken, + kill / − death), compute REINFORCE returns, and take **one Adam step**
|
| 445 |
+
that nudges the HARD brain toward what worked against real humans — the backprop is
|
| 446 |
+
hand-written in numpy and verified against PyTorch to ~1e-8. A frozen copy of the
|
| 447 |
+
sim-trained weights is an **anchor** (gentle pull-back) so it can't drift into nonsense,
|
| 448 |
+
and with a `HF_TOKEN` + `MM_DATASET_REPO` secret the adapted weights persist to a
|
| 449 |
+
HuggingFace Dataset across Space restarts. (Only HARD fights train, so the adaptation data
|
| 450 |
+
stays on-policy.)
|
| 451 |
+
"""
|
| 452 |
+
|
| 453 |
+
USES_MD = r"""
|
| 454 |
+
## 🌍 Three real-world applications of this architecture
|
| 455 |
+
|
| 456 |
+
The reusable idea isn't "a boss" — it's **small, independently-trainable specialists that
|
| 457 |
+
coordinate through a shared latent instead of through brittle hand-written rules or one giant
|
| 458 |
+
monolithic model.** That pattern transfers well beyond games:
|
| 459 |
+
|
| 460 |
+
**1. On-device / edge robotics & IoT control.**
|
| 461 |
+
A drone, robot arm, or wearable can't run a huge policy. Give it a handful of tiny specialists
|
| 462 |
+
— *balance*, *obstacle-avoidance*, *navigation*, *battery/thermal management* — each cheap
|
| 463 |
+
enough for a microcontroller, coordinating through one shared latent. You can **add or replace
|
| 464 |
+
a specialist** (e.g., a new sensor) without retraining the whole stack, and the latent bridge
|
| 465 |
+
fuses their context in a single cheap step — exactly what this boss does at 2–4 Hz on a CPU.
|
| 466 |
+
|
| 467 |
+
**2. Explainable, designer-tunable AI for games & simulations.**
|
| 468 |
+
Studios want NPC/boss/crowd AI that's *steerable and inspectable*, not a black box. With this
|
| 469 |
+
pattern a designer can tune or hot-swap one behavior specialist (more aggressive, more cautious)
|
| 470 |
+
and **see exactly which specialist fired** for any decision — the same live panel shown here.
|
| 471 |
+
That makes balancing, debugging, and difficulty tuning tractable in ways a single end-to-end
|
| 472 |
+
policy isn't.
|
| 473 |
+
|
| 474 |
+
**3. Modular AI agents / mixture-of-specialists that talk in latent space.**
|
| 475 |
+
The original Modular Mind motivation: instead of an "agent chain" that re-serializes everything
|
| 476 |
+
to **text** at every hop (lossy, slow), let domain specialists — *math*, *code*, *retrieval*,
|
| 477 |
+
*safety/policy* — communicate through a **latent bridge** (`RecursiveLink` + a residual highway
|
| 478 |
+
for deep chains). A small language model can consult a math or tool specialist **without
|
| 479 |
+
flattening to tokens**, each specialist is trained/upgraded independently, and the system stays
|
| 480 |
+
auditable. Useful for cost-sensitive assistants, industrial decisioning (risk + liquidity +
|
| 481 |
+
fraud specialists), or clinical triage (modular diagnostic experts) where you must know *why*.
|
| 482 |
+
"""
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
PIANO_INTRO = """
|
| 486 |
+
### 🎹 Bonus: a self-playing piano — same Modular Mind method, trained on a song
|
| 487 |
+
Under the boss fight, the *same architecture* (tiny specialists → `RecursiveLink` → a coordinator)
|
| 488 |
+
applied to **playing piano in chords**. It was trained by **multi-note next-frame prediction** on a
|
| 489 |
+
*polyphonic* transcription of a song: six specialists (Bass / Tenor / Soprano registers + Sustain /
|
| 490 |
+
Onset / Phrase modulators) emit latents, the bridge merges them, and the coordinator picks the **set
|
| 491 |
+
of notes** to play next. It plays itself with **real recorded acoustic-piano samples**. Press **play**
|
| 492 |
+
and watch the keys + specialists light up.
|
| 493 |
+
<sub>Rough by design — one song, a tiny model, crude polyphonic transcription — the *method carrying over* is the point.</sub>
|
| 494 |
+
"""
|
| 495 |
+
|
| 496 |
+
with gr.Blocks(theme=gr.themes.Base(), title="Quazim0t0's 🍄 Thousand Token Wood Entry", head=HEAD) as demo:
|
| 497 |
+
gr.Markdown(INTRO)
|
| 498 |
+
gr.HTML(INDEX_HTML)
|
| 499 |
+
|
| 500 |
+
gr.Markdown(PIANO_INTRO)
|
| 501 |
+
gr.HTML(PIANO_HTML)
|
| 502 |
+
|
| 503 |
+
gr.Markdown(REPO_MD)
|
| 504 |
+
with gr.Accordion("🧠 How the Modular Mind works (technical breakdown)", open=False):
|
| 505 |
+
gr.Markdown(TECH_MD)
|
| 506 |
+
with gr.Accordion("🌍 Three real-world applications", open=False):
|
| 507 |
+
gr.Markdown(USES_MD)
|
| 508 |
+
|
| 509 |
+
# the third application, made real: a live mixture-of-experts at the bottom
|
| 510 |
+
if build_moe_panel is not None:
|
| 511 |
+
build_moe_panel()
|
| 512 |
+
|
| 513 |
+
# hidden API plumbing: the browser calls /decide via the Gradio REST API
|
| 514 |
+
inp = gr.Textbox(visible=False)
|
| 515 |
+
out = gr.Textbox(visible=False)
|
| 516 |
+
trigger = gr.Button(visible=False)
|
| 517 |
+
trigger.click(decide, inp, out, api_name="decide")
|
| 518 |
+
|
| 519 |
+
linp = gr.Textbox(visible=False)
|
| 520 |
+
lout = gr.Textbox(visible=False)
|
| 521 |
+
ltrigger = gr.Button(visible=False)
|
| 522 |
+
ltrigger.click(learn, linp, lout, api_name="learn")
|
| 523 |
+
|
| 524 |
+
pinp = gr.Textbox(visible=False)
|
| 525 |
+
pout = gr.Textbox(visible=False)
|
| 526 |
+
ptrigger = gr.Button(visible=False)
|
| 527 |
+
ptrigger.click(piano, pinp, pout, api_name="piano")
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
if __name__ == "__main__":
|
| 531 |
+
demo.queue(default_concurrency_limit=8).launch(
|
| 532 |
+
server_name="0.0.0.0",
|
| 533 |
+
server_port=int(os.environ.get("PORT", "7860")),
|
| 534 |
+
allowed_paths=[AUDIO_DIR, PIANO_SAMPLES_DIR],
|
| 535 |
+
)
|
assets_data.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
audio/sfx/hurt3_monster.wav
ADDED
|
Binary file (57.6 kB). View file
|
|
|
audio/sfx/hurt_knight.wav
ADDED
|
Binary file (20.1 kB). View file
|
|
|
audio/sfx/hurt_monster.wav
ADDED
|
Binary file (99.4 kB). View file
|
|
|
audio/sfx/jump_knight.wav
ADDED
|
Binary file (21.8 kB). View file
|
|
|
audio/sfx/roar2_monster.wav
ADDED
|
Binary file (48.2 kB). View file
|
|
|
audio/sfx/roar3_monster.wav
ADDED
|
Binary file (47.2 kB). View file
|
|
|
audio/sfx/roar4_monster.wav
ADDED
|
Binary file (41.9 kB). View file
|
|
|
audio/sfx/roar5_monster.wav
ADDED
|
Binary file (43.5 kB). View file
|
|
|
audio/sfx/roar6_monster.wav
ADDED
|
Binary file (27.6 kB). View file
|
|
|
audio/sfx/roar_monster.wav
ADDED
|
Binary file (40 kB). View file
|
|
|
audio/sfx/walk_boss.wav
ADDED
|
Binary file (27.3 kB). View file
|
|
|
audio/sfx/walk_knight.wav
ADDED
|
Binary file (22.9 kB). View file
|
|
|
features.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
features.py -- the single source of truth for what the Modular Mind sees and the
|
| 3 |
+
action set it chooses from. Imported by the duel simulator (training env), the
|
| 4 |
+
torch model, and the numpy inference brain so all three agree exactly.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"]
|
| 11 |
+
|
| 12 |
+
FEATURES = [
|
| 13 |
+
"abs_dist", # 0 normalised |player - boss|, 0 close .. 1 far
|
| 14 |
+
"in_cleave_range", # 1 player within the boss's cleave reach
|
| 15 |
+
"in_mid_range", # 2 player in the "step in and swing" band
|
| 16 |
+
"boss_hp", # 3 boss hp fraction
|
| 17 |
+
"player_hp", # 4 player hp fraction
|
| 18 |
+
"boss_ready", # 5 1 = cleave off cooldown, 0 = recovering
|
| 19 |
+
"player_pressuring", # 6 player attacking or moving in
|
| 20 |
+
"player_open", # 7 player in attack/roll recovery = punishable
|
| 21 |
+
"player_blocking", # 8 player is holding block
|
| 22 |
+
"player_threat", # 9 player is swinging AT the boss in melee range (block cue)
|
| 23 |
+
"bias", # 10
|
| 24 |
+
]
|
| 25 |
+
NF = len(FEATURES)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def extract_features(s: dict) -> np.ndarray:
|
| 29 |
+
arena = float(s.get("arenaW", 900))
|
| 30 |
+
dist = abs(float(s["playerX"]) - float(s["bossX"]))
|
| 31 |
+
abs_dist = min(dist / (arena * 0.6), 1.0)
|
| 32 |
+
cleave_reach = float(s.get("cleaveReach", 160))
|
| 33 |
+
f = np.zeros(NF, dtype=np.float32)
|
| 34 |
+
f[0] = abs_dist
|
| 35 |
+
f[1] = 1.0 if dist <= cleave_reach else 0.0
|
| 36 |
+
f[2] = 1.0 if cleave_reach < dist <= cleave_reach * 2.2 else 0.0
|
| 37 |
+
f[3] = float(s.get("bossHP", 1.0))
|
| 38 |
+
f[4] = float(s.get("playerHP", 1.0))
|
| 39 |
+
f[5] = 1.0 if float(s.get("bossCooldown", 0.0)) <= 0.0 else 0.0
|
| 40 |
+
f[6] = 1.0 if (s.get("playerAttacking") or s.get("playerApproaching")) else 0.0
|
| 41 |
+
f[7] = 1.0 if s.get("playerRecovering") else 0.0
|
| 42 |
+
f[8] = 1.0 if s.get("playerBlocking") else 0.0
|
| 43 |
+
f[9] = 1.0 if s.get("playerThreat") else 0.0
|
| 44 |
+
f[10] = 1.0
|
| 45 |
+
return f
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def legal_mask(s: dict) -> np.ndarray:
|
| 49 |
+
"""CLEAVE is illegal while on cooldown; everything else always allowed."""
|
| 50 |
+
m = np.ones(len(ACTIONS), dtype=np.float32)
|
| 51 |
+
if float(s.get("bossCooldown", 0.0)) > 0.0:
|
| 52 |
+
m[ACTIONS.index("CLEAVE")] = 0.0
|
| 53 |
+
return m
|
mm_grad.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mm_grad.py -- pure-numpy forward + backward (REINFORCE gradient) for the Modular
|
| 3 |
+
Mind policy, so the boss can be **finetuned from real player data on a CPU** with
|
| 4 |
+
no torch at runtime.
|
| 5 |
+
|
| 6 |
+
The math is identical to mm_torch.ModularMindPolicy, hand-differentiated so a
|
| 7 |
+
gradient step is a few thousand FLOPs (microseconds). Verified against torch
|
| 8 |
+
autograd in test_grad() to <1e-6.
|
| 9 |
+
|
| 10 |
+
Pipeline:
|
| 11 |
+
player plays a fight -> browser logs (state, action, bossHP, playerHP) per boss
|
| 12 |
+
decision + who died -> /learn -> we rebuild the per-step rewards (damage dealt
|
| 13 |
+
- taken, + kill/- death), compute REINFORCE returns, and take one Adam step that
|
| 14 |
+
nudges the policy toward what worked against real humans. A frozen copy of the
|
| 15 |
+
sim-trained weights is kept as an anchor (small pull-back) so it can't drift far.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
from features import ACTIONS, NF, extract_features, legal_mask
|
| 22 |
+
from modular_mind import SPEC_DEFS, D_LATENT, H
|
| 23 |
+
|
| 24 |
+
NA = len(ACTIONS)
|
| 25 |
+
EPS = 1e-5
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _ln_fwd(x, w, b):
|
| 29 |
+
mu = x.mean()
|
| 30 |
+
var = ((x - mu) ** 2).mean()
|
| 31 |
+
std = np.sqrt(var + EPS)
|
| 32 |
+
xhat = (x - mu) / std
|
| 33 |
+
return xhat * w + b, (xhat, std, w)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _ln_bwd(gy, cache):
|
| 37 |
+
xhat, std, w = cache
|
| 38 |
+
n = xhat.shape[0]
|
| 39 |
+
gw = gy * xhat
|
| 40 |
+
gb = gy.copy()
|
| 41 |
+
gxhat = gy * w
|
| 42 |
+
gx = (gxhat - gxhat.mean() - xhat * (gxhat * xhat).mean()) / std
|
| 43 |
+
return gx, gw, gb
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _relu(x):
|
| 47 |
+
return np.maximum(x, 0.0)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class OnlineLearner:
|
| 51 |
+
"""Holds the live weights + Adam state; updates them from player trajectories."""
|
| 52 |
+
|
| 53 |
+
def __init__(self, weights, lr=5e-3, gamma=0.97, anchor_pull=0.02,
|
| 54 |
+
w_deal=6.0, w_take=5.0, time_pen=0.01, entropy_coef=0.01):
|
| 55 |
+
self.W = {k: v.astype(np.float64).copy() for k, v in weights.items()}
|
| 56 |
+
self.anchor = {k: v.copy() for k, v in self.W.items()} # sim-trained anchor
|
| 57 |
+
self.lr, self.gamma, self.anchor_pull = lr, gamma, anchor_pull
|
| 58 |
+
self.w_deal, self.w_take, self.time_pen = w_deal, w_take, time_pen
|
| 59 |
+
self.entropy_coef = entropy_coef
|
| 60 |
+
self.owns = [ACTIONS.index(o) if o else None for _, o, _ in SPEC_DEFS]
|
| 61 |
+
self.m = {k: np.zeros_like(v) for k, v in self.W.items()}
|
| 62 |
+
self.v = {k: np.zeros_like(v) for k, v in self.W.items()}
|
| 63 |
+
self.t = 0
|
| 64 |
+
|
| 65 |
+
# ---- forward with cached intermediates -------------------------------
|
| 66 |
+
def _forward(self, f):
|
| 67 |
+
W = self.W
|
| 68 |
+
hs, lats, drives = [], [], np.zeros(NA)
|
| 69 |
+
for i, owns in enumerate(self.owns):
|
| 70 |
+
pre = W[f"s{i}_fc1_w"] @ f + W[f"s{i}_fc1_b"]
|
| 71 |
+
h = np.tanh(pre)
|
| 72 |
+
hs.append(h)
|
| 73 |
+
lat = W[f"s{i}_lat_w"] @ h + W[f"s{i}_lat_b"]
|
| 74 |
+
lats.append(lat)
|
| 75 |
+
if owns is not None:
|
| 76 |
+
drives[owns] += W[f"s{i}_drv_w"][0] @ h + W[f"s{i}_drv_b"][0]
|
| 77 |
+
z = np.sum(lats, axis=0)
|
| 78 |
+
zn, ln_in_c = _ln_fwd(z, W["link_ni_w"], W["link_ni_b"])
|
| 79 |
+
pre_g = W["link_g"] @ zn
|
| 80 |
+
g_act = _relu(pre_g)
|
| 81 |
+
v_act = W["link_v"] @ zn
|
| 82 |
+
reglu = g_act * v_act
|
| 83 |
+
out = W["link_d"] @ reglu
|
| 84 |
+
shared, ln_out_c = _ln_fwd(out + z, W["link_no_w"], W["link_no_b"])
|
| 85 |
+
modulation = W["coord_w"] @ shared + W["coord_b"]
|
| 86 |
+
logits = drives + modulation
|
| 87 |
+
cache = dict(f=f, hs=hs, lats=lats, z=z, zn=zn, ln_in_c=ln_in_c, pre_g=pre_g,
|
| 88 |
+
g_act=g_act, v_act=v_act, reglu=reglu, out=out, shared=shared,
|
| 89 |
+
ln_out_c=ln_out_c)
|
| 90 |
+
return logits, cache
|
| 91 |
+
|
| 92 |
+
# ---- backward: accumulate grads of (advantage * -logpi - H) ----------
|
| 93 |
+
def _backward(self, cache, g_logits, grads):
|
| 94 |
+
W = self.W
|
| 95 |
+
# coordinator
|
| 96 |
+
grads["coord_w"] += np.outer(g_logits, cache["shared"])
|
| 97 |
+
grads["coord_b"] += g_logits
|
| 98 |
+
g_shared = W["coord_w"].T @ g_logits
|
| 99 |
+
# owned-action drives
|
| 100 |
+
g_drive = {}
|
| 101 |
+
for i, owns in enumerate(self.owns):
|
| 102 |
+
if owns is not None:
|
| 103 |
+
g_drive[i] = g_logits[owns]
|
| 104 |
+
# out + z layernorm
|
| 105 |
+
g_outz, gw, gb = _ln_bwd(g_shared, cache["ln_out_c"])
|
| 106 |
+
grads["link_no_w"] += gw
|
| 107 |
+
grads["link_no_b"] += gb
|
| 108 |
+
g_out = g_outz
|
| 109 |
+
g_z = g_outz.copy()
|
| 110 |
+
# out = Wd @ reglu
|
| 111 |
+
grads["link_d"] += np.outer(g_out, cache["reglu"])
|
| 112 |
+
g_reglu = W["link_d"].T @ g_out
|
| 113 |
+
# reglu = relu(Wg@zn) * (Wv@zn)
|
| 114 |
+
g_g_act = g_reglu * cache["v_act"]
|
| 115 |
+
g_v_act = g_reglu * cache["g_act"]
|
| 116 |
+
g_pre_g = g_g_act * (cache["pre_g"] > 0)
|
| 117 |
+
grads["link_g"] += np.outer(g_pre_g, cache["zn"])
|
| 118 |
+
grads["link_v"] += np.outer(g_v_act, cache["zn"])
|
| 119 |
+
g_zn = W["link_g"].T @ g_pre_g + W["link_v"].T @ g_v_act
|
| 120 |
+
# zn = layernorm(z)
|
| 121 |
+
g_z_ln, gw, gb = _ln_bwd(g_zn, cache["ln_in_c"])
|
| 122 |
+
grads["link_ni_w"] += gw
|
| 123 |
+
grads["link_ni_b"] += gb
|
| 124 |
+
g_z += g_z_ln
|
| 125 |
+
# z = sum(lat_i) -> each specialist
|
| 126 |
+
for i, owns in enumerate(self.owns):
|
| 127 |
+
h = cache["hs"][i]
|
| 128 |
+
g_lat = g_z
|
| 129 |
+
grads[f"s{i}_lat_w"] += np.outer(g_lat, h)
|
| 130 |
+
grads[f"s{i}_lat_b"] += g_lat
|
| 131 |
+
g_h = W[f"s{i}_lat_w"].T @ g_lat
|
| 132 |
+
if owns is not None:
|
| 133 |
+
grads[f"s{i}_drv_w"][0] += g_drive[i] * h
|
| 134 |
+
grads[f"s{i}_drv_b"][0] += g_drive[i]
|
| 135 |
+
g_h = g_h + W[f"s{i}_drv_w"][0] * g_drive[i]
|
| 136 |
+
g_pre = g_h * (1.0 - h * h)
|
| 137 |
+
grads[f"s{i}_fc1_w"] += np.outer(g_pre, cache["f"])
|
| 138 |
+
grads[f"s{i}_fc1_b"] += g_pre
|
| 139 |
+
|
| 140 |
+
def logpi_grad(self, f, action, advantage, mask):
|
| 141 |
+
"""Grad of advantage * -log pi(action|state) (+ entropy bonus), accumulated."""
|
| 142 |
+
logits, cache = self._forward(f)
|
| 143 |
+
masked = np.where(mask > 0.5, logits, -1e9)
|
| 144 |
+
p = np.exp(masked - masked.max())
|
| 145 |
+
p = p / p.sum()
|
| 146 |
+
onehot = np.zeros(NA)
|
| 147 |
+
onehot[action] = 1.0
|
| 148 |
+
# d(-adv*logpi)/dlogits = adv*(p - onehot); entropy bonus grad = ent_coef*(p*(logp+H_)...)
|
| 149 |
+
g_logits = advantage * (p - onehot)
|
| 150 |
+
# entropy regularizer (encourage exploration): d(-ent_coef*H)/dlogits
|
| 151 |
+
with np.errstate(divide="ignore"):
|
| 152 |
+
logp = np.where(p > 1e-12, np.log(p), 0.0)
|
| 153 |
+
ent_term = self.entropy_coef * p * (logp + (p * (-logp)).sum())
|
| 154 |
+
g_logits = g_logits + np.where(mask > 0.5, ent_term, 0.0)
|
| 155 |
+
grads = {k: np.zeros_like(v) for k, v in self.W.items()}
|
| 156 |
+
self._backward(cache, g_logits, grads)
|
| 157 |
+
return grads
|
| 158 |
+
|
| 159 |
+
def _trajectory_rewards(self, steps, result):
|
| 160 |
+
"""Rebuild per-decision rewards from logged HP (damage dealt - taken)."""
|
| 161 |
+
n = len(steps)
|
| 162 |
+
rews = np.zeros(n)
|
| 163 |
+
for t in range(n):
|
| 164 |
+
nb = steps[t + 1]["bossHP"] if t + 1 < n else (0.0 if result.get("bossDied") else steps[t]["bossHP"])
|
| 165 |
+
npl = steps[t + 1]["playerHP"] if t + 1 < n else (0.0 if result.get("playerDied") else steps[t]["playerHP"])
|
| 166 |
+
dealt = max(0.0, steps[t]["playerHP"] - npl)
|
| 167 |
+
taken = max(0.0, steps[t]["bossHP"] - nb)
|
| 168 |
+
rews[t] = dealt * self.w_deal - taken * self.w_take - self.time_pen
|
| 169 |
+
if result.get("playerDied"):
|
| 170 |
+
rews[-1] += 8.0
|
| 171 |
+
elif result.get("bossDied"):
|
| 172 |
+
rews[-1] -= 5.0
|
| 173 |
+
return rews
|
| 174 |
+
|
| 175 |
+
def update(self, trajectories):
|
| 176 |
+
"""trajectories: list of {steps:[{state,action,bossHP,playerHP}], result:{}}.
|
| 177 |
+
Returns dict of stats. Mutates self.W in place (one Adam step)."""
|
| 178 |
+
grads = {k: np.zeros_like(v) for k, v in self.W.items()}
|
| 179 |
+
all_returns, nsteps = [], 0
|
| 180 |
+
# first pass: gather returns for a baseline
|
| 181 |
+
per_traj = []
|
| 182 |
+
for tr in trajectories:
|
| 183 |
+
steps = tr.get("steps", [])
|
| 184 |
+
if len(steps) < 2:
|
| 185 |
+
continue
|
| 186 |
+
rews = self._trajectory_rewards(steps, tr.get("result", {}))
|
| 187 |
+
G = np.zeros(len(rews))
|
| 188 |
+
acc = 0.0
|
| 189 |
+
for t in reversed(range(len(rews))):
|
| 190 |
+
acc = rews[t] + self.gamma * acc
|
| 191 |
+
G[t] = acc
|
| 192 |
+
per_traj.append((steps, G))
|
| 193 |
+
all_returns.extend(G.tolist())
|
| 194 |
+
if not per_traj:
|
| 195 |
+
return {"updated": False, "reason": "not enough data"}
|
| 196 |
+
baseline = float(np.mean(all_returns))
|
| 197 |
+
adv_std = float(np.std(all_returns)) + 1e-6
|
| 198 |
+
# second pass: accumulate gradient
|
| 199 |
+
for steps, G in per_traj:
|
| 200 |
+
for t, st in enumerate(steps):
|
| 201 |
+
s = st["state"]
|
| 202 |
+
f = extract_features(s).astype(np.float64)
|
| 203 |
+
mask = legal_mask(s)
|
| 204 |
+
action = ACTIONS.index(st["action"]) if isinstance(st["action"], str) else int(st["action"])
|
| 205 |
+
adv = (G[t] - baseline) / adv_std
|
| 206 |
+
g = self.logpi_grad(f, action, adv, mask)
|
| 207 |
+
for k in grads:
|
| 208 |
+
grads[k] += g[k]
|
| 209 |
+
nsteps += 1
|
| 210 |
+
# average + anchor pull-back (stay near the sim-trained policy)
|
| 211 |
+
self.t += 1
|
| 212 |
+
b1, b2 = 0.9, 0.999
|
| 213 |
+
for k in self.W:
|
| 214 |
+
gk = grads[k] / max(1, nsteps) + self.anchor_pull * (self.W[k] - self.anchor[k])
|
| 215 |
+
self.m[k] = b1 * self.m[k] + (1 - b1) * gk
|
| 216 |
+
self.v[k] = b2 * self.v[k] + (1 - b2) * (gk * gk)
|
| 217 |
+
mhat = self.m[k] / (1 - b1 ** self.t)
|
| 218 |
+
vhat = self.v[k] / (1 - b2 ** self.t)
|
| 219 |
+
self.W[k] -= self.lr * mhat / (np.sqrt(vhat) + 1e-8)
|
| 220 |
+
return {"updated": True, "steps": nsteps, "trajectories": len(per_traj),
|
| 221 |
+
"avg_return": round(baseline, 3)}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def test_grad():
|
| 225 |
+
"""Verify the numpy logpi-gradient matches torch autograd."""
|
| 226 |
+
import torch
|
| 227 |
+
from mm_torch import ModularMindPolicy
|
| 228 |
+
m = ModularMindPolicy().double()
|
| 229 |
+
m.export_npz("_gradchk.npz")
|
| 230 |
+
W = {k: v for k, v in np.load("_gradchk.npz").items()}
|
| 231 |
+
learner = OnlineLearner(W, entropy_coef=0.0)
|
| 232 |
+
rng = np.random.default_rng(0)
|
| 233 |
+
maxrel = 0.0
|
| 234 |
+
for _ in range(5):
|
| 235 |
+
f = rng.normal(size=NF)
|
| 236 |
+
action = int(rng.integers(NA))
|
| 237 |
+
mask = np.ones(NA)
|
| 238 |
+
# numpy grad of -logpi(action) (advantage=1)
|
| 239 |
+
gnp = learner.logpi_grad(f, action, 1.0, mask)
|
| 240 |
+
# torch grad
|
| 241 |
+
m.zero_grad()
|
| 242 |
+
x = torch.tensor(f, dtype=torch.float64).unsqueeze(0)
|
| 243 |
+
logits, _ = m(x)
|
| 244 |
+
logp = torch.log_softmax(logits, dim=-1)[0, action]
|
| 245 |
+
(-logp).backward()
|
| 246 |
+
# compare coordinator weight grad as a representative
|
| 247 |
+
gt = m.coordinator.weight.grad.numpy()
|
| 248 |
+
rel = np.abs(gnp["coord_w"] - gt).max() / (np.abs(gt).max() + 1e-9)
|
| 249 |
+
maxrel = max(maxrel, rel)
|
| 250 |
+
import os
|
| 251 |
+
os.remove("_gradchk.npz")
|
| 252 |
+
print(f"max relative grad error (coord_w) vs torch: {maxrel:.2e}")
|
| 253 |
+
return maxrel
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
test_grad()
|
modular_mind.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
modular_mind.py -- numpy inference for the trained Modular Mind boss brain.
|
| 3 |
+
|
| 4 |
+
Loads the weights produced by train.py (mm_weights.npz) and runs the exact same
|
| 5 |
+
forward pass as mm_torch.ModularMindPolicy, in pure numpy (no torch needed at game
|
| 6 |
+
runtime -> tiny, fast, instant cold-start on a HuggingFace Space).
|
| 7 |
+
|
| 8 |
+
decide(state) returns the chosen boss action plus rich telemetry (per-specialist
|
| 9 |
+
drives, the shared latent, the coordinator's modulation) so the game can VISUALISE
|
| 10 |
+
the Modular Mind making each decision -- including the two modulator specialists
|
| 11 |
+
(Punisher, Enrage) whose only influence is the latent they write into the bridge.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
|
| 19 |
+
from features import ACTIONS, NF, extract_features, legal_mask
|
| 20 |
+
|
| 21 |
+
# must match mm_torch.SPEC_DEFS
|
| 22 |
+
SPEC_DEFS = [
|
| 23 |
+
("Aggressor", "CLEAVE", "#ff4d4d"),
|
| 24 |
+
("Stalker", "APPROACH", "#4da6ff"),
|
| 25 |
+
("Survivor", "RETREAT", "#9b59b6"),
|
| 26 |
+
("Baiter", "IDLE", "#f1c40f"),
|
| 27 |
+
("Defender", "BLOCK", "#48b0c4"),
|
| 28 |
+
("Punisher", None, "#e67e22"),
|
| 29 |
+
("Enrage", None, "#c0392b"),
|
| 30 |
+
]
|
| 31 |
+
WEIGHTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mm_weights.npz")
|
| 32 |
+
H, D_LATENT = 24, 12
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _layernorm(x, w, b, eps=1e-5):
|
| 36 |
+
mu = x.mean()
|
| 37 |
+
var = ((x - mu) ** 2).mean()
|
| 38 |
+
return (x - mu) / np.sqrt(var + eps) * w + b
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _relu(x):
|
| 42 |
+
return np.maximum(x, 0.0)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class ModularMind:
|
| 46 |
+
def __init__(self, weights_path=WEIGHTS_PATH):
|
| 47 |
+
if os.path.exists(weights_path):
|
| 48 |
+
self.W = {k: v for k, v in np.load(weights_path).items()}
|
| 49 |
+
self.trained = True
|
| 50 |
+
else: # fallback so the game still runs before training finishes
|
| 51 |
+
self.W = self._random_weights()
|
| 52 |
+
self.trained = False
|
| 53 |
+
|
| 54 |
+
def _random_weights(self):
|
| 55 |
+
rng = np.random.default_rng(0)
|
| 56 |
+
W = {}
|
| 57 |
+
for i, (_, owns, _) in enumerate(SPEC_DEFS):
|
| 58 |
+
W[f"s{i}_fc1_w"] = rng.normal(0, .3, (H, NF))
|
| 59 |
+
W[f"s{i}_fc1_b"] = np.zeros(H)
|
| 60 |
+
W[f"s{i}_lat_w"] = rng.normal(0, .3, (D_LATENT, H))
|
| 61 |
+
W[f"s{i}_lat_b"] = np.zeros(D_LATENT)
|
| 62 |
+
if owns is not None:
|
| 63 |
+
W[f"s{i}_drv_w"] = rng.normal(0, .3, (1, H))
|
| 64 |
+
W[f"s{i}_drv_b"] = np.zeros(1)
|
| 65 |
+
W["link_ni_w"] = np.ones(D_LATENT); W["link_ni_b"] = np.zeros(D_LATENT)
|
| 66 |
+
W["link_v"] = rng.normal(0, .3, (2 * D_LATENT, D_LATENT))
|
| 67 |
+
W["link_g"] = rng.normal(0, .3, (2 * D_LATENT, D_LATENT))
|
| 68 |
+
W["link_d"] = rng.normal(0, .3, (D_LATENT, 2 * D_LATENT))
|
| 69 |
+
W["link_no_w"] = np.ones(D_LATENT); W["link_no_b"] = np.zeros(D_LATENT)
|
| 70 |
+
W["coord_w"] = rng.normal(0, .3, (len(ACTIONS), D_LATENT))
|
| 71 |
+
W["coord_b"] = np.zeros(len(ACTIONS))
|
| 72 |
+
return W
|
| 73 |
+
|
| 74 |
+
def decide(self, state: dict, explore: float = 0.06):
|
| 75 |
+
W = self.W
|
| 76 |
+
f = extract_features(state).astype(np.float64)
|
| 77 |
+
|
| 78 |
+
drives = np.zeros(len(ACTIONS))
|
| 79 |
+
latents = []
|
| 80 |
+
per_spec = []
|
| 81 |
+
for i, (name, owns, color) in enumerate(SPEC_DEFS):
|
| 82 |
+
h = np.tanh(W[f"s{i}_fc1_w"] @ f + W[f"s{i}_fc1_b"])
|
| 83 |
+
lat = W[f"s{i}_lat_w"] @ h + W[f"s{i}_lat_b"]
|
| 84 |
+
latents.append(lat)
|
| 85 |
+
drv = None
|
| 86 |
+
if owns is not None:
|
| 87 |
+
drv = float((W[f"s{i}_drv_w"] @ h + W[f"s{i}_drv_b"]).item())
|
| 88 |
+
drives[ACTIONS.index(owns)] += drv
|
| 89 |
+
per_spec.append({
|
| 90 |
+
"name": name, "owns": owns, "color": color,
|
| 91 |
+
"drive": round(drv, 3) if drv is not None else None,
|
| 92 |
+
"latent_norm": round(float(np.linalg.norm(lat)), 3),
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
# RecursiveLink: shared latent bridge
|
| 96 |
+
z = np.sum(latents, axis=0)
|
| 97 |
+
zn = _layernorm(z, W["link_ni_w"], W["link_ni_b"])
|
| 98 |
+
reglu = _relu(W["link_g"] @ zn) * (W["link_v"] @ zn)
|
| 99 |
+
out = W["link_d"] @ reglu
|
| 100 |
+
shared = _layernorm(out + z, W["link_no_w"], W["link_no_b"])
|
| 101 |
+
|
| 102 |
+
# Coordinator read-out (modulator influence flows in here)
|
| 103 |
+
modulation = W["coord_w"] @ shared + W["coord_b"]
|
| 104 |
+
logits = drives + modulation
|
| 105 |
+
|
| 106 |
+
mask = legal_mask(state)
|
| 107 |
+
masked = np.where(mask > 0.5, logits, -1e9)
|
| 108 |
+
|
| 109 |
+
probs = np.exp(masked - masked.max())
|
| 110 |
+
probs = probs / probs.sum()
|
| 111 |
+
# `explore` is a true mistake-rate: with prob `explore` take a uniformly
|
| 112 |
+
# random LEGAL action (the difficulty dial), else the best action. (Sampling
|
| 113 |
+
# from `probs` barely degrades play -- the policy is too confident -- so we
|
| 114 |
+
# use uniform mistakes to make easier tiers actually easier.)
|
| 115 |
+
if explore > 0 and np.random.random() < explore:
|
| 116 |
+
legal_idx = np.where(mask > 0.5)[0]
|
| 117 |
+
choice = int(np.random.choice(legal_idx))
|
| 118 |
+
else:
|
| 119 |
+
choice = int(np.argmax(masked))
|
| 120 |
+
|
| 121 |
+
phase = 2 if state.get("bossHP", 1.0) < 0.5 else 1
|
| 122 |
+
return {
|
| 123 |
+
"action": ACTIONS[choice],
|
| 124 |
+
"phase": phase,
|
| 125 |
+
"trained": self.trained,
|
| 126 |
+
"specialists": per_spec,
|
| 127 |
+
"base_drive": {a: round(float(d), 3) for a, d in zip(ACTIONS, drives)},
|
| 128 |
+
"modulation": {a: round(float(m), 3) for a, m in zip(ACTIONS, modulation)},
|
| 129 |
+
"final_drive": {a: round(float(d), 3) for a, d in zip(ACTIONS, logits)},
|
| 130 |
+
"probs": {a: round(float(p), 3) for a, p in zip(ACTIONS, probs)},
|
| 131 |
+
"legal": {a: bool(m > 0.5) for a, m in zip(ACTIONS, mask)},
|
| 132 |
+
"shared_latent": [round(float(x), 3) for x in shared],
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ---- difficulty tiers --------------------------------------------------------
|
| 137 |
+
# Each tier = a different training checkpoint PLUS a decision-noise level. Easy is a
|
| 138 |
+
# partially-trained brain that also "thinks loosely" (more exploration -> more
|
| 139 |
+
# mistakes, more openings for the player); Hard is the fully-trained brain playing
|
| 140 |
+
# sharply. Missing weight files fall back to the hard weights.
|
| 141 |
+
_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 142 |
+
# Difficulty = decision-noise on the trained brain. `explore` is the mistake-rate
|
| 143 |
+
# (prob of a random legal action); higher = the boss makes more exploitable mistakes.
|
| 144 |
+
# (Once the boss learned to BLOCK it dominates the sim even early in training, so a
|
| 145 |
+
# "weaker checkpoint" no longer yields an easy boss -- noise is the controllable dial:
|
| 146 |
+
# vs a near-optimal dodger these give boss win-rates ~0.35 / ~0.65 / ~0.95.)
|
| 147 |
+
DIFFICULTY = {
|
| 148 |
+
"easy": {"file": "mm_weights.npz", "explore": 0.50},
|
| 149 |
+
"normal": {"file": "mm_weights.npz", "explore": 0.22},
|
| 150 |
+
"hard": {"file": "mm_weights.npz", "explore": 0.04},
|
| 151 |
+
}
|
| 152 |
+
_MINDS: dict[str, "ModularMind"] = {}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_mind(difficulty: str = "hard") -> ModularMind:
|
| 156 |
+
key = difficulty if difficulty in DIFFICULTY else "hard"
|
| 157 |
+
if key not in _MINDS:
|
| 158 |
+
path = os.path.join(_DIR, DIFFICULTY[key]["file"])
|
| 159 |
+
if not os.path.exists(path): # tier not trained yet
|
| 160 |
+
path = os.path.join(_DIR, DIFFICULTY["hard"]["file"])
|
| 161 |
+
mind = ModularMind(path)
|
| 162 |
+
# the HARD tier shares the online learner's live (player-adapted) weights,
|
| 163 |
+
# so finetuning from real fights takes effect immediately. (Lazy import to
|
| 164 |
+
# avoid an import cycle: online -> mm_grad -> modular_mind.)
|
| 165 |
+
if key == "hard":
|
| 166 |
+
try:
|
| 167 |
+
import online
|
| 168 |
+
if online.ENABLED:
|
| 169 |
+
mind.W = online.live_weights()
|
| 170 |
+
mind.trained = True
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"[modular_mind] online weights not shared ({e})")
|
| 173 |
+
_MINDS[key] = mind
|
| 174 |
+
return _MINDS[key]
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def decide(state: dict) -> dict:
|
| 178 |
+
"""Route a decision to the brain for the requested difficulty tier (each tier
|
| 179 |
+
has its own checkpoint and exploration/mistake level)."""
|
| 180 |
+
key = str(state.get("difficulty", "hard"))
|
| 181 |
+
if key not in DIFFICULTY:
|
| 182 |
+
key = "hard"
|
| 183 |
+
out = get_mind(key).decide(state, explore=DIFFICULTY[key]["explore"])
|
| 184 |
+
out["difficulty"] = key
|
| 185 |
+
return out
|
online.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
online.py -- persistent finetuning of the boss from real player fights.
|
| 3 |
+
|
| 4 |
+
Flow: the browser logs every boss decision (state, action, HP) during a fight and,
|
| 5 |
+
on fight end, POSTs the trajectory + who-died to /learn. We buffer trajectories and,
|
| 6 |
+
every MM_UPDATE_EVERY fights, run one REINFORCE step (mm_grad.OnlineLearner) that
|
| 7 |
+
nudges the HARD brain toward what actually worked against humans. The adapted
|
| 8 |
+
weights feed straight back into the live boss.
|
| 9 |
+
|
| 10 |
+
ONLY HARD-tier fights are used, so the data stays on-policy (Easy/Normal are
|
| 11 |
+
deliberately handicapped checkpoints; learning from them would be off-policy).
|
| 12 |
+
|
| 13 |
+
Persistence (optional, set as Space secrets):
|
| 14 |
+
HF_TOKEN - a write token
|
| 15 |
+
MM_DATASET_REPO - e.g. "your-name/boss-fight-online"
|
| 16 |
+
If set, adapted weights are pushed to / pulled from that dataset so learning
|
| 17 |
+
survives Space restarts. Without them it still adapts live, just in-memory.
|
| 18 |
+
|
| 19 |
+
Safety: a frozen copy of the sim-trained weights is kept as an anchor (the learner
|
| 20 |
+
pulls gently back toward it), so a weird run can't brick the boss.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
import threading
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
from mm_grad import OnlineLearner
|
| 30 |
+
|
| 31 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 32 |
+
BASE_WEIGHTS = os.path.join(HERE, "mm_weights.npz") # sim-trained HARD brain
|
| 33 |
+
LIVE_WEIGHTS = os.path.join(HERE, "mm_weights_live.npz") # player-adapted snapshot
|
| 34 |
+
|
| 35 |
+
ENABLED = os.environ.get("MM_ONLINE", "1") == "1"
|
| 36 |
+
UPDATE_EVERY = int(os.environ.get("MM_UPDATE_EVERY", "3")) # fights per update
|
| 37 |
+
ADAPT_TIER = "hard"
|
| 38 |
+
DATASET_REPO = os.environ.get("MM_DATASET_REPO")
|
| 39 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 40 |
+
WEIGHTS_IN_REPO = "mm_weights_live.npz"
|
| 41 |
+
|
| 42 |
+
_LOCK = threading.Lock()
|
| 43 |
+
_LEARNER = None
|
| 44 |
+
_BUFFER = []
|
| 45 |
+
_FIGHTS = 0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _pull_from_dataset():
|
| 49 |
+
"""Try to download the latest adapted weights from the HF dataset."""
|
| 50 |
+
if not (DATASET_REPO and HF_TOKEN):
|
| 51 |
+
return None
|
| 52 |
+
try:
|
| 53 |
+
from huggingface_hub import hf_hub_download
|
| 54 |
+
p = hf_hub_download(repo_id=DATASET_REPO, filename=WEIGHTS_IN_REPO,
|
| 55 |
+
repo_type="dataset", token=HF_TOKEN)
|
| 56 |
+
return {k: v for k, v in np.load(p).items()}
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"[online] no persisted weights pulled ({e}); starting from sim weights")
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _push_to_dataset():
|
| 63 |
+
"""Upload the current adapted weights to the HF dataset (best-effort)."""
|
| 64 |
+
if not (DATASET_REPO and HF_TOKEN):
|
| 65 |
+
return
|
| 66 |
+
try:
|
| 67 |
+
from huggingface_hub import upload_file
|
| 68 |
+
upload_file(path_or_fileobj=LIVE_WEIGHTS, path_in_repo=WEIGHTS_IN_REPO,
|
| 69 |
+
repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN)
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"[online] dataset push failed ({e})")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_learner():
|
| 75 |
+
global _LEARNER
|
| 76 |
+
if _LEARNER is None:
|
| 77 |
+
base = _pull_from_dataset()
|
| 78 |
+
if base is None and os.path.exists(LIVE_WEIGHTS):
|
| 79 |
+
base = {k: v for k, v in np.load(LIVE_WEIGHTS).items()}
|
| 80 |
+
if base is None:
|
| 81 |
+
base = {k: v for k, v in np.load(BASE_WEIGHTS).items()}
|
| 82 |
+
# anchor is ALWAYS the pristine sim-trained weights
|
| 83 |
+
_LEARNER = OnlineLearner(base)
|
| 84 |
+
_LEARNER.anchor = {k: v.astype(np.float64).copy()
|
| 85 |
+
for k, v in np.load(BASE_WEIGHTS).items()}
|
| 86 |
+
return _LEARNER
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def live_weights():
|
| 90 |
+
"""The HARD brain's current (possibly player-adapted) weights, shared live."""
|
| 91 |
+
return get_learner().W
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _save_live():
|
| 95 |
+
np.savez(LIVE_WEIGHTS, **{k: v.astype(np.float32) for k, v in get_learner().W.items()})
|
| 96 |
+
_push_to_dataset()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def record_fight(trajectory: dict) -> dict:
|
| 100 |
+
"""Called by /learn. trajectory = {difficulty, steps:[{state,action,bossHP,playerHP}],
|
| 101 |
+
result:{bossDied,playerDied}}. Buffers it and updates every UPDATE_EVERY fights."""
|
| 102 |
+
global _FIGHTS
|
| 103 |
+
if not ENABLED:
|
| 104 |
+
return {"enabled": False}
|
| 105 |
+
if trajectory.get("difficulty") != ADAPT_TIER:
|
| 106 |
+
return {"enabled": True, "skipped": "only HARD-tier fights train the brain"}
|
| 107 |
+
if len(trajectory.get("steps", [])) < 2:
|
| 108 |
+
return {"enabled": True, "skipped": "too short"}
|
| 109 |
+
with _LOCK:
|
| 110 |
+
_BUFFER.append(trajectory)
|
| 111 |
+
_FIGHTS += 1
|
| 112 |
+
if len(_BUFFER) >= UPDATE_EVERY:
|
| 113 |
+
stats = get_learner().update(list(_BUFFER))
|
| 114 |
+
_BUFFER.clear()
|
| 115 |
+
if stats.get("updated"):
|
| 116 |
+
_save_live()
|
| 117 |
+
return {"enabled": True, "fights": _FIGHTS, **stats}
|
| 118 |
+
return {"enabled": True, "fights": _FIGHTS, "buffered": len(_BUFFER),
|
| 119 |
+
"update_in": UPDATE_EVERY - len(_BUFFER)}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def status() -> dict:
|
| 123 |
+
return {"enabled": ENABLED, "fights_seen": _FIGHTS, "buffered": len(_BUFFER),
|
| 124 |
+
"persistent": bool(DATASET_REPO and HF_TOKEN), "adapt_tier": ADAPT_TIER}
|
piano/notes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"seq": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 23, 0, 23, 17, 11, 11, 11, 11, 17, 17, 17, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 13, 13, 13, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, 6, 9, 9, 12, 9, 9, 9, 13, 13, 13, 9, 13, 13, 13, 13, 13, 18, 23, 23, 23, 18, 13, 13, 4, 4, 4, 2, 2, 2, 2, 2, 2, 3, 4, 4, 4, 4, 4, 3, 2, 2, 2, 2, 3, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 20, 20, 20, 20, 20, 20, 16, 16, 16, 10, 9, 7, 6, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 8, 8, 8, 8, 8, 8, 8, 8, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 20, 20, 20, 20, 20, 14, 14, 14, 14, 14, 14, 14, 14, 20, 20, 22, 23, 23, 23, 23, 23, 21, 21, 21, 21, 20, 20, 20, 9, 2, 2, 2, 2, 20, 20, 20, 20, 21, 21, 21, 21, 21, 17, 17, 17, 17, 17, 8, 8, 8, 8, 8, 8, 5, 5, 1, 1, 1, 1, 1, 1, 3, 3, 5, 5, 1, 0, 0, 0, 0, 0, 0, 11, 15, 13, 13, 15, 13, 13, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 18, 18, 18, 18, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 11, 21, 21, 23, 23, 23, 23, 23, 14, 14, 14, 14, 14, 14, 14, 14, 18, 18, 18, 20, 20, 21, 21, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 21, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 10, 9, 9, 9, 10, 11, 20, 20, 20, 23, 23, 24, 25, 25, 25, 25, 13, 13, 13, 13, 23, 23, 23, 23, 23, 22, 21, 21, 21, 21, 21, 21, 21, 18, 13, 6, 6, 6, 6, 16, 16, 16, 16, 16, 16, 14, 14, 14, 13, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 24, 24, 24, 24, 13, 1, 1, 1, 1, 1, 1, 1, 3, 0, 5, 5, 11, 12, 13, 13, 13, 13, 15, 25, 0, 0, 0, 13, 0, 0, 15, 18, 18, 18, 18, 18, 18, 18, 18, 16, 16, 16, 16, 16, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 16, 13, 13, 13, 13, 12, 12, 8, 8, 8, 8, 9, 9, 9, 10, 20, 20, 20, 20, 21, 21, 21, 21, 18, 18, 18, 18, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 16, 16, 14, 14, 14, 13, 13, 12, 11, 11, 11, 11, 11, 18, 18, 18, 18, 18, 13, 13, 13, 13, 13, 14, 18, 18, 18, 18, 18, 20, 20, 20, 20, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 17, 16, 16, 16, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 16, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 19, 19, 18, 18, 18, 18, 18, 19, 20, 20, 20, 19, 18, 18, 18, 10, 9, 9, 9, 10, 21, 21, 21, 21, 9, 9, 9, 9, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 23, 23, 12, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 16, 18, 18, 21, 21, 21, 21, 21, 21, 19, 19, 18, 18, 18, 16, 16, 16, 16, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 11, 11, 11, 11, 13, 13, 13, 13, 21, 23, 23, 23, 23, 23, 21, 18, 12, 12, 11, 11, 11, 11, 11, 11, 11, 12, 13, 13, 18, 20, 20, 20, 21, 21, 20, 20, 20, 16, 16, 16, 16, 18, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 20, 20, 20, 20, 9, 9, 9, 9, 9, 15, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 20, 20, 20, 20, 23, 23, 23, 23, 21, 21, 21, 21, 21, 14, 14, 14, 14, 14, 14, 18, 25, 25, 25, 25, 25, 25, 25, 25, 24, 24, 24, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 18, 13, 18, 13, 18, 18, 0, 0, 0, 0, 0, 13, 13, 13, 13, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 25, 25, 23, 23, 23, 13, 21, 23, 21, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 13, 25, 25, 25, 25, 25, 23, 23, 22, 21, 21, 17, 17, 17, 17, 18, 18, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 14, 9, 9, 8, 9, 9, 9, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 13, 13, 9, 9, 9, 14, 25, 25, 25, 25, 25, 25, 25, 25, 20, 19, 14, 14, 14, 14, 14, 14, 14, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 23, 25, 25, 25, 25, 25, 23, 5, 5, 5, 5, 5, 9, 13, 0, 13, 7, 1, 1, 1, 1, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 8, 0, 13, 0, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 14, 23, 23, 23, 23, 23, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 18, 18, 17, 17, 17, 17, 17, 18, 18, 18, 18, 7, 7, 6, 6, 6, 6, 7, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 11, 11, 11, 11, 11, 10, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 13, 13, 13, 13, 13, 13, 13, 23, 23, 23, 23, 23, 13, 13, 9, 9, 9, 9, 9, 8, 8, 8, 8, 9, 18, 21, 21, 21, 21, 25, 25, 25, 25, 21, 10, 9, 9, 9, 9, 20, 20, 20, 20, 20, 20, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 11, 11, 13, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 11, 11, 11, 11, 11, 11, 11, 9, 9, 6, 6, 6, 6, 6, 9, 9, 9, 17, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 17, 13, 9, 6, 6, 0, 0, 0, 0, 0, 16, 16, 0, 14, 11, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 11, 13, 13, 13, 9, 6, 6, 6, 6, 6, 7, 9, 9, 9, 9, 11, 11, 11, 11, 11, 13, 13, 16, 20, 20, 20, 20, 18, 18, 18, 17, 17, 18, 18, 18, 18, 13, 15, 15, 13, 13, 13, 7, 10, 20, 20, 20, 20, 20, 20, 20, 20, 20, 16, 13, 13, 13, 13, 13, 13, 14, 15, 14, 13, 13, 13, 13, 13, 13, 13, 11, 9, 9, 9, 9, 16, 22, 22, 22, 21, 21, 20, 16, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 17, 17, 18, 20, 20, 20, 20, 20, 20, 21, 22, 0, 22, 0, 22, 22, 22, 22, 22, 22, 20, 18, 13, 13, 13, 13, 15, 18, 20, 20, 0, 20, 20, 21, 22, 22, 22, 22, 0, 0, 0, 0, 20, 0, 0, 22, 22, 22, 22, 0, 0, 0, 0, 0, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 16, 20, 20, 17, 17, 17, 17, 18, 22, 22, 22, 22, 18, 15, 15, 15, 15, 15, 15, 15, 15, 19, 19, 20, 25, 25, 25, 25, 25, 15, 15, 15, 15, 15, 21, 27, 0, 27, 27, 25, 25, 25, 25, 25, 25, 17, 8, 10, 8, 8, 10, 0, 19, 27, 0, 27, 27, 25, 25, 25, 25, 25, 25, 25, 22, 20, 20, 20, 20, 20, 20, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 13, 13, 13, 13, 13, 13, 13, 10, 10, 10, 10, 10, 15, 15, 15, 15, 15, 15, 15, 13, 11, 10, 8, 8, 8, 8, 10, 19, 20, 20, 20, 20, 20, 20, 20, 20, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 19, 19, 11, 1, 1, 1, 1, 1, 8, 8, 9, 20, 20, 22, 22, 22, 20, 20, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 8, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 23, 23, 23, 23, 17, 16, 11, 10, 10, 10, 10, 10, 13, 13, 13, 13, 13, 15, 23, 23, 23, 23, 23, 23, 15, 13, 13, 11, 11, 11, 11, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 22, 22, 22, 22, 21, 21, 21, 21, 22, 22, 23, 23, 23, 23, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 22, 22, 20, 20, 20, 18, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 15, 15, 15, 15, 15, 15, 10, 12, 15, 15, 15, 15, 15, 15, 8, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 15, 15, 15, 15, 15, 22, 22, 22, 22, 22, 22, 22, 22, 16, 16, 11, 15, 15, 15, 19, 19, 20, 20, 0, 0, 0, 0, 0, 0, 20, 20, 16, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 18, 18, 19, 19, 19, 18, 0, 0, 0, 0, 3, 0, 0, 10, 12, 13, 12, 11, 11, 15, 15, 22, 22, 22, 22, 22, 16, 11, 11, 8, 6, 6, 6, 6, 6, 6, 6, 6, 10, 11, 11, 11, 11, 11, 11, 11, 15, 15, 15, 15, 15, 15, 15, 22, 22, 22, 23, 23, 23, 23, 22, 16, 16, 16, 16, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 4, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 20, 20, 20, 20, 20, 20, 13, 13, 13, 13, 13, 14, 14, 14, 16, 16, 15, 15, 15, 15, 16, 16, 23, 23, 23, 23, 23, 16, 16, 16, 15, 11, 10, 10, 10, 10, 11, 18, 22, 23, 23, 23, 22, 20, 13, 8, 8, 8, 8, 8, 8, 8, 10, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 10, 10, 10, 10, 16, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 19, 19, 19, 19, 19, 19, 11, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 5, 9, 9, 9, 9, 9, 9, 9, 14, 20, 20, 20, 20, 6, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 11, 12, 13, 13, 13, 13, 13, 13, 9, 9, 9, 9, 9, 9, 9, 9, 10, 11, 11, 11, 11, 12, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 9, 9, 9, 9, 9, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 11, 11, 11, 11, 11, 14, 17, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 18, 25, 25, 25, 25, 25, 25, 25, 23, 23, 20, 11, 11, 11, 11, 11, 11, 11, 11, 13, 18, 22, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 14, 18, 18, 18, 18, 18, 19, 20, 19, 15, 15, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 18, 18, 18, 18, 18, 13, 13, 13, 13, 13, 13, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 13, 13, 13, 13, 13, 13, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 9, 9, 9, 1, 4, 4, 4, 4, 11, 11, 11, 11, 9, 9, 9, 9, 9, 9, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 13, 13, 13, 13, 13, 13, 17, 17, 17, 17, 17, 17, 17, 17, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 12, 12, 19, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 23, 25, 25, 25, 25, 21, 21, 21, 21, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 16, 16, 16, 16, 16, 11, 11, 11, 6, 6, 6, 6, 6, 9, 11, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 17, 13, 13, 13, 13, 13, 13, 13, 13, 1, 1, 1, 1, 1, 2, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 20, 20, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 12, 11, 11, 11, 11, 7, 4, 4, 4, 10, 11, 12, 13, 13, 13, 13, 13, 13, 13, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 11, 11, 11, 11, 11, 11, 11, 11, 14, 19, 20, 21, 21, 20, 21, 21, 20, 20, 20, 20, 20, 18, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16, 25, 25, 25, 25, 25, 25, 25, 25, 25, 22, 10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 0, 0, 23, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 15, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 6, 6, 6, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 6, 6, 6, 6, 5, 5, 5, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 10, 11, 11, 12, 13, 13, 15, 18, 20, 20, 20, 21, 21, 21, 21, 0, 6, 0, 0, 0, 0, 21, 0, 6, 6, 6, 6, 6, 25, 25, 25, 25, 21, 9, 9, 9, 6, 6, 6, 6, 6, 6, 7, 7, 6, 4, 5, 8, 14, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 13, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 25, 25, 25, 25, 25, 25, 25, 25, 12, 11, 11, 11, 11, 11, 11, 11, 11, 17, 23, 23, 24, 24, 23, 23, 14, 14, 14, 14, 14, 14, 11, 11, 11, 11, 11, 11, 0, 0, 0, 0, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 20, 22, 22, 22, 23, 23, 23, 20, 20, 20, 20, 19, 20, 20, 20, 20, 20, 14, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 1, 1, 1, 9, 1, 1, 6, 9, 6, 3, 3, 7, 9, 9, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 21, 21, 21, 19, 18, 18, 18, 18, 18, 18, 18, 6, 6, 6, 6, 6, 13, 15, 21, 21, 21, 15, 9, 5, 0, 0, 0, 0, 25, 0, 0, 6, 6, 6, 6, 7, 9, 13, 21, 21, 21, 21, 21, 21, 23, 23, 24, 25, 25, 25, 25, 25, 25, 9, 5, 6, 15, 15, 15, 15, 19, 21, 21, 18, 18, 18, 18, 21, 21, 21, 21, 21, 19, 19, 18, 18, 19, 21, 21, 21, 21, 23, 22, 21, 19, 19, 20, 20, 20, 20, 22, 22, 20, 21, 21, 19, 19, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 11, 11, 11, 11, 19, 23, 25, 26, 25, 25, 25, 25, 23, 23, 23, 24, 23, 23, 23, 23, 23, 23, 20, 20, 23, 23, 25, 25, 25, 25, 25, 25, 25, 25, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 20, 20, 20, 13, 13, 13, 13, 13, 13, 13, 13, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 23, 23, 24, 25, 25, 25, 25, 25, 25, 25, 18, 18, 18, 18, 18, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 23, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 21, 21, 18, 18, 18, 18, 16, 16, 16, 16, 16, 16, 17, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 18, 18, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 21, 22, 21, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 24, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25, 25, 18, 13, 14, 14, 13, 13, 13, 13, 12, 11, 11, 11, 11, 11, 11, 8, 6, 6, 6, 14, 23, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 6, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 15, 15, 15, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 18, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 22, 22, 21, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 25, 25, 25, 25, 25, 23, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 20, 20, 21, 21, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 16, 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 13, 10, 10, 10, 10, 11, 11, 11, 20, 20, 20, 20, 20, 20, 20, 15, 9, 9, 9, 9, 9, 9, 9, 9, 9, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 14, 13, 13, 13, 13, 15, 17, 17, 17, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 23, 23, 23, 21, 21, 21, 21, 21, 21, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 5, 5, 5, 5, 5, 5, 3, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 7, 13, 13, 13, 16, 0, 20, 19, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 11, 9, 9, 8, 2, 2, 2, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 13, 13, 15, 15, 15, 15, 15, 14, 14, 13, 13, 13, 8, 8, 8, 8, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 0, 0, 0, 0, 16, 23, 23, 23, 23, 23, 10, 10, 9, 9, 10, 10, 11, 20, 20, 20, 20, 23, 23, 26, 26, 26, 23, 13, 13, 13, 13, 13, 13, 12, 12, 12, 13, 17, 21, 24, 24, 24, 24, 21, 18, 13, 13, 13, 13, 15, 16, 16, 16, 16, 16, 16, 16, 14, 14, 13, 13, 13, 13, 5, 5, 5, 13, 25, 25, 25, 25, 25, 25, 25, 18, 8, 5, 5, 5, 5, 5, 5, 5, 5, 11, 17, 17, 17, 0, 0, 0, 0, 0, 11, 11, 0, 23, 0, 0, 0, 0, 18, 18, 18, 18, 18, 18, 18, 18, 17, 16, 16, 16, 16, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 16, 16, 13, 13, 13, 13, 12, 8, 8, 8, 8, 8, 9, 9, 10, 20, 20, 20, 20, 21, 21, 21, 21, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 17, 17, 17, 17, 15, 14, 14, 14, 14, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 13, 13, 13, 10, 9, 9, 9, 9, 9, 18, 18, 18, 20, 22, 22, 22, 22, 20, 20, 20, 20, 19, 19, 19, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 25, 25, 25, 25, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 9, 9, 9, 9, 9, 18, 18, 18, 18, 18, 16, 16, 16, 16, 16, 16, 16, 16, 20, 20, 20, 20, 20, 20, 12, 11, 11, 10, 10, 10, 10, 13, 13, 13, 13, 13, 13, 13, 13, 13, 18, 21, 21, 21, 21, 21, 21, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 23, 23, 21, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 13, 13, 13, 13, 20, 20, 21, 21, 21, 21, 20, 20, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 6, 6, 6, 6, 6, 6, 6, 8, 21, 23, 23, 21, 18, 18, 18, 18, 18, 11, 11, 10, 10, 10, 9, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 14, 14, 14, 14, 14, 14, 14, 14, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 23, 23, 23, 17, 11, 11, 8, 5, 0, 0, 5, 0, 0, 13, 21, 21, 21, 21, 21, 21, 21, 23, 23, 23, 20, 13, 13, 13, 13, 11, 11, 11, 13, 21, 21, 21, 21, 21, 21, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 13, 8, 8, 8, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 20, 20, 15, 9, 9, 9, 9, 9, 9, 9, 18, 18, 20, 20, 20, 20, 20, 20, 11, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 13, 18, 18, 18, 20, 20, 20, 20, 20, 20, 20, 20, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 13, 13, 13, 13, 11, 20, 17, 0, 25, 25, 25, 13, 5, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 18, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 8, 8, 11, 11, 11, 12, 11, 11, 11, 9, 8, 7, 8, 8, 8, 8, 8, 8, 13, 13, 13, 13, 13, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 9, 8, 8, 8, 9, 13, 17, 17, 17, 17, 10, 10, 9, 10, 10, 11, 11, 11, 11, 11, 12, 12, 13, 11, 11, 9, 8, 8, 4, 4, 0, 0, 0, 0, 0, 0, 0, 17, 20, 20, 20, 14, 14, 14, 8, 2, 2, 2, 5, 8, 8, 8, 8, 8, 8, 8, 8, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tok2midi": {"0": 0, "1": 59, "2": 60, "3": 61, "4": 62, "5": 63, "6": 64, "7": 65, "8": 66, "9": 67, "10": 68, "11": 69, "12": 70, "13": 71, "14": 72, "15": 73, "16": 74, "17": 75, "18": 76, "19": 77, "20": 78, "21": 79, "22": 80, "23": 81, "24": 82, "25": 83, "26": 84, "27": 85}, "n_tokens": 28, "fps": 8, "midi_lo": 59, "midi_hi": 85}
|
piano/piano_mind.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
piano_mind.py -- a Modular Mind that plays piano, built with the SAME method as the
|
| 3 |
+
boss-fight brain: several tiny specialist MLPs emit latents, a RecursiveLink merges
|
| 4 |
+
them into one shared latent, and a coordinator reads it to pick the next note. Some
|
| 5 |
+
specialists "own" a register (Bass/Tenor/Soprano/Rest) and add a drive to those keys;
|
| 6 |
+
two are modulators (Onset/Phrase) that only write into the shared latent.
|
| 7 |
+
|
| 8 |
+
Trained by next-note prediction on a note sequence transcribed from a song
|
| 9 |
+
(piano/notes.json). Tiny -> trains in seconds and runs instantly for the self-playing
|
| 10 |
+
piano in the Space. torch is used (already a Space dep); the architecture mirrors
|
| 11 |
+
mm_torch / modular_mind.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
import json, os
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
+
K = 40 # context window (frames of recent notes) = 5s at 8fps
|
| 21 |
+
H, D_LATENT = 64, 32
|
| 22 |
+
# (name, owned register or None) -- mirrors the boss's 5 owners + 2 modulators
|
| 23 |
+
SPECS = [("Bass", "low"), ("Tenor", "mid"), ("Soprano", "high"),
|
| 24 |
+
("Rest", "rest"), ("Onset", None), ("Phrase", None)]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _group_masks(n_tokens):
|
| 28 |
+
"""token 0 = rest; notes 1..n-1 split into low/mid/high thirds."""
|
| 29 |
+
m = {g: torch.zeros(n_tokens) for g in ("low", "mid", "high", "rest")}
|
| 30 |
+
m["rest"][0] = 1.0
|
| 31 |
+
notes = list(range(1, n_tokens))
|
| 32 |
+
third = max(1, len(notes) // 3)
|
| 33 |
+
for j, t in enumerate(notes):
|
| 34 |
+
g = "low" if j < third else ("mid" if j < 2 * third else "high")
|
| 35 |
+
m[g][t] = 1.0
|
| 36 |
+
return m
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class PianoMind(nn.Module):
|
| 40 |
+
def __init__(self, n_tokens):
|
| 41 |
+
super().__init__()
|
| 42 |
+
self.n_tokens = n_tokens
|
| 43 |
+
self.fc1 = nn.ModuleList([nn.Linear(K, H) for _ in SPECS])
|
| 44 |
+
self.lat = nn.ModuleList([nn.Linear(H, D_LATENT) for _ in SPECS])
|
| 45 |
+
self.drv = nn.ModuleDict({n: nn.Linear(H, 1) for n, owns in SPECS if owns})
|
| 46 |
+
# RecursiveLink (ReGLU + residual), same shape as the boss bridge
|
| 47 |
+
self.ni = nn.LayerNorm(D_LATENT)
|
| 48 |
+
self.v = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
|
| 49 |
+
self.g = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
|
| 50 |
+
self.d = nn.Linear(2 * D_LATENT, D_LATENT, bias=False)
|
| 51 |
+
self.no = nn.LayerNorm(D_LATENT)
|
| 52 |
+
self.coord = nn.Linear(D_LATENT, n_tokens)
|
| 53 |
+
gm = _group_masks(n_tokens)
|
| 54 |
+
for g, t in gm.items():
|
| 55 |
+
self.register_buffer(f"mask_{g}", t)
|
| 56 |
+
|
| 57 |
+
def forward(self, feat): # feat [B, K] in [0,1]
|
| 58 |
+
B = feat.shape[0]
|
| 59 |
+
drives = torch.zeros(B, self.n_tokens, device=feat.device)
|
| 60 |
+
lats = []
|
| 61 |
+
for i, (name, owns) in enumerate(SPECS):
|
| 62 |
+
h = torch.tanh(self.fc1[i](feat))
|
| 63 |
+
lats.append(self.lat[i](h))
|
| 64 |
+
if owns:
|
| 65 |
+
drives = drives + self.drv[name](h) * getattr(self, f"mask_{owns}")
|
| 66 |
+
z = torch.stack(lats, 0).sum(0)
|
| 67 |
+
zn = self.ni(z)
|
| 68 |
+
shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
|
| 69 |
+
return drives + self.coord(shared) # logits [B, n_tokens]
|
| 70 |
+
|
| 71 |
+
@torch.no_grad()
|
| 72 |
+
def telemetry(self, feat): # feat [1, K] -> logits, per-specialist info, shared
|
| 73 |
+
drives = torch.zeros(1, self.n_tokens)
|
| 74 |
+
lats, per = [], []
|
| 75 |
+
for i, (name, owns) in enumerate(SPECS):
|
| 76 |
+
hh = torch.tanh(self.fc1[i](feat))
|
| 77 |
+
lat = self.lat[i](hh); lats.append(lat)
|
| 78 |
+
d = None
|
| 79 |
+
if owns:
|
| 80 |
+
d = float(self.drv[name](hh).item())
|
| 81 |
+
drives = drives + d * getattr(self, f"mask_{owns}")
|
| 82 |
+
per.append({"name": name, "owns": owns,
|
| 83 |
+
"drive": round(d, 3) if d is not None else None,
|
| 84 |
+
"act": round(float(lat.norm().item()), 3)})
|
| 85 |
+
z = torch.stack(lats, 0).sum(0); zn = self.ni(z)
|
| 86 |
+
shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
|
| 87 |
+
logits = drives + self.coord(shared)
|
| 88 |
+
return logits[0], per, [round(float(v), 2) for v in shared[0].tolist()]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _seq_feats(seq, n_tokens):
|
| 92 |
+
import torch
|
| 93 |
+
s = torch.tensor(seq, dtype=torch.long)
|
| 94 |
+
X, Y = [], []
|
| 95 |
+
for t in range(K, len(s)):
|
| 96 |
+
X.append(s[t - K:t].float() / n_tokens)
|
| 97 |
+
Y.append(s[t])
|
| 98 |
+
return torch.stack(X), torch.tensor(Y)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def train_and_save(notes_path=os.path.join(HERE, "notes.json"),
|
| 102 |
+
out=os.path.join(HERE, "piano_weights.pt"), epochs=900, seed=0):
|
| 103 |
+
torch.manual_seed(seed)
|
| 104 |
+
meta = json.load(open(notes_path))
|
| 105 |
+
seq, n_tokens = meta["seq"], meta["n_tokens"]
|
| 106 |
+
X, Y = _seq_feats(seq, n_tokens)
|
| 107 |
+
model = PianoMind(n_tokens)
|
| 108 |
+
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
|
| 109 |
+
n = X.shape[0]; bs = 512
|
| 110 |
+
for ep in range(epochs):
|
| 111 |
+
perm = torch.randperm(n)
|
| 112 |
+
tot = 0.0
|
| 113 |
+
for i in range(0, n, bs):
|
| 114 |
+
idx = perm[i:i + bs]
|
| 115 |
+
logits = model(X[idx])
|
| 116 |
+
loss = F.cross_entropy(logits, Y[idx])
|
| 117 |
+
opt.zero_grad(); loss.backward(); opt.step()
|
| 118 |
+
tot += loss.item() * len(idx)
|
| 119 |
+
if ep % 80 == 0 or ep == epochs - 1:
|
| 120 |
+
with torch.no_grad():
|
| 121 |
+
acc = (model(X).argmax(1) == Y).float().mean().item()
|
| 122 |
+
print(f" epoch {ep:4d} loss {tot/n:.3f} next-note acc {acc:.3f}")
|
| 123 |
+
import numpy as _np
|
| 124 |
+
arr = _np.array(seq) # seed from the most-sounding window (song fades in)
|
| 125 |
+
bi = int(max(range(len(seq) - K), key=lambda i: int((arr[i:i + K] > 0).sum())))
|
| 126 |
+
torch.save({"state": model.state_dict(), "n_tokens": n_tokens,
|
| 127 |
+
"tok2midi": meta["tok2midi"], "K": K, "fps": meta.get("fps", 8),
|
| 128 |
+
"seed_seq": seq[bi:bi + K]}, out)
|
| 129 |
+
print(f"saved -> {out}")
|
| 130 |
+
return model, meta
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class PianoPlayer:
|
| 134 |
+
"""Loads the trained PianoMind and autoregressively yields the next note token."""
|
| 135 |
+
def __init__(self, weights=os.path.join(HERE, "piano_weights.pt")):
|
| 136 |
+
ck = torch.load(weights, map_location="cpu")
|
| 137 |
+
self.n_tokens = ck["n_tokens"]; self.K = ck["K"]; self.fps = ck["fps"]
|
| 138 |
+
self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
|
| 139 |
+
self.seed_seq = ck["seed_seq"]
|
| 140 |
+
self.model = PianoMind(self.n_tokens); self.model.load_state_dict(ck["state"]); self.model.eval()
|
| 141 |
+
|
| 142 |
+
@torch.no_grad()
|
| 143 |
+
def next_token(self, history, temperature=0.95, anti_silence=3, rep_penalty=1.5):
|
| 144 |
+
h = list(history)[-self.K:]
|
| 145 |
+
if len(h) < self.K:
|
| 146 |
+
h = [0] * (self.K - len(h)) + h
|
| 147 |
+
feat = torch.tensor([[x / self.n_tokens for x in h]], dtype=torch.float32)
|
| 148 |
+
logits, per, shared = self.model.telemetry(feat)
|
| 149 |
+
logits = logits / max(1e-3, temperature)
|
| 150 |
+
# keep it musical: don't collapse to silence, don't get stuck on one key
|
| 151 |
+
if anti_silence and all(t == 0 for t in h[-anti_silence:]):
|
| 152 |
+
logits[0] -= 8.0
|
| 153 |
+
for t in set(h[-3:]):
|
| 154 |
+
if t > 0:
|
| 155 |
+
logits[t] -= rep_penalty
|
| 156 |
+
tok = int(torch.multinomial(F.softmax(logits, -1), 1).item())
|
| 157 |
+
return tok, self.tok2midi.get(tok, 0), {"spec": per, "shared": shared}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
model, meta = train_and_save()
|
| 162 |
+
# quick listen: generate 40 notes from the song's seed
|
| 163 |
+
p = PianoPlayer()
|
| 164 |
+
hist = list(p.seed_seq)
|
| 165 |
+
out = []
|
| 166 |
+
for _ in range(40):
|
| 167 |
+
tok, midi, _ = p.next_token(hist); hist.append(tok); out.append(midi)
|
| 168 |
+
print("sample MIDI stream:", out)
|
piano/poly_mind.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
poly_mind.py -- a CHORD-capable Modular Mind (same method as the boss / mono piano:
|
| 3 |
+
specialists -> RecursiveLink -> coordinator), but multi-LABEL: it predicts the SET of
|
| 4 |
+
notes sounding in the next frame, not a single note. Trained by multi-label next-frame
|
| 5 |
+
prediction on a polyphonic transcription (poly_notes.json, a piano-roll of chords).
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import json, os
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
|
| 14 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
K, H, D_LATENT = 16, 64, 32 # context frames (=2s @8fps), hidden, latent
|
| 16 |
+
SPECS = [("Bass", "low"), ("Tenor", "mid"), ("Soprano", "high"),
|
| 17 |
+
("Sustain", None), ("Onset", None), ("Phrase", None)]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _group_masks(n):
|
| 21 |
+
m = {g: torch.zeros(n) for g in ("low", "mid", "high")}
|
| 22 |
+
third = max(1, n // 3)
|
| 23 |
+
for i in range(n):
|
| 24 |
+
g = "low" if i < third else ("mid" if i < 2 * third else "high")
|
| 25 |
+
m[g][i] = 1.0
|
| 26 |
+
return m
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class PolyMind(nn.Module):
|
| 30 |
+
def __init__(self, n_notes):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.n = n_notes
|
| 33 |
+
nf = K * n_notes
|
| 34 |
+
self.fc1 = nn.ModuleList([nn.Linear(nf, H) for _ in SPECS])
|
| 35 |
+
self.lat = nn.ModuleList([nn.Linear(H, D_LATENT) for _ in SPECS])
|
| 36 |
+
self.drv = nn.ModuleDict({nm: nn.Linear(H, 1) for nm, o in SPECS if o})
|
| 37 |
+
self.ni = nn.LayerNorm(D_LATENT)
|
| 38 |
+
self.v = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
|
| 39 |
+
self.g = nn.Linear(D_LATENT, 2 * D_LATENT, bias=False)
|
| 40 |
+
self.d = nn.Linear(2 * D_LATENT, D_LATENT, bias=False)
|
| 41 |
+
self.no = nn.LayerNorm(D_LATENT)
|
| 42 |
+
self.coord = nn.Linear(D_LATENT, n_notes)
|
| 43 |
+
for gname, t in _group_masks(n_notes).items():
|
| 44 |
+
self.register_buffer("mask_" + gname, t)
|
| 45 |
+
|
| 46 |
+
def _core(self, feat, tel=False):
|
| 47 |
+
drives = torch.zeros(feat.shape[0], self.n, device=feat.device)
|
| 48 |
+
lats, per = [], []
|
| 49 |
+
for i, (nm, o) in enumerate(SPECS):
|
| 50 |
+
h = torch.tanh(self.fc1[i](feat))
|
| 51 |
+
lat = self.lat[i](h); lats.append(lat)
|
| 52 |
+
dd = None
|
| 53 |
+
if o:
|
| 54 |
+
dd = self.drv[nm](h)
|
| 55 |
+
drives = drives + dd * getattr(self, "mask_" + o)
|
| 56 |
+
if tel:
|
| 57 |
+
per.append({"name": nm, "owns": o,
|
| 58 |
+
"drive": round(float(dd.mean().item()), 3) if dd is not None else None,
|
| 59 |
+
"act": round(float(lat.norm().item()), 3)})
|
| 60 |
+
z = torch.stack(lats, 0).sum(0); zn = self.ni(z)
|
| 61 |
+
shared = self.no(self.d(F.relu(self.g(zn)) * self.v(zn)) + z)
|
| 62 |
+
logits = drives + self.coord(shared)
|
| 63 |
+
return logits, per, shared
|
| 64 |
+
|
| 65 |
+
def forward(self, feat):
|
| 66 |
+
return self._core(feat)[0]
|
| 67 |
+
|
| 68 |
+
@torch.no_grad()
|
| 69 |
+
def telemetry(self, feat):
|
| 70 |
+
logits, per, shared = self._core(feat, tel=True)
|
| 71 |
+
return logits[0], per, [round(float(v), 2) for v in shared[0].tolist()]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _rolls(seq, n):
|
| 75 |
+
R = np.zeros((len(seq), n), dtype=np.float32)
|
| 76 |
+
for i, fr in enumerate(seq):
|
| 77 |
+
for t in fr:
|
| 78 |
+
R[i, t] = 1.0
|
| 79 |
+
return R
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def train_and_save(notes=os.path.join(HERE, "poly_notes.json"),
|
| 83 |
+
out=os.path.join(HERE, "poly_weights.pt"), epochs=700, seed=0):
|
| 84 |
+
torch.manual_seed(seed)
|
| 85 |
+
meta = json.load(open(notes)); seq, n = meta["seq"], meta["n_tokens"]
|
| 86 |
+
R = _rolls(seq, n)
|
| 87 |
+
X = np.stack([R[t - K:t].reshape(-1) for t in range(K, len(R))])
|
| 88 |
+
Y = np.stack([R[t] for t in range(K, len(R))])
|
| 89 |
+
X, Y = torch.tensor(X), torch.tensor(Y)
|
| 90 |
+
model = PolyMind(n)
|
| 91 |
+
opt = torch.optim.Adam(model.parameters(), lr=8e-3)
|
| 92 |
+
pw = torch.tensor(float((Y.numel() - Y.sum()) / (Y.sum() + 1))) # balance sparse positives
|
| 93 |
+
N, bs = X.shape[0], 512
|
| 94 |
+
for ep in range(epochs):
|
| 95 |
+
perm = torch.randperm(N); tot = 0.0
|
| 96 |
+
for i in range(0, N, bs):
|
| 97 |
+
idx = perm[i:i + bs]
|
| 98 |
+
loss = F.binary_cross_entropy_with_logits(model(X[idx]), Y[idx], pos_weight=pw)
|
| 99 |
+
opt.zero_grad(); loss.backward(); opt.step(); tot += loss.item() * len(idx)
|
| 100 |
+
if ep % 100 == 0 or ep == epochs - 1:
|
| 101 |
+
with torch.no_grad():
|
| 102 |
+
p = (torch.sigmoid(model(X)) > 0.5).float()
|
| 103 |
+
tp = (p * Y).sum(); prec = tp / (p.sum() + 1); rec = tp / (Y.sum() + 1)
|
| 104 |
+
print(f" ep {ep:4d} loss {tot/N:.3f} precision {prec:.2f} recall {rec:.2f}")
|
| 105 |
+
cnt = R.sum(1)
|
| 106 |
+
bi = int(np.argmax(np.convolve(cnt, np.ones(K), "valid")))
|
| 107 |
+
torch.save({"state": model.state_dict(), "n_tokens": n, "tok2midi": meta["tok2midi"],
|
| 108 |
+
"K": K, "fps": meta.get("fps", 8), "seed": [seq[bi + j] for j in range(K)]}, out)
|
| 109 |
+
print("saved ->", out)
|
| 110 |
+
return model, meta
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class PolyPlayer:
|
| 114 |
+
def __init__(self, weights=os.path.join(HERE, "poly_weights.pt")):
|
| 115 |
+
ck = torch.load(weights, map_location="cpu")
|
| 116 |
+
self.n, self.K, self.fps = ck["n_tokens"], ck["K"], ck["fps"]
|
| 117 |
+
self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
|
| 118 |
+
self.seed = ck["seed"]
|
| 119 |
+
self.model = PolyMind(self.n); self.model.load_state_dict(ck["state"]); self.model.eval()
|
| 120 |
+
|
| 121 |
+
@torch.no_grad()
|
| 122 |
+
def next_frame(self, history, thresh=0.45, maxn=4):
|
| 123 |
+
h = list(history)[-self.K:]
|
| 124 |
+
while len(h) < self.K:
|
| 125 |
+
h = [[]] + h
|
| 126 |
+
R = np.zeros((self.K, self.n), dtype=np.float32)
|
| 127 |
+
for i, fr in enumerate(h):
|
| 128 |
+
for t in fr:
|
| 129 |
+
if 0 <= t < self.n:
|
| 130 |
+
R[i, t] = 1.0
|
| 131 |
+
feat = torch.tensor(R.reshape(1, -1))
|
| 132 |
+
logits, per, shared = self.model.telemetry(feat)
|
| 133 |
+
probs = torch.sigmoid(logits)
|
| 134 |
+
# anti-silence: if the last 2 frames were empty, force the most likely note(s)
|
| 135 |
+
if all(len(f) == 0 for f in h[-2:]):
|
| 136 |
+
thresh = min(thresh, float(probs.max()) * 0.6 + 1e-3)
|
| 137 |
+
idx = (probs > thresh).nonzero().flatten().tolist()
|
| 138 |
+
if len(idx) > maxn:
|
| 139 |
+
idx = torch.topk(probs, maxn).indices.tolist()
|
| 140 |
+
toks = sorted(int(t) for t in idx)
|
| 141 |
+
return toks, [self.tok2midi.get(t, 0) for t in toks], {"spec": per, "shared": shared}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
train_and_save()
|
| 146 |
+
p = PolyPlayer()
|
| 147 |
+
hist = list(p.seed)
|
| 148 |
+
print("sample chords (MIDI sets):")
|
| 149 |
+
for _ in range(10):
|
| 150 |
+
toks, mid, _ = p.next_frame(hist); hist.append(toks); print(" ", mid)
|
piano/poly_notes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"seq": [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [6], [6], [6], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [7], [6], [], [], [], [], [], [], [], [], [], [6], [9], [9], [9], [], [9], [9], [9], [9], [9], [], [9], [9], [9], [9], [9, 11], [9, 11], [9], [11], [], [3], [3, 6], [9], [11], [11], [11], [11], [], [11], [11], [11], [11], [1], [1], [1, 11], [1], [1], [1], [], [1], [1], [1, 13], [13], [1], [1], [1, 16], [1, 16], [1, 23], [], [13], [25], [25], [13, 25, 37], [4, 7, 13], [4, 19], [4, 19], [4], [4], [4, 23], [4, 23], [23], [14], [14], [14, 23], [4, 40], [4], [4, 33], [4], [4], [4, 14, 33], [14, 23], [14, 23], [4, 14, 23], [14], [], [2], [2], [2], [2], [2], [2], [2], [2], [2], [14], [2], [2, 24], [2, 24], [2], [2], [2], [2], [2], [2, 21], [14], [], [], [11, 15], [15], [15], [15], [15, 27], [11, 21], [11], [], [15], [15], [15], [15], [15], [15, 21], [], [], [], [15], [15], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 7, 26, 37], [4, 7], [4, 7, 28, 38], [7, 28, 31, 35], [7, 28, 31, 35], [7, 16], [7, 16, 28, 30], [19, 28, 30, 35], [7, 16, 28, 30], [7, 11, 28, 30], [9, 30, 37], [7, 11, 14, 30], [11, 14, 26], [11, 14, 26], [11, 14, 16], [11, 14, 16], [11, 14, 26], [11, 14, 18, 26], [11, 20, 26], [11, 14, 19, 31], [11, 19], [12, 19], [9, 13, 19], [9, 14, 19, 26], [12, 14, 19, 26], [12, 14, 19, 26], [4, 12, 26], [12], [12, 18], [11], [12, 16, 24], [11, 24], [11, 16], [11, 19], [18, 23], [11, 18, 23], [11, 17, 23], [11, 18, 23], [9, 18], [17], [9, 18], [9, 18], [9], [11, 19, 20], [19, 22], [19, 23], [19, 23], [19, 23], [19, 23], [11, 20, 43], [11, 21, 43], [7, 12, 21], [11, 43], [9, 11], [9, 18, 30], [9, 18, 30], [6, 9, 18, 30], [9, 12, 18, 30], [7, 18, 30], [7, 18, 30, 31], [7, 12, 18, 30], [6, 11, 28], [6, 11, 16, 28], [6, 11, 28], [11, 16], [16, 23, 28], [16, 23, 28, 30], [11, 16, 23, 28], [11, 16, 28], [11, 16, 28], [11, 29], [11, 16], [9, 16], [9, 19], [7], [6, 9], [6, 9, 18], [6, 9], [6, 9, 18], [6, 9, 21], [6, 9, 18], [6, 9, 18], [6, 9, 18], [6, 9], [6, 11, 19], [6, 11, 31], [23, 30, 33, 38], [30, 33, 35, 38], [7, 33], [7, 19], [6], [7, 16, 19], [6, 16, 26, 28], [6, 16, 26, 28], [16, 19, 26, 28], [7, 19], [7, 9, 22, 31], [28, 30, 36, 37], [28, 30, 33, 37], [12, 18, 28, 30], [9, 30], [7, 20], [12, 20, 24, 33], [12, 24, 28, 33], [12, 21, 28, 33], [7, 12, 21], [7], [7, 11, 15, 18], [11, 18], [30, 33], [6, 11, 15, 23], [9], [7, 9, 20, 32], [19, 21, 27, 33], [7, 33], [7, 11, 21, 28], [9, 11, 19], [9, 11, 19], [11, 19], [11, 30, 31, 40], [11, 30, 31, 40], [11, 30, 37], [11, 30], [19, 23, 31, 35], [11, 19], [11], [7, 11], [11, 18], [12, 18], [2, 18, 30, 33], [12, 18, 30, 33], [6, 12, 30, 33], [12, 26, 30, 33], [12, 26, 30, 33], [12, 18, 26, 33], [12, 26, 33, 42], [12, 16, 28, 38], [12, 28], [12, 27], [12, 18, 27, 34], [12, 18, 27, 34], [11, 18, 27, 34], [6, 11, 18, 26], [11, 18, 26], [11, 15, 18], [11, 15, 18], [11, 18, 27, 33], [11, 15, 18, 27], [11, 15, 18], [11, 15, 18], [11, 15, 18], [11], [6, 11], [11], [], [11], [11], [15], [15], [11, 15], [0, 3], [], [], [6], [], [], [], [], [21], [], [], [], [27], [23], [], [], [21], [], [], [], [6], [17, 19, 23], [4, 15, 19, 30], [4, 16, 19, 28], [16, 19], [4, 16, 19], [4, 19], [7, 15, 19], [16, 18, 28, 30], [7, 16, 18, 30], [18, 30, 35, 40], [18, 28, 30, 40], [7, 18, 28, 30], [7, 11, 14, 30], [7, 11, 18], [11, 16, 26], [16, 26, 28], [11, 16, 26], [11, 18, 26, 33], [14, 19, 26, 33], [11, 19, 26, 31], [11, 19, 26, 33], [11, 19, 26, 33], [9, 12, 19, 31], [12, 20, 26], [12, 14, 19], [12, 14, 19], [12], [10, 12, 18], [12, 18], [12, 16, 18], [12, 16], [11, 16], [11, 16], [11, 16], [11, 18], [11, 18], [11, 18, 30], [18, 33], [11, 18], [9, 11, 18], [9, 11, 18], [9, 11, 18, 30], [9, 11], [0, 9, 21, 33], [11, 19, 31, 35], [19, 31, 35, 38], [11, 31, 35, 38], [11, 19, 23, 35], [11, 23, 35], [11, 33], [11, 21, 33], [21, 24, 33], [11], [11, 25], [18, 24, 33], [24], [9, 18, 24], [6, 9, 18, 29], [9, 18, 23], [7, 23], [18, 23, 35, 42], [7, 23], [6, 16, 28], [6, 9], [6, 21], [4, 19, 30, 33], [23, 31, 35, 38], [23, 31, 35, 38], [32, 35], [19, 23, 33, 35], [21, 31, 33, 38], [19, 31, 33, 38], [21, 30, 33, 37], [7, 21, 28, 33], [16, 19, 21, 28], [6, 16, 21, 33], [6, 16, 21, 28], [6, 33], [6, 21], [6, 21], [6], [6], [6], [6], [6], [6, 14, 24], [7, 31, 33, 35], [31, 33, 35, 42], [31, 35], [7], [7], [6, 14, 17], [6, 16, 20], [6, 16], [7, 16], [7, 16], [6, 21, 31], [21, 30, 33, 36], [9, 30, 33, 40], [9, 30, 33], [9], [9], [7, 22], [24, 33, 35, 36], [24, 33, 35, 40], [7, 21, 24, 35], [19, 36], [7, 15, 17, 23], [7, 15, 18, 23], [23], [6, 15, 18, 23], [9], [9, 21, 33], [7, 21, 33], [7, 21, 33], [7, 21, 27, 33], [], [9, 15, 19, 23], [19, 31, 35], [23, 31, 35, 40], [31, 34, 35, 40], [23, 31, 35, 40], [19, 23, 31, 40], [11, 23, 31, 40], [11, 28], [11], [11, 16], [11, 16], [12, 23, 29, 35], [2, 12, 25, 42], [12, 26, 30, 33], [12, 27, 30, 33], [12, 27, 30, 33], [12, 27, 30, 42], [12, 26, 33], [12, 33, 42], [12, 24, 30, 33], [12, 16, 28], [12, 16, 28, 35], [11, 16, 23], [11, 15, 35], [11, 15, 18, 35], [12, 15, 18, 35], [12, 15, 23, 35], [15, 18, 23, 35], [15, 18, 23, 35], [11, 15, 18, 35], [15, 18, 22, 35], [15, 18, 23, 35], [15, 18, 23, 35], [11, 15, 22, 34], [11, 15, 23, 35], [11, 15, 35], [11, 15, 23], [23], [], [11], [11], [6], [], [8], [3, 6], [], [1], [], [5], [2], [21], [6, 23], [4, 23], [], [5], [], [], [], [0], [4, 10], [2], [1, 5, 10], [0, 10], [10], [0, 10], [4, 28], [4, 18, 28], [4, 20, 28], [4, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 26], [12, 20, 26, 32], [0, 27, 31], [0, 26, 31], [7, 12, 26, 30], [7, 12, 30], [0, 7, 12, 27], [12, 28, 30, 37], [7, 12, 27, 28], [2, 26, 28, 35], [2, 9, 38], [2, 30, 37, 38], [9, 26, 30, 37], [2, 9, 27, 30], [2, 26, 30, 37], [2, 26, 30, 37], [2, 14, 23, 30], [4], [4, 11, 22, 43], [4, 24, 38], [4, 16], [38], [4, 8, 11], [4, 11], [4, 9, 11], [18, 27], [20, 28], [16, 19, 28], [16, 19, 28], [2, 19, 28], [4, 19, 30], [4, 11, 16, 30], [4, 11, 19, 30], [0, 4, 18, 30], [0, 7, 19], [0, 7, 19, 30], [0, 16, 19, 31], [0, 16, 30], [0, 16, 19, 28], [7, 16, 28], [7, 16, 28], [9, 16, 26, 28], [9, 18, 26, 30], [18, 26, 30, 38], [18, 26, 30, 38], [9, 26, 30, 38], [2, 18, 26, 30], [2, 9, 18, 26], [4, 26, 40], [4], [4], [4, 28], [4, 11, 28], [4, 11, 28], [4, 8, 11], [4], [4, 30], [4, 30, 37], [4, 29, 31, 38], [4, 19, 31, 38], [19, 28], [4, 7, 19, 28], [4, 19, 28], [4, 11, 19, 28], [4, 19], [4, 7, 18], [7, 19, 26, 31], [0, 19, 26, 31], [0, 19, 26], [0, 4, 26], [0, 18, 24], [0, 7, 18, 24], [0, 18, 24], [2, 9, 22, 30], [9, 18, 23, 30], [2, 9, 23, 30], [9, 18, 23, 30], [9, 18, 21, 30], [2, 18, 21], [9, 18, 21, 30], [4, 16, 19, 21], [4, 38], [4, 38, 40], [4], [4, 11, 28, 35], [4, 11], [4], [4], [4], [4, 19, 30], [31, 35], [2, 20], [4, 7, 19, 24], [4, 7, 19, 23], [19, 28], [4, 11, 19, 28], [4, 18, 28], [0, 3, 19, 30], [0, 19], [0, 16, 19, 31], [0, 16, 30, 31], [0, 16, 30], [0, 16, 28, 30], [0, 7, 16], [4, 16, 28], [9, 18, 28], [9, 18, 30], [2, 9, 18, 30], [2, 9, 18, 30], [4, 9, 18, 26], [9, 18, 27, 30], [9, 18, 30], [9, 16, 26], [4, 11, 26, 28], [11, 28, 35, 40], [3, 11, 28], [1, 11, 28], [11, 27, 28, 35], [4, 27, 28], [4, 8, 28], [4, 28], [4, 19, 31, 38], [4, 19, 31, 40], [28, 31, 38, 40], [4, 28, 31, 40], [4, 28, 31, 40], [28, 31, 40], [11, 19, 31, 38], [4, 19, 27], [4, 19, 31, 38], [0, 26, 31, 38], [0, 31, 38], [0, 26, 31, 38], [26, 31], [7, 19, 26, 31], [0, 7, 19, 26], [0, 7, 16], [6, 9, 29], [9, 30], [2, 9, 18, 26], [2, 9], [2, 6, 9], [2, 9, 18, 24], [2, 9, 24], [2], [4, 24], [4, 23], [4], [4, 11], [4, 11], [11, 29, 35], [16, 28, 35], [7, 14, 16], [30, 38, 43], [4, 31, 38, 40], [4, 31, 38, 40], [4, 28, 31, 38], [4, 28, 31], [4, 30, 31], [4, 16, 30, 31], [4, 19, 30], [0, 4, 31], [0, 31, 38], [16, 31, 35, 38], [0, 31, 42], [2, 7, 16], [16, 19, 27, 28], [28, 31, 38, 40], [0, 31, 38, 40], [18], [26, 30, 36], [18, 26], [2, 4, 18, 30], [2, 4, 9], [2, 8, 18, 28], [2, 18, 28], [2, 4, 37], [4, 16, 28], [4, 16, 19, 28], [4, 19, 28], [4, 11], [4, 19], [4, 20, 29], [4, 19], [4, 8], [4, 31], [4, 19, 31], [4, 18, 31], [19, 28], [4, 11, 19], [4, 19, 29], [19, 28], [4, 19, 27], [0, 4, 7, 31], [0, 7, 31], [0, 7, 31, 38], [27, 31, 38], [26, 31], [0, 7, 43], [0, 24], [0, 7, 10], [2, 21, 28, 33], [9, 30, 35, 37], [2, 9, 30, 35], [30, 33, 35, 42], [9, 21, 29, 33], [2, 6, 18, 22], [2, 9, 19], [2, 16, 19], [4, 19], [4, 16, 19], [4, 19], [4, 11, 19], [11, 19, 23], [4, 8, 11], [4, 38], [4, 7, 16, 18], [4, 11, 19, 31], [11, 31], [2, 19, 31, 38], [7, 19], [2, 4, 7], [1, 4, 19, 28], [4, 19, 28], [19, 28, 29], [0, 4, 19], [19, 31, 38], [0, 7, 31], [0, 30, 31], [0, 16], [0, 16, 20, 29], [0, 16], [0, 7, 16], [9, 29, 30], [2, 9], [2, 9, 18], [9, 18, 26], [4, 18, 26], [2, 18, 26], [2, 10, 18, 28], [3, 18], [4], [4, 16], [4, 29], [0, 11, 16, 28], [11, 16, 28], [4, 11], [4], [4, 7, 12], [4, 28, 31], [19, 28, 35, 38], [4, 19, 28, 31], [4, 28, 31, 35], [4, 6, 16, 19], [4, 7, 19, 31], [30, 33, 37, 40], [18, 21], [4, 11, 19, 21], [7, 11, 19, 23], [19, 24, 31, 36], [19, 23, 35], [11, 23, 31, 37], [19, 23, 31, 38], [19, 23, 31, 38], [11, 31, 38], [12, 31, 32, 33], [4, 33, 36, 40], [24, 33, 36, 40], [12, 28, 33, 40], [12, 19, 28, 33], [23, 28, 31, 33], [11, 28, 31, 35], [11, 28], [4, 18, 21], [18, 21, 26], [6, 18, 22, 27], [6, 18, 21, 27], [18, 23], [6, 9, 18], [9, 18], [11, 16, 18, 22], [17, 22, 29], [19, 23, 29, 31], [7, 19, 23, 31], [7, 19, 31, 35], [7, 23, 28, 31], [19, 23, 28, 31], [19, 23, 29, 31], [11, 19, 20, 27], [3, 6, 9, 30], [9, 21, 23, 30], [30, 32, 35, 40], [7, 28, 31, 35], [2, 7, 9], [7, 15, 18], [6, 15, 18, 23], [6, 9, 15], [7, 19, 27, 30], [4, 19, 28, 31], [4, 28, 31, 35], [4, 28, 35, 38], [4, 19, 28, 35], [4, 19, 28], [4, 28, 35, 38], [4, 19, 21, 28], [4, 18, 27, 30], [6, 15, 27], [6, 15, 27, 43], [6, 27, 43], [6], [6], [6], [6], [2, 15, 18], [16, 19], [6, 16, 18, 23], [6, 15, 23], [6, 15, 19], [6, 28, 35, 38], [9, 28], [7, 13], [4, 9, 33, 41], [30, 33, 37, 40], [19, 24, 29, 33], [9, 16, 19], [19, 28, 31, 38], [4, 19, 28, 38], [7, 19, 28], [4, 7], [7, 30, 33], [6, 28, 30, 33], [6, 20, 29], [6, 16, 28, 35], [6, 11, 16, 35], [18, 27, 30, 33], [27, 30, 33], [6, 21, 33], [4, 29, 30, 33], [23, 28, 31, 35], [11, 28, 31, 35], [11, 28, 31, 35], [11, 31], [4, 6, 11, 31], [11, 19, 31], [6, 11], [12, 19, 23], [12, 21, 24], [6, 12, 24, 28], [12, 24, 28, 33], [12, 24, 28, 33], [12, 28, 33], [12, 21, 28, 33], [7, 12], [3, 20, 33, 35], [27, 30, 35], [30, 35], [2, 30, 35], [30, 35], [11, 28, 30, 34], [11, 27, 30, 35], [23, 27, 30, 35], [11, 27, 30, 34], [11, 27, 30], [11, 28, 30], [22, 28, 30, 35], [3, 11, 30], [30, 35], [11, 18, 30, 35], [11, 30, 35], [4, 18, 30, 33], [3], [3, 10], [3, 10, 33], [3, 10], [2, 10], [3, 10], [3, 10], [2], [3, 10], [3, 10], [3, 10], [3, 10, 33], [3, 10], [3, 10], [3, 10], [4, 10], [3, 10], [3, 10], [3, 10], [3, 10], [2, 10], [9], [8], [4, 30, 31, 34], [19, 31, 35], [4, 23, 31, 38], [4, 18, 31, 38], [7, 18, 23, 35], [4, 18, 21, 30], [30, 33, 35, 37], [4, 11, 30, 33], [4, 18, 21, 33], [19, 23, 35], [11, 20, 23, 35], [11, 20, 23, 35], [19, 23], [11, 23, 35, 42], [7, 11, 19], [11, 18, 20, 23], [4, 12, 22, 35], [12, 33, 36], [12, 24, 33, 36], [9, 12, 33, 36], [9, 12, 19], [19, 23, 35, 40], [11, 31, 35, 38], [11, 21], [3, 11, 18, 21], [11, 19, 21], [9, 11, 19, 21], [11, 19, 21], [6, 21], [6, 9, 21], [9, 21], [9, 18], [0, 18, 21], [7, 19, 23, 35], [19, 23, 31, 42], [11, 19, 35, 42], [19, 23, 30, 35], [11, 19, 23], [11, 19, 23, 35], [11, 18, 26, 35], [4, 9, 18, 33], [9, 18, 21, 33], [28, 32, 33, 35], [28, 31, 35, 38], [7, 28, 31], [2, 6, 15, 17], [6, 9, 15, 30], [6, 15, 18], [4, 27, 30], [16, 28, 31, 38], [16, 19, 28, 31], [4, 19, 28, 31], [19, 28, 31, 33], [19, 28, 31], [7, 28, 31, 38], [4, 19, 28, 31], [6, 27, 30, 35], [6, 27, 30, 35], [6, 26, 30, 35], [6, 30, 35], [6, 30, 35], [6, 35], [6], [6], [14, 17, 19], [16, 19, 43], [7, 15, 19], [6, 9, 15, 18], [6, 9, 15, 31], [28, 31, 35, 38], [7, 31], [7, 16], [28, 31, 33, 38], [21, 30, 33, 37], [16, 30, 33], [16, 33], [19, 28, 31, 35], [16, 28, 31, 38], [4, 9, 19, 31], [4, 7, 16], [23, 34, 36, 41], [30, 33, 37, 40], [20, 23, 33, 40], [19, 23, 40], [6, 11], [0, 19, 21], [11, 24, 33], [6, 19, 24], [4, 36, 43], [31, 35, 40], [31, 35, 38, 40], [20, 28, 35, 40], [11, 28, 35, 40], [28, 35, 42], [6, 28, 35, 42], [6, 23], [12, 16, 24, 29], [18, 25, 30], [6, 18, 24, 40], [18, 24, 30, 40], [18, 24, 36, 40], [0, 18, 24, 40], [12, 18, 24, 28], [12], [4, 24], [11, 15, 27, 35], [11, 27, 35], [11, 15, 27, 35], [3, 11, 27, 35], [11, 15, 27, 35], [11, 23, 27, 35], [27, 35, 42], [8, 11, 27, 35], [3, 23, 27, 35], [3, 27, 35], [3, 27, 35], [23, 27, 35], [15, 26, 35, 42], [15, 23, 26, 35], [23, 27, 35], [2, 6, 15, 23], [11, 27, 35], [6], [], [], [6, 15], [15], [6, 15], [6], [], [], [], [], [2, 9], [1], [11], [3, 5, 11], [11], [], [], [], [], [35], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 40], [4], [4], [40], [], [40], [40], [40], [], [40], [], [], [], [40], [40], [], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4], [], [4], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4, 40], [4, 40], [4], [], [], [], [40], [40], [], [40], [33, 40], [], [], [40], [40], [40], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [4], [4], [4], [], [], [], [], [], [], [9], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [40], [], [], [40], [40], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [8], [8], [8], [8], [0, 8], [8], [2, 5, 8], [4, 7, 11, 23], [4, 8, 11], [4, 7, 11], [4, 8, 11], [4, 8, 11], [3, 7, 19], [4, 8, 11], [5, 7, 11, 40], [4, 8, 11], [4, 7, 11], [6, 9, 14, 21], [1, 9, 16, 21], [16, 21], [1, 10, 16, 21], [10, 16, 20, 21], [4, 16, 20, 21], [4, 7, 11, 20], [8, 16, 19], [4, 8], [4, 8, 20], [8, 19, 28], [4, 7, 11, 16], [4, 8, 11, 18], [8, 18], [4, 7, 11, 18], [4, 18, 30], [10, 19, 33], [9, 14, 21], [16, 22], [1, 10, 16, 21], [1, 10, 16, 21], [1, 7, 16, 19], [4, 7, 19, 28], [8, 16, 19, 28], [7, 16, 19, 28], [3, 16, 19], [8, 16, 19], [0, 4, 7], [8, 16, 19], [0, 7, 16], [4, 7, 11], [4, 8, 11, 16], [0, 10, 16, 33], [1, 16, 28, 42], [16], [10, 16, 28], [16, 28], [2, 8, 28], [4, 7, 19, 28], [8, 16], [6, 11, 16], [4, 11, 16], [4, 8, 28], [2, 7, 11, 16], [5, 7, 16], [1, 8, 16], [4, 7, 11], [4, 8, 43], [1, 10, 33], [5, 9], [6, 9], [1, 10], [0, 10], [0, 5, 10], [4, 7, 11], [4, 8], [4, 8, 19, 40], [0, 4, 8], [8, 23], [4, 7, 11, 43], [8, 11], [4, 7], [4, 7, 11, 23], [0, 4, 8], [1, 9, 15, 20], [10, 16, 21, 43], [16, 21], [1, 10, 16, 21], [2, 10, 16, 21], [3, 8, 16, 19], [4, 7, 19, 28], [8, 16, 19, 23], [7, 16, 19, 28], [4, 16, 19, 23], [16, 19], [4, 7], [4, 8, 16, 18], [3, 8, 16, 18], [4, 7, 16, 30], [4, 18, 30, 40], [5, 9, 15], [2, 16, 22, 24], [3, 16, 24, 28], [0, 10, 30], [10], [0, 5, 23], [7, 16, 23, 28], [8, 16, 23], [4, 7, 16, 23], [0, 16, 23], [16, 19, 23, 28], [4, 7, 19, 28], [8, 16, 19, 28], [8, 19, 23], [4, 7, 11, 19], [4, 9, 18], [10, 15, 18, 21], [16, 18], [16, 18], [10, 16, 18, 30], [0, 16, 18, 30], [8, 16, 18, 28], [4, 16, 19], [4, 8, 16], [4, 7, 40], [4, 8, 11, 16], [4, 8, 16], [4, 7, 11], [1, 4, 8, 16], [4, 8, 16], [4, 7, 11], [4, 9, 16], [6, 9, 16, 33], [5, 9, 16], [2, 6], [0, 10, 33], [0, 5, 10], [0, 6, 10, 33], [9], [0], [1, 6, 10], [1, 10], [0, 10], [4, 7], [16, 19, 28, 31], [16, 19, 28, 31], [4, 7, 19, 28], [8, 19, 28, 31], [4, 7, 28, 31], [4, 7, 20, 28], [8, 16, 20, 28], [4, 7, 19, 28], [4, 11, 19], [2, 5, 8, 16], [10, 17, 20, 30], [18, 21, 30], [18, 21, 30, 33], [21, 30, 33, 36], [10, 30, 33], [4, 7, 28, 31], [19, 28, 31, 35], [19, 28, 31, 35], [4, 28, 31, 35], [16, 19, 28, 31], [4, 8, 28, 43], [8, 19, 28, 35], [19, 28, 35], [4, 7, 19], [4, 11, 18], [2, 5, 18, 27], [15, 27, 30, 37], [27, 30, 34, 37], [1, 10, 27, 30], [1, 10], [33], [4, 7, 16], [1, 8, 16], [7, 16], [4, 11, 16], [16, 19, 43], [0, 7, 16], [4, 8, 23], [4, 40], [3, 7, 11], [0, 23, 43], [6], [4, 8, 11, 19], [4, 8], [4, 6, 11, 40], [4, 8, 11], [4, 9], [0, 10, 19, 36], [9], [1, 21, 33], [10, 16, 21], [21], [2, 7, 16, 31], [7, 16, 19, 31], [16, 19, 31], [4, 7, 11, 31], [7, 19, 31], [7, 31], [4, 11, 18, 30], [3, 8, 18, 30], [1, 6, 30], [4, 11, 18, 30], [9, 22], [10, 19, 31, 33], [1, 4, 21], [2, 33, 40], [1, 10, 28, 33], [10, 22], [4, 7, 19], [19], [8, 16, 19], [7, 11, 19, 28], [0, 4, 19], [7, 19], [4, 7, 19, 30], [8, 18, 30], [0, 3, 6, 8], [4, 8, 18, 30], [9, 19, 38], [0, 10, 16], [10, 16, 28], [16, 24, 28], [1, 10, 16, 28], [16, 28, 35, 36], [4, 7, 12, 16], [11, 16, 23, 28], [8, 16, 23, 28], [4, 7, 16, 28], [0, 4, 16, 23], [4, 11, 16, 23], [4, 7, 11, 16], [8, 11, 16], [3, 7, 11, 16], [4, 8, 11, 16], [4, 11, 16], [0, 10, 16, 33], [0, 10, 16], [9, 16], [10, 16], [10, 16, 36], [7, 11, 16], [4, 8, 11], [8], [4, 6, 11, 40], [0, 4, 8], [7, 23], [4, 7, 33], [8, 16, 23, 35], [3, 6, 35], [4, 23, 35], [0, 4, 11, 35], [0, 10, 21, 33], [16, 21, 43], [9, 21, 43], [10, 21, 43], [4, 21, 23, 43], [4, 7, 19], [11, 19, 28, 31], [8, 19, 23, 31], [7, 11, 31], [0, 8, 23], [8, 19, 31], [4, 7, 11, 18], [4, 8, 18], [3, 7, 18, 30], [4, 7, 11, 18], [4, 19, 21], [0, 3, 10, 33], [2, 31, 35, 36], [1, 31, 36, 40], [0, 31, 36, 40], [31, 35, 36], [4, 7, 35, 43], [4, 30, 35], [8, 23, 30, 35], [7, 28, 35, 40], [0], [19], [4, 8, 11], [8, 16, 31], [0, 3, 19, 31], [4, 8, 11, 31], [4, 7, 31], [3, 10, 18], [4, 9, 18, 30], [2, 10, 18, 30], [0, 10, 18, 30], [4, 33], [2, 7, 12, 16], [11, 16], [4, 8, 16, 28], [7, 11, 16, 28], [1, 11, 16, 28], [8, 16, 19], [7, 11, 16, 28], [8, 16, 23, 28], [4, 7, 11, 16], [4, 8, 11, 16], [4, 11, 16, 28], [10, 16], [0, 9, 16, 28], [11, 16, 28], [0, 10, 16], [10, 16], [3, 6, 10, 28], [16], [1, 40], [0, 3, 6, 10], [0, 3, 6, 10], [7, 9], [4, 11, 23, 31], [4, 8, 28, 31], [4, 7, 19, 31], [4, 19, 28, 31], [8, 19, 28, 31], [4, 7, 28, 31], [4, 8, 28, 31], [4, 19, 28, 31], [11, 19, 28, 35], [0, 4, 9], [2, 5, 16, 19], [21, 36, 37], [6, 21, 37], [3, 6, 10, 37], [6, 9, 11, 37], [5, 8], [4, 7], [4, 8, 11, 31], [7, 16], [4, 11, 19], [4, 11, 43], [1, 7, 16], [1, 16, 19], [4, 7, 10, 28], [4, 10, 19], [10, 19], [4, 10, 27, 30], [4, 8, 30, 33], [2, 8, 33, 42], [2, 18], [4, 11, 33], [8, 16, 33], [4, 16, 28], [4, 11, 16], [2, 7, 16, 23], [4, 16, 23, 28], [4, 11, 16, 23], [2, 7, 11, 16], [], [], [40], [], [], [], [40], [6], [], [26, 38], [26, 38], [], [23], [], [], [4, 19], [9, 19], [19], [21], [21], [], [], [18], [19], [19], [19], [19], [], [23], [11, 23], [11, 23], [], [7, 16], [7, 16], [16], [16], [], [], [19], [6, 21, 40], [21, 40], [40], [19], [19], [], [21], [21, 40], [7, 23], [7, 40], [7, 40], [30], [31], [30], [31], [], [6, 27], [27], [27], [30], [30], [40], [40], [40], [4, 7, 23], [4, 7, 23], [4, 7, 23], [23], [], [], [], [], [6, 11], [11, 30], [30], [30], [30], [30], [40], [30], [4, 9], [9], [9], [9, 23], [9, 23], [9], [9], [26], [4, 9], [26], [26, 38], [], [24], [23], [23], [23, 40], [4, 7], [7, 23], [7, 23], [19], [19], [], [], [], [6], [21], [40], [19], [19], [19], [30, 37], [], [4, 7, 23], [4, 23], [23], [23], [23], [23], [23], [23], [6], [27], [27], [40], [], [30], [30], [], [0, 6], [], [], [32], [32], [], [], [], [8], [8], [8], [8, 40], [28], [32], [32], [32], [6, 23], [23], [23], [23, 40], [28, 40], [], [30], [], [], [], [], [6], [30], [32], [32], [32], [37], [], [], [], [30], [], [], [], [4, 8, 32], [8, 32], [32], [], [], [40], [40], [40], [6, 23], [9, 23], [23], [23], [23], [23, 25], [23, 25], [], [5, 23], [23], [23, 25], [25], [23, 25], [], [23, 30], [30], [6], [30], [], [25], [25], [25], [32], [32], [8, 32], [8, 32], [], [25], [25], [25], [], [25], [6, 11], [], [25], [25], [25], [], [35], [35], [6, 35], [35], [], [25], [25], [], [25], [37], [6], [], [], [], [37], [], [35], [35], [4, 8], [4], [35], [], [37], [37], [38], [11, 38], [6, 21], [6], [37], [37], [37], [37], [37], [], [1, 5, 35], [1, 35], [1, 35], [1, 35], [8, 35], [1], [5, 35], [8, 13, 16], [0, 20, 29, 36], [10, 30, 37], [18, 21, 30, 33], [21, 30, 33, 37], [6, 9, 30], [0, 21, 23], [3, 8, 11, 23], [11, 20], [3, 6, 9, 23], [9, 23, 24], [4, 9, 25], [4, 9, 13, 25], [4, 9, 25], [1, 8, 25, 33], [2, 8, 21, 25], [13, 21, 25, 33], [6, 25, 33], [14, 26, 35, 38], [14, 26, 35, 38], [6, 14, 26, 35], [6, 14, 35], [0, 9, 21, 26], [9, 21, 25], [2, 13, 21, 25], [4, 8], [1, 8, 11, 23], [8, 20, 24], [8, 20, 23, 32], [0, 4, 8, 20], [2, 8, 11, 20], [5, 11, 20], [4, 11], [9, 19, 24, 31], [9, 26, 33, 40], [9, 25, 33, 40], [9, 25, 33, 40], [9, 26, 33, 40], [21, 26, 33, 40], [25, 33, 40], [9, 13, 25], [4, 20, 23], [3, 11, 20], [11, 18, 21, 37], [18, 21, 30, 33], [6, 11, 18, 21], [4, 8, 17], [4, 11, 17, 29], [4, 8, 11], [6, 29, 32, 36], [6, 18, 30, 33], [6, 21, 30, 33], [6, 21, 30, 33], [6, 21, 30, 33], [0, 9, 30, 33], [9, 21, 30, 33], [21, 30], [6, 8, 17, 33], [3, 17, 29, 32], [17, 20, 29], [8, 11, 17, 29], [8, 11, 17, 29], [2, 8, 17, 29], [3, 8], [5, 8, 29], [4, 9, 20, 32], [4, 30, 33, 37], [4, 9, 30], [4, 9, 17, 29], [9, 17, 20, 29], [4, 9, 30], [4, 21, 30, 33], [4, 8, 18, 21], [6, 11, 21, 32], [6, 11, 20, 32], [6, 11, 20, 32], [6, 11, 32], [11, 18, 21, 30], [1, 5, 18, 21], [18, 21], [9, 19, 24, 36], [1, 8, 32, 35], [1, 8, 32, 35], [1, 8, 22, 35], [8, 18, 30, 37], [8, 30, 33, 37], [8, 23, 32, 36], [20, 23, 32, 35], [1, 11, 20, 24], [4, 8], [1, 8, 25], [1, 8, 25], [1, 9, 25], [0, 25, 33], [1, 8], [0, 8, 25], [13, 21, 22, 26], [9, 25, 31, 33], [9, 14, 25], [9, 26, 35, 42], [9, 14, 26, 35], [9, 14, 26, 35], [9, 14, 23, 26], [9, 23, 26, 33], [14, 20, 23], [2, 6, 8, 32], [8, 25, 32, 37], [1, 8, 32, 37], [8, 25, 32, 37], [8, 25, 32, 37], [8, 20, 32, 37], [0, 32, 37, 39], [25, 32, 37, 39], [5, 9, 32, 37], [20, 25, 32, 37], [5, 9, 32, 37], [5, 9, 25, 32], [9, 20, 25, 32], [2, 9, 25, 32], [9, 25, 32], [2, 9, 25, 32], [5, 20, 26, 32], [20, 25, 32], [17, 25, 32], [17, 20, 25, 32], [0, 5], [0, 5, 9], [5, 9, 11], [5], [2, 5, 9, 11], [5, 9], [5, 9], [5, 9], [2, 5, 9, 11], [2, 5, 9, 11], [0, 9], [2, 5, 9], [4], [5], [5, 11], [5, 11], [5], [5, 11], [5, 11], [5, 11], [10, 27, 30], [10, 18, 30, 37], [1, 10, 18, 30], [1, 25, 30, 37], [0, 10], [0, 4, 20, 25], [3, 8, 11, 20], [8, 11, 20, 23], [4, 6, 9, 20], [4, 9, 25], [9, 21], [9, 13, 25], [4, 9, 25, 30], [8, 21, 25, 30], [1, 8, 21, 25], [8, 21, 25, 37], [3, 11, 33, 40], [6, 11, 35, 39], [14, 23, 35, 38], [6, 23, 35, 38], [6, 11, 14], [6, 11, 22, 27], [6, 11, 21], [6, 11, 19, 25], [4, 6, 11, 20], [1, 8, 11], [1, 8, 11, 35], [8, 23], [8, 11, 23, 35], [8, 11, 23, 32], [11], [3, 11], [4, 9, 20, 27], [9, 25], [9, 25, 37], [9, 37], [9, 37], [9, 25, 33, 37], [2, 9, 25, 33], [1, 9], [6, 20, 23, 25], [4, 11, 18, 20], [4, 11, 18], [4, 11, 18, 33], [2, 18, 21, 33], [3, 17, 20, 29], [11, 20, 29, 32], [6, 11, 29, 32], [6, 17, 29, 32], [6, 21, 31, 38], [6, 30, 33, 40], [6, 21, 33, 40], [6, 29, 33, 40], [2, 6, 33, 40], [10, 21, 30, 40], [10, 17, 32, 40], [3, 8, 17, 29], [8, 17, 29, 32], [8, 17, 20, 29], [8, 11, 20, 32], [6, 8, 11, 32], [2, 9], [2, 9], [3, 8], [4, 9, 31], [4, 30, 33, 40], [4, 9], [9, 17, 29, 32], [9, 17, 20], [2, 9, 30, 33], [0, 8, 30, 33], [2, 8, 18, 33], [4, 11, 30], [11, 20, 32, 35], [11, 20, 32, 35], [11, 35], [6, 11, 21], [18, 21, 33], [18, 21], [3, 9, 11], [0, 23, 35, 42], [1, 8, 11, 35], [1, 8, 11, 18], [8, 25, 30, 37], [8, 18, 30, 33], [0, 8, 20], [8, 26, 32, 38], [8, 28, 31, 38], [0, 4, 8, 20], [1, 8, 25], [8, 25], [8, 25], [8, 22, 25], [8, 21, 25], [21, 25], [4, 25, 26, 29], [2, 9, 28], [2, 9, 23], [9, 14, 23, 35], [9, 14, 23, 38], [9, 23, 26, 35], [9, 14, 23, 35], [9, 23, 26, 38], [9, 14, 20, 26], [4, 8, 20, 26], [1, 8, 25, 32], [8, 32, 37], [8, 37], [8, 32, 37], [5, 8, 32], [0, 20, 32, 37], [20, 25, 32], [5, 9, 32, 37], [5, 20, 32, 39], [2, 32, 37, 39], [2, 20, 25, 32], [4, 8, 25, 32], [2, 25, 37, 39], [6, 20, 32, 37], [20, 32, 37], [3, 13, 20, 32], [5, 13, 37], [5, 37], [25], [], [], [], [11], [5, 9, 25], [2, 5], [0, 4, 11], [1, 4, 11], [1], [1], [0], [3], [4, 13], [], [5], [1], [13], [1], [1, 5], [13], [2, 6, 13], [6, 25, 32], [25, 32], [1, 25, 33], [1, 21, 25, 33], [6, 13, 21, 33], [33], [13, 21, 33], [9, 13, 32], [9, 21, 33], [], [6], [], [32], [13, 20, 39], [], [9], [9, 16], [18, 21, 30], [30], [18, 21, 30], [21, 30], [29], [], [], [], [], [9], [9], [18, 30, 33], [18, 21, 30, 33], [18, 30, 33, 40], [19, 22], [20, 22], [20, 23], [6, 20, 23], [20, 23], [20, 23], [20, 23], [20, 23], [20], [20, 21], [20, 21], [20, 21], [21], [21], [21], [21], [20, 29], [17, 20, 29, 36], [29, 32], [17, 20, 29], [33], [33], [29], [29], [], [], [], [], [], [], [], [], [23], [20, 32, 37], [21, 24, 33], [6, 21, 33], [25, 33], [21, 33], [21, 33], [33], [33], [20, 32, 35], [23, 32, 35], [20, 32], [], [21], [6, 18, 21], [18, 21, 33], [16], [16, 20], [15, 32], [4, 16, 32], [4, 16], [16], [], [16], [], [20], [21, 33], [8, 21, 33], [8, 21, 33], [11, 21, 33], [11, 21, 33, 40], [12], [6, 20], [13, 25], [21, 25, 32, 33], [6, 25, 32, 33], [25, 33], [25, 32, 40], [9, 25, 32, 33], [32, 33], [13, 39], [13, 33], [13, 21, 33], [13, 39], [21, 25, 32], [6, 21, 26, 33], [14, 26], [], [8], [8, 20, 25, 32], [12, 20], [32], [33], [33], [], [], [], [], [], [], [], [], [], [], [9, 21], [9, 21, 24], [21, 25], [21, 24], [21, 24], [21], [9, 25], [9, 19, 21], [18, 21, 33], [18, 21, 30, 33], [18, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18, 33], [23], [19, 23], [20, 23], [21, 23], [6, 20], [37], [21, 25, 33, 37], [33], [11], [21, 24], [23, 26], [23, 26], [26], [26], [21, 25], [21, 25], [21, 25, 33], [21, 33, 37, 40], [21, 33, 37, 40], [25, 33, 40], [33, 40], [25, 33], [8, 20, 26], [8, 21, 26, 33], [8, 21, 25, 33], [8, 13, 26], [8, 13], [13, 20], [8, 20, 23], [8, 20, 23, 35], [8, 20, 23], [8, 23], [], [21, 30, 33], [18, 21, 33], [32, 33], [6, 32], [], [], [], [], [18, 21], [18, 21, 30, 33], [18, 21, 30, 33], [18, 21, 30, 33], [18], [6, 18, 30], [20, 32, 35], [20, 23, 32, 35], [4, 20], [20, 23], [20, 23, 35], [20, 23], [23, 35], [8, 23], [8, 23], [8, 20], [8, 21, 23], [20, 32, 35], [20, 32], [8, 17], [8, 17], [17, 29], [17, 20, 29, 32], [], [1], [13, 17, 20, 32], [13, 20, 29, 32], [20, 29, 32, 36], [20, 29, 32], [20, 29, 32, 36], [17, 20, 29, 32], [29, 32], [16, 20], [13, 17, 32], [29], [16, 32], [17, 32], [17], [16], [], [], [], [], [], [], [], [], [41], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [0, 3, 7, 11], [7, 11], [7, 11], [4, 7, 11], [4, 7, 11], [4, 7, 10], [7, 11], [7], [7, 11], [], [11], [7, 11], [11], [7, 11], [6, 11], [7, 16], [7, 16, 28], [7, 16], [16], [16], [7, 16, 28], [7, 16, 28], [16], [7, 16, 28, 35], [7, 16, 28], [16, 28], [], [7], [7], [7, 16], [7, 16, 28], [7, 16], [7, 9, 16], [7, 16, 18], [9, 18], [4, 9, 18], [9, 18], [9, 18], [9, 18], [18], [7, 18], [7, 18, 30], [7, 18], [7, 18], [7, 18], [7, 18], [7, 18], [], [6, 14], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [6, 9, 15], [6, 9, 15], [15], [15], [15], [9], [9], [9], [3], [], [10, 19], [11, 31, 38], [11, 19, 31], [19, 31, 35, 38], [19, 31, 35, 38], [11, 19, 31], [11, 31], [], [9, 19, 30, 31], [9, 18, 30], [9, 18, 28, 30], [9, 18, 30], [], [7, 15], [7, 16], [7, 12, 16], [6, 14], [2, 6, 14, 26], [6, 14, 26], [6, 14, 26], [6, 14, 26], [14, 26], [6, 14, 26], [6, 14], [6, 14], [], [], [9], [], [6, 10, 12], [6, 12], [11], [6, 11], [4, 7, 11], [4, 7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 9, 11], [6, 11], [6, 11], [6, 11], [6, 11], [6, 11], [], [], [], [], [7, 12, 21, 23], [7, 14, 23], [14, 23], [14, 23], [23], [23], [11], [11, 19], [11, 19], [11, 19, 31], [11, 19], [11, 19, 31], [11, 19, 31], [11, 19], [11, 19], [], [11, 19, 21], [12, 21], [21], [21], [], [23, 26], [23], [], [23, 24], [16, 24, 28, 36], [16, 24, 28], [16, 24, 28, 36], [36], [14, 23], [23], [23], [23, 26], [23], [23, 35, 42], [6, 23, 35], [23], [23], [23, 35], [23], [23], [23, 35], [11, 21], [12, 21], [12, 21], [12, 21], [6, 12, 21], [21], [11, 19], [11, 19], [11, 19], [7, 11, 19], [11], [], [], [11], [11, 19, 31], [11, 19, 30, 31], [11, 19, 31, 35], [11, 19, 31, 35], [11, 19, 31, 38], [11, 21, 33, 43], [12, 21, 31, 33], [12, 21, 33], [2, 12, 21], [12, 21], [12, 21, 33], [6, 12, 21, 33], [12, 21, 33], [12, 21, 33], [12, 21], [12, 21], [21, 24, 33], [31, 33], [31, 33], [33], [], [6], [30, 37], [30], [11], [11, 18, 30], [11, 18, 30], [11, 30, 37], [11, 18, 30, 37], [18, 30], [11, 18, 30, 37], [11, 30, 37], [11, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [11, 18, 30, 37], [5, 10, 30, 37], [3], [], [], [2, 5], [], [], [], [10], [10], [3], [3, 10], [3, 10], [3, 10], [10], [3, 10], [2, 9], [3, 10], [3, 10], [3, 10], [3, 10], [3, 5, 10], [3, 10], [3, 10], [3, 10], [3, 10], [3, 6, 10], [2, 10], [5, 10], [2, 5, 10], [3, 10], [3, 5, 10], [4, 30], [4, 35], [4, 19, 35], [23, 31, 35, 38], [3, 6, 35, 38], [4, 8, 23, 35], [4, 23, 31, 35], [4, 23, 35], [2, 23, 31, 35], [2, 7, 23, 31], [1, 4], [1, 18, 21], [1, 6, 21], [1, 10], [1, 6, 18], [1, 5, 10, 21], [1, 6, 18, 22], [1, 6, 10, 21], [1, 6, 10], [1, 6, 10], [2, 10, 12], [4, 8, 34], [4, 8, 35], [4, 8, 35], [0, 8, 35], [1, 23, 35, 38], [4, 8, 35], [4, 8, 35], [4, 8, 35], [2, 40], [2, 33, 40], [2, 4, 18], [0, 6, 21], [1, 10, 18, 21], [1, 10, 18, 21], [2, 21], [1, 6, 10], [1, 6, 9], [1, 10], [1, 5, 10], [2, 6, 10], [1, 6], [0, 6, 21], [4, 18, 30, 34], [4, 35, 38], [23, 35, 38], [23, 35, 38], [4, 8, 35, 38], [4, 8, 35], [4, 8, 35], [23, 31, 35, 38], [2, 7, 23, 35], [3, 23], [0, 18, 21], [1, 6, 10, 21], [1, 10, 18, 21], [18, 20], [18, 21], [0, 3, 6, 21], [0, 7, 21], [0, 3, 7, 21], [3, 6], [2], [3, 6], [4, 26, 29, 33], [4, 8, 19], [16, 19, 29, 31], [4, 8, 19, 31], [4, 8, 11], [4, 8, 21], [4, 8, 21, 33], [4, 21, 30, 33], [2, 7], [2, 4, 19], [1, 4], [1, 6, 11, 18], [1, 6, 15, 18], [1, 10, 15, 18], [0, 6, 18], [1, 5, 10, 18], [5, 10, 18, 23], [1, 10, 15, 18], [0, 6, 9, 18], [2, 7], [4, 7], [0, 3, 8], [4, 8, 11], [4, 8], [0, 4, 8], [0, 4, 9], [4], [4, 8], [4, 8], [4, 8, 11], [0, 4, 11], [1, 4, 8, 16], [3, 8, 43], [4, 8, 31, 43], [4, 8, 31, 43], [4, 7, 31], [2, 6, 28, 31], [2, 16, 31], [2, 6, 11], [2, 7, 28], [0, 6, 23], [6, 23], [2, 11, 23], [0, 11, 23, 38], [0, 7, 11, 38], [23, 31, 38], [2, 6, 12, 38], [0, 4, 7, 12], [0, 7, 12, 38], [0, 38], [1, 12, 38], [2, 7, 12, 38], [2, 7, 12, 38], [2, 5, 12, 38], [0, 7], [0, 43], [1, 24], [0, 7, 12], [0, 7, 12], [0, 7, 12], [0, 3, 12], [0, 3, 9, 12], [2, 9, 11, 43], [2, 9, 11, 43], [4], [4, 9, 11, 21], [4, 42], [0, 4, 23], [4, 11], [4, 11], [4, 11, 40], [4, 11, 40], [3, 6, 11], [11, 16, 32, 42], [0], [0, 3, 8, 31], [4, 8, 31], [4, 8, 31], [1, 4, 31, 43], [0, 6], [0, 6, 16, 32], [6, 40], [1, 7], [4, 6], [4, 7], [4, 9, 11], [2, 9, 11, 38], [9, 11, 26, 38], [1, 9, 38], [0, 3, 6, 33], [2, 5, 9], [2, 9, 33], [2, 40], [2, 9, 38, 43], [4, 9, 38, 43], [4, 9, 38, 43], [2, 5, 19], [9, 19, 38], [1, 9, 14], [4, 14], [2, 9, 19, 31], [9, 14, 31], [2, 9, 14, 19], [1, 38], [0, 14, 21, 33], [0, 7, 21, 43], [12, 21], [6, 12], [6, 12, 21], [0, 11, 23], [1, 4, 11], [3, 6, 11], [1, 23, 30], [2, 11], [0, 6, 11], [0, 11], [0, 23], [4, 6, 11, 23], [2, 6, 11], [6, 12, 27], [14, 27, 39], [0, 15, 27], [5, 15, 27, 39], [6, 15, 39], [6, 16, 39, 42], [0, 3, 42], [1, 16, 42], [2, 16], [1, 6, 11], [6, 11], [30], [6, 16], [6, 11, 16], [2, 6, 11, 16], [0, 15, 35], [2, 5, 15], [1, 10, 15], [1, 15], [15, 30, 35, 37], [0, 6, 30, 37], [6, 11, 37], [16], [16], [5, 16], [6, 16], [6, 15, 35], [2, 5, 35], [15, 34], [0, 3, 5], [2, 11, 16], [4, 16], [4, 8, 16], [4, 8, 40], [4, 8, 40], [4, 8, 40], [4, 11, 40], [4, 11], [0, 3, 9, 16], [4, 8, 16], [4, 8, 16], [1, 4, 16], [4, 8, 11, 16], [4, 8, 16], [4, 8], [4, 8, 11, 40], [2, 7, 16], [2, 7, 16], [2, 7, 16], [6, 16, 35], [6, 16, 36], [6, 16, 36], [0, 7, 16, 38], [0, 7, 16], [0, 7, 16, 38], [0, 7, 22, 38], [0, 7, 23, 38], [0, 7, 23, 38], [0, 7, 38], [0, 7, 23, 36], [0, 3, 7, 36], [0, 7, 23], [0, 4, 7, 43], [0, 7, 23, 43], [0, 7, 21, 33], [0, 7, 21, 31], [0, 7, 21, 31], [0, 7, 43], [0, 7, 33], [0, 7, 23, 33], [2, 5, 9, 23], [2, 9], [2, 5, 9, 23], [0, 4], [4], [4, 11], [4, 11], [4, 11, 31], [4, 11, 43], [4, 11, 31], [4, 11, 43], [0, 3, 6], [6, 11, 24, 42], [0, 4], [4, 8, 40], [4, 8, 40], [4, 40], [4, 8, 24, 40], [4, 8, 24, 40], [0, 6, 23], [6, 23], [23], [4, 21, 23], [4, 21], [5, 23, 31, 38], [2, 9, 19, 31], [2, 9, 31, 38], [0, 9, 30, 38], [0, 3, 9, 38], [2, 9, 16, 38], [2, 9, 38], [0, 9, 16, 38], [2, 9, 40], [2, 9, 16, 21], [0, 8, 16], [2, 5, 9, 16], [2, 9, 16, 18], [2, 9, 16, 38], [2, 9, 16, 31], [5, 15, 31, 38], [2, 9, 19], [2, 9], [1, 9], [0, 4, 12, 21], [2, 7, 12, 21], [1, 7, 11], [6, 11], [6, 12, 23], [12, 42], [12, 23], [6, 11, 23], [6, 12, 23], [11, 18], [0, 6, 11, 18], [3, 5, 11], [11, 23], [0, 6, 23, 39], [6, 27, 39], [0, 6, 27, 39], [2, 6, 11, 23], [6, 11, 23], [6, 11], [1, 6, 27, 39], [1, 9, 11, 39], [9, 21, 39], [6, 11], [2, 6, 11], [0, 6, 11], [0, 6, 11], [6, 11], [0, 6, 23, 35], [0, 6, 11], [2, 6, 11, 35], [6, 11, 35], [0, 6, 11], [2, 9, 35], [6, 11], [6, 11], [3, 12], [14], [2, 15], [15], [4, 6, 15], [6, 11, 16], [3, 6, 11, 16], [6, 11, 16], [2], [2, 6, 35], [0, 8, 18, 30], [4, 8, 30, 34], [4, 8, 31, 33], [4, 8, 35, 38], [4, 19, 35, 38], [4, 23, 35, 38], [4, 23, 31, 35], [4, 8, 23, 35], [4, 35, 38, 42], [2, 19, 23, 31], [2, 7, 11, 19], [1, 21], [1, 6, 18, 21], [1, 6, 10, 21], [1, 6, 10, 21], [1, 6, 10, 21], [1, 6, 14], [1, 6, 10], [1, 10], [0, 3, 7], [2, 7], [0, 7, 16, 20], [4, 12, 18, 22], [4, 8, 19, 23], [4, 19, 23], [2, 19, 23], [4, 8, 19, 23], [4, 8, 14, 23], [4, 8, 19, 23], [2, 19, 21, 23], [2, 4, 7, 21], [2, 18, 21], [1, 6], [1, 10, 18, 22], [1, 10, 18], [1, 6, 18, 21], [1, 6, 21], [1, 6, 10], [1, 6, 10], [1, 10], [0, 3, 6, 9], [0, 7], [3, 7], [0, 4, 29, 34], [4, 8, 36], [4, 23, 31, 35], [4, 8, 23, 35], [23, 31, 35, 38], [4, 8, 35, 38], [4, 19, 23, 35], [1, 4, 31, 35], [1, 23, 31, 35], [2, 19, 23, 31], [2, 7], [0, 6, 21], [1, 10, 18, 20], [1, 10, 18, 21], [1, 18, 21], [1, 6, 10, 21], [6, 10, 21], [0, 3, 10], [1, 6, 10], [0, 7], [2, 7], [0, 7, 18, 30], [4, 8, 19, 31], [4, 8, 19, 28], [1, 4, 19, 31], [0, 4, 8], [0, 4, 8], [4, 29, 31, 32], [4, 8, 30, 33], [1, 7, 33, 40], [2, 7, 11, 19], [2, 7, 28], [0, 6, 15, 18], [1, 11, 15, 18], [0, 10, 15, 18], [1, 18], [1, 6, 15, 18], [1, 6, 15, 18], [1, 6, 15, 18], [1, 6, 10, 15], [0, 3, 7], [1, 7], [7], [7], [7, 35], [4, 7, 11, 35], [4, 7, 11], [4, 11], [4], [4], [4], [4], [4, 30], [], [6], [6], [6, 11], [11], [6, 11], [11], [11], [], [40], [28, 40], [4], [9], [4, 9], [4, 9], [4, 9], [4, 9], [4, 9], [9], [9], [9, 16], [9], [9], [9], [9], [9, 31, 43], [30], [9, 19], [9, 19, 23], [9, 19, 23], [7], [19, 38], [4, 6], [7], [0, 7], [0, 7], [7], [7], [7, 12], [0, 7], [0, 7], [0], [0], [2, 5, 16], [2, 6, 9, 16], [2, 9, 16], [2, 9], [2, 9], [2, 9, 14], [2, 9, 23], [2, 9, 23], [9, 14, 23], [23], [40], [7, 11, 16, 23], [4, 7, 11], [4, 23], [4, 11], [4], [4, 23], [4, 23], [4], [4], [2, 16], [6, 16], [6, 11, 15], [6, 11, 15], [6, 11, 15], [6, 11, 16], [6, 16], [6, 11], [6, 11], [6, 11], [6, 11, 21], [4, 11, 21], [4, 7], [4, 7, 19], [4, 19, 38], [4, 7], [2, 7, 19], [19], [11, 40], [7, 40], [14, 21], [9], [6], [11, 23], [6, 11, 23], [11, 23], [40], [1, 6, 10, 40], [1, 10, 30], [1, 6, 10, 30], [6, 10], [31, 43], [43], [9, 31], [9, 31], [4, 9], [9], [9], [4, 9], [4, 7], [4, 9], [4], [4, 8], [4, 8], [9, 16], [16], [9, 16], [4, 7], [4, 9], [4, 9], [9, 35], [35], [19], [6, 19], [1, 7, 19], [0, 7], [7, 16], [7, 16], [0, 7], [2, 7, 38], [2, 38], [7, 12, 38], [2, 7, 38], [4, 7, 38], [4], [2, 6], [2, 6, 9, 38], [2, 9, 38], [2, 38], [2, 6, 9, 38], [9, 37], [6, 9, 14], [2, 4, 9, 14], [2, 6, 9, 35], [2, 9, 35], [2, 4, 35], [4, 7, 16], [4, 7, 35], [4, 7, 11], [3], [4, 7, 11], [4, 11], [4, 11], [4, 7], [4], [4], [4], [6, 11], [6, 11], [6, 11], [6], [11, 15], [6, 11], [6, 11], [6, 11], [6, 11, 15], [6, 11, 15, 35], [11, 34], [4, 8, 11, 33], [4, 8, 35], [0, 4], [0, 4, 7, 35], [23, 31, 35, 38], [4, 8, 23, 35], [4, 8, 23, 35], [2, 7, 23, 35], [2, 7, 23, 35], [2, 19, 22, 31], [1, 6, 10, 18], [1, 6, 10, 21], [1, 10, 22], [3, 10, 21], [3, 6, 10, 21], [1, 6, 10, 21], [1, 6, 9], [1, 6, 10], [0, 4, 7], [0, 3, 7], [0, 4, 21, 33], [4, 8, 21, 34], [4, 8, 35], [4, 8, 23, 35], [2, 8, 23, 35], [4, 8, 18], [4, 8, 24], [4, 8, 36, 43], [4, 24, 33, 36], [2, 4, 23, 35], [2, 7, 11, 23], [2, 6, 9], [1, 6, 18, 21], [1, 10, 18, 21], [1, 6, 10, 21], [2, 4, 9, 21], [1, 6, 10, 21], [1, 10], [1, 6, 10], [0, 3, 5, 9], [0, 7], [0, 6, 12], [4, 8, 20, 22], [4, 7, 42], [4, 8, 19, 23], [0, 19, 23, 35], [0, 4, 8, 23], [4, 8, 23], [4, 7, 23], [4, 23], [4, 7, 19, 23], [2, 7, 19, 23], [2, 5], [1, 6, 10, 33], [1, 6, 33, 40], [1, 10, 33, 39], [0, 21, 32, 39], [1, 10, 33], [1, 10, 21, 33], [0, 6, 10, 21], [1, 6, 10], [0, 4, 7], [0, 3, 7], [4, 8, 14, 19], [4, 8, 18, 29], [4, 19, 28, 31], [19, 28, 31], [0, 4, 8], [4, 14, 18], [4, 19, 31, 33], [4, 21, 30, 33], [1, 4, 7, 30], [2, 7, 11, 19], [2], [0, 6, 9, 18], [1, 6, 15, 18], [2, 10, 18], [0, 6, 18], [0, 3, 6, 10], [1, 4, 10, 18], [1, 4, 10, 18], [0, 5, 9, 18], [0, 3, 7], [0, 7], [2, 7], [4, 39], [4, 8, 11, 40], [4, 8, 40], [4, 8, 40], [4, 8, 11, 40], [4], [4, 11], [2, 40], [2, 7, 43], [2, 7, 40], [3, 7, 40], [0, 7, 40], [0, 7, 40], [0, 7, 41], [4, 7, 40, 42], [4, 6, 40, 42], [0, 7, 40, 42], [0, 7, 40, 42], [0, 4, 6, 40], [0, 4, 35, 42], [0, 7, 40], [4, 9, 35, 42], [2, 9, 35, 42], [9], [9], [4, 9, 31, 38], [2, 9, 39, 42], [2, 9], [1, 9, 31, 38], [1, 4, 9, 38], [2, 9], [0, 4], [4, 11], [4, 42], [0, 4, 11, 40], [4, 11, 40], [4, 11, 40], [4, 40], [4, 11, 40], [4, 7, 38, 40], [3, 6, 40], [4, 6, 40], [0, 4, 38, 40], [4, 8, 40], [4, 8, 40, 43], [4, 8, 38, 40], [4, 8, 40], [4, 8], [4, 8, 11, 40], [4, 8, 42], [4, 7, 40, 43], [2, 7, 40, 43], [2, 7, 40, 43], [0, 4, 7, 42], [0, 3, 7], [0, 7, 43], [0, 7, 42], [0, 4, 7, 40], [0, 7, 40], [0, 7, 40], [1, 7, 38, 40], [0, 4, 38, 43], [2, 11], [2, 7, 42], [4, 6, 9, 31], [9, 31, 35], [2, 9, 31], [9, 31, 35, 43], [2, 6, 9], [2, 9], [2, 9, 43], [2, 9, 34, 42], [4, 9], [2, 9, 38], [0, 7, 35, 38], [4, 11], [4, 19, 38], [6, 11], [6, 11, 35], [4], [4, 8, 42], [1, 4, 8, 11], [4, 8], [0, 4, 8], [4], [4, 6], [4, 8], [4, 8, 11], [4, 11, 38, 40], [4, 7], [4, 8, 11], [4, 8, 11], [4, 8, 11, 40], [4, 7, 11, 40], [2, 7, 11, 40], [2, 7], [0, 4, 7], [0, 7, 43], [7, 16, 40], [0, 7, 40], [0, 3, 7, 43], [3, 7, 28, 40], [7, 11, 40, 43], [2, 7, 28, 40], [0, 3, 7, 40], [0, 7, 40], [0, 5, 9, 38], [2, 9], [2, 9, 38], [2, 9, 40], [4, 9], [4, 9, 38], [2, 9], [2, 9, 38, 40], [4, 7, 9, 33], [4, 9, 30], [2, 9, 38], [0, 4], [4, 11, 40], [4, 7, 38, 40], [6, 11, 38, 40], [2, 5, 11, 31], [0, 4, 40], [0, 4, 8, 43], [0, 4, 40], [0, 5, 8, 34], [1, 4, 8, 38], [4, 38, 40, 43], [4, 8, 11, 38], [4, 8, 43], [4, 8, 38, 40], [4, 8, 31, 38], [4, 8, 38, 43], [2, 7, 38], [2, 7, 11, 40], [2, 7, 28], [4, 6], [6, 38, 40, 43], [1, 4, 7, 36], [0, 4, 7], [0, 7, 38, 43], [0, 7, 40, 43], [0, 7, 36], [0, 7], [0, 7, 40], [0, 7, 38, 40], [0, 5, 36], [2, 7, 11, 40], [2, 7, 38, 40], [4, 9, 38], [2, 9, 42], [2, 9, 42], [2, 9], [4, 9], [1, 9, 38], [2, 9, 38], [2, 9, 35, 38], [4, 9], [2, 9, 28], [4, 28], [4, 11, 40], [4, 11], [4, 11], [6, 11, 16, 35], [4, 11], [0, 4, 8, 35], [0, 4, 8, 38], [4, 8], [0, 4, 11], [11, 34], [0, 4, 7], [4, 8, 11], [4, 8, 11, 31], [4, 8, 31], [4, 8], [0, 4, 7, 31], [2, 7, 11], [2, 11, 40], [2, 7, 31, 42], [6, 31], [6, 11, 42], [0, 4, 7, 34], [0, 7, 16, 23], [0, 7, 16, 23], [0, 7, 23], [0, 4, 7, 23], [2, 23], [2, 7, 11, 23], [2, 7, 23], [4, 7], [0, 7, 43], [0, 7, 23, 43], [2, 9], [2, 9, 30], [2, 6, 9], [2, 9, 43], [0, 3, 9, 40], [2, 9, 38], [2, 9, 43], [2, 9, 43], [2, 9, 38, 43], [2, 9, 38], [0, 3, 5, 11], [4, 11, 31], [4, 11], [3, 11, 43], [4, 11, 33, 38], [0], [4, 8], [1, 4, 38, 43], [4, 7], [0, 4, 8], [4, 8], [4], [4, 8, 42], [4, 8], [4, 11], [4, 6, 11], [4, 38, 40], [2, 7, 28], [2, 11, 40], [4, 38, 40], [0, 6, 29, 40], [0, 38, 40], [0, 7, 40, 43], [0, 7, 42, 43], [1, 7, 43], [0, 7, 43], [0, 6, 31, 43], [2, 7, 11, 43], [2, 7, 11, 43], [2, 7, 11, 43], [0, 4, 7, 43], [0, 7, 40], [2, 4, 9, 31], [2, 9, 31, 43], [2, 9, 31], [2, 9, 31], [4, 6, 9, 43], [2, 9, 31, 35], [2, 9, 35], [2, 9, 35], [2, 5, 9, 35], [2, 9, 34], [4, 10, 33], [4, 6, 11], [4, 11], [0, 4], [33, 35], [4, 11, 31], [0, 4, 35, 43], [0, 4, 8], [4, 8], [4, 7, 31], [4, 8], [4], [4, 8, 11], [4, 8], [0, 4, 8, 30], [4, 8, 11], [4, 11, 26, 38], [1, 4, 7], [2, 7], [2, 7], [4, 7, 11, 40], [6, 38, 40], [3, 6, 38], [0, 4, 7], [0, 7, 16], [0, 7, 16, 30], [0, 4, 7, 38], [2, 9, 42], [2, 7, 11, 43], [2, 7, 11, 43], [2, 6], [0, 7], [0, 7], [2, 6, 9], [2, 9, 31], [2, 9, 38], [2, 9, 31], [5, 9, 31], [2, 9, 31], [2, 9, 42], [2, 9, 38], [4, 9, 30, 38], [0, 9, 30], [4, 43], [4, 11, 31], [4, 11, 30, 40], [0, 4, 11], [6, 11, 28, 40], [4, 11, 40], [4, 8], [4, 8, 11], [0, 4], [0, 4, 8, 23], [4, 8, 23, 38], [0, 4, 7, 38], [4, 8, 39, 40], [4, 8, 11, 40], [1, 4, 8, 40], [4, 8, 11, 40], [4, 8, 40], [2, 40], [2, 4, 7, 39], [2, 4, 11, 38], [6, 11, 38], [6, 11, 30], [0, 4, 7], [0, 3, 7], [0, 7, 43], [0, 7], [0, 4, 7, 9], [2, 7], [2, 7, 11, 40], [2, 7, 11], [0, 3, 33, 38], [0, 3, 7, 40], [1, 4, 6, 38], [2, 9, 35, 38], [2, 9, 38, 43], [0, 9, 35, 38], [9, 31, 35, 38], [2, 9, 38], [2, 9, 30], [2, 9, 38], [0, 6, 9, 43], [2, 9, 38], [4, 9, 40], [3, 6, 11], [4, 11, 40], [4, 11, 40], [0, 4, 11], [0, 3, 6, 11], [3, 28], [4, 8, 43], [4, 8, 11, 43], [0, 3, 8], [4, 8, 42], [4, 8], [4, 7, 33, 40], [4, 8, 40], [4, 8, 35, 38], [4, 8, 23, 35], [4, 35, 38, 40], [4, 8, 35, 40], [4, 8, 21, 33], [4, 19, 33, 40], [2, 4, 7, 21], [2, 11, 40], [2, 6, 40], [1, 6, 22, 34], [1, 10, 33, 40], [1, 10, 33, 40], [0, 9, 32, 39], [1, 4, 10, 40], [1, 10, 33, 40], [1, 10, 33, 40], [1, 6, 10, 40], [0, 4, 7, 40], [0, 7, 40], [4, 8, 12, 21], [4, 8, 35, 40], [4, 8, 23, 35], [14, 23, 31, 35], [1, 4, 7, 40], [0, 4, 8, 11], [4, 8, 35], [4, 24, 33, 36], [7, 24, 33, 36], [2, 11, 19, 23], [2, 7, 40], [0, 4, 18, 21], [1, 6, 10, 21], [1, 6, 10], [1, 10, 42], [1, 6, 10], [1, 10], [1, 6, 10, 42], [1, 6, 10, 42], [0, 3, 6, 43], [0, 7, 11, 43], [7, 11, 16, 20], [4, 8, 29, 33], [4, 8, 35, 40], [14, 23, 31, 35], [4, 19, 31, 35], [4, 8, 31, 35], [4, 8, 23, 35], [4, 8, 35, 40], [4, 7, 23], [2, 7, 23, 40], [2, 7, 40], [2, 5, 39], [1, 10, 30, 33], [1, 6, 10, 32], [1, 10, 39], [1, 4, 33, 39], [0, 7, 21, 33], [0, 7, 39], [0, 3, 7, 39], [0, 3, 7], [2, 6, 39], [0, 6], [4, 19, 27, 40], [4, 8, 19, 40], [4, 19, 28], [3], [0, 3, 6, 8], [4, 8, 11, 19], [4, 8, 30, 33], [4, 30, 33], [2, 4, 7, 11], [2, 7, 11, 40], [1, 5, 10], [0, 6, 10, 18], [1, 10, 15, 18], [0, 15, 18], [0, 7, 15, 18], [4, 7, 18], [0, 15, 18], [6], [2, 6, 42], [1, 6, 10], [1, 6, 10], [0, 3, 8, 23], [4, 8, 40, 41], [4, 8, 35, 42], [3, 28, 35, 42], [8, 28, 35, 42], [4, 8, 35, 42], [4, 8, 42], [4, 8, 35, 42], [4, 7, 35, 42], [2, 7, 35, 42], [2, 11, 28, 35], [0, 35, 40, 42], [2, 35, 38, 42], [0, 8, 35, 42], [0, 7, 35, 40], [0, 4, 7, 35], [0, 7, 35, 40], [0, 7, 35, 42], [31, 35, 38, 42], [2, 7, 11, 40], [2, 7, 31, 40], [2, 31, 38], [2, 9, 32, 39], [2, 9, 32], [0, 9, 33, 40], [9, 33], [2, 9, 33], [2, 9, 33], [2, 9], [2, 9], [2, 9, 33], [0, 3, 33, 40], [4, 11, 33, 40], [4, 7, 11, 31], [4, 11], [6, 32, 35, 40], [6, 31, 35], [4, 31, 35], [4, 8, 11, 43], [4, 8, 11, 43], [4, 8, 40, 42], [3, 28, 35, 40], [4, 8, 11], [0, 4, 8], [4, 8], [4, 8, 11, 33], [0, 4, 8, 35], [4, 8, 17, 35], [4, 25, 33, 42], [8, 34, 35, 41], [8, 23, 35, 42], [1, 6, 35, 43], [2, 7, 35, 42], [2, 7, 26, 33], [0, 7, 33, 43], [0, 7, 33, 40], [0, 7, 33, 38], [0, 7, 33, 38], [0, 4, 33, 38], [0, 7, 33, 38], [2, 7, 11, 38], [2, 7, 38, 40], [0, 7, 33, 40], [0, 7, 33], [2, 9, 35, 42], [2, 9, 38], [4, 6, 9, 38], [2, 33, 35, 40], [0, 9, 38], [1, 6, 9, 38], [4, 33, 38, 40], [4, 9, 33, 40], [9, 33, 40], [2, 9, 38], [3, 28, 35, 38], [4, 11, 19, 38], [4, 11, 37, 42], [4, 6, 30, 38], [0, 6, 35], [2, 6, 35], [1, 4, 33], [4, 8, 11, 31], [4, 8, 31], [4, 11, 31], [4, 11, 31, 38], [4, 19, 38, 43], [0, 4, 8, 35], [4, 8, 11], [4, 8, 11], [4, 8, 11], [0, 4, 8, 11], [1, 27, 28, 35], [2, 11, 28, 35], [2, 7, 35, 40], [6, 28, 35, 40], [6, 28, 40, 43], [0, 7, 30, 40], [0, 37, 40], [0, 7, 40], [0, 7, 39, 40], [0, 7, 40], [0, 40], [2, 7, 28, 40], [2, 7, 11, 28], [2, 7], [0, 7, 30], [0, 7, 40, 43], [2, 9, 31, 38], [2, 9, 30, 38], [2, 9, 38], [2, 9, 38], [2, 9, 38], [2, 4, 9, 38], [4, 9, 26, 38], [4, 9, 26, 38], [4, 9, 26, 38], [4, 9], [4, 33], [4, 11, 33, 35], [4, 11], [4, 6, 35], [0, 6, 11, 33], [11, 33, 35], [8, 31], [4, 8, 11, 31], [0, 4, 31, 38], [4, 11, 31], [4, 11, 31, 38], [0, 4, 7, 43], [4, 8, 31, 35], [4, 8, 31], [4, 31, 38, 42], [4, 8, 11, 31], [4, 11, 31, 38], [2, 7, 31, 38], [2, 31, 33, 38], [30, 31, 37, 40], [6, 30, 33, 37], [11, 31, 33, 40], [7, 33], [0, 7, 30, 33], [7, 33, 40], [7, 33, 40], [3, 7, 33, 40], [2, 7, 33, 43], [2, 7, 31, 33], [2, 7, 33, 40], [0, 7, 33, 36], [0, 7, 31, 36], [0, 7, 31], [2, 9, 31], [2, 9, 33], [2, 9, 33, 38], [2, 9, 33, 38], [2, 6, 31, 33], [2, 9, 18, 37], [4, 31, 36, 37], [4, 30, 37, 38], [4, 9, 30, 38], [1, 9, 30, 37], [4, 6, 35, 40], [4, 35], [4, 35], [4, 35, 42], [4, 28, 35], [4, 35], [4, 35], [4, 35], [3, 28, 35], [2, 28, 35], [6], [0, 3], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [0, 18, 30, 33], [19, 29, 31, 36], [19, 28, 31, 35], [19, 28, 31, 35], [16, 28, 31, 35], [19, 31], [28, 30], [18, 28, 30, 35], [16, 28, 30, 35], [7, 18, 28, 30], [18, 30, 33], [7, 30], [7, 18], [14, 26], [14, 19], [19, 26], [19, 26], [20], [11, 19, 26, 31], [11, 14, 19, 26], [11, 19], [12, 19, 31], [12, 19], [9, 12, 19, 26], [9, 12, 19, 26], [4, 12, 19], [12, 19], [12, 19], [12, 21], [11, 20], [12, 19, 24], [12, 19], [11, 20], [12, 18], [11, 18, 23], [11, 18], [11, 18, 23], [10, 18], [19, 23], [11, 19], [18], [9, 18], [9], [17], [19, 31, 35], [19, 31, 35, 42], [19, 35, 42], [19, 35, 38, 43], [31, 35], [19, 21, 36, 43], [33, 36, 40, 43], [21, 36, 43], [11, 22], [], [18, 30, 34], [30, 36, 43], [6, 30, 36, 43], [30, 36, 43], [30, 36], [18, 30, 36, 43], [18, 30, 36, 43], [7, 16, 30], [6, 16], [6, 16, 28], [4, 28, 35], [4, 28, 35], [28, 35], [16, 28, 35], [16, 28, 35], [16, 28, 35], [28, 35], [11, 23], [7, 18, 20, 21], [19, 21, 33, 40], [7, 19], [6, 19], [6, 21], [6, 21, 33, 37], [6, 22], [6, 22, 33, 40], [6, 21, 30, 40], [6, 21, 30], [6], [6, 9], [6, 14, 19, 22], [6, 19, 23, 33], [31, 33, 35, 38], [31, 33, 35, 42], [], [], [14], [6, 14, 16, 19], [6, 14, 16, 26], [6, 19, 26, 28], [16, 19, 26, 31], [7], [7, 24, 28, 33], [28, 30, 33, 37], [30, 33, 37, 40], [4, 28, 30, 40], [30], [14, 21], [21, 24, 28, 33], [21, 24, 28, 33], [21, 24, 28, 33], [25, 28], [7, 24], [7, 15, 19], [18, 23], [15, 19, 22, 24], [6, 15, 23, 30], [], [19, 27, 31, 33], [15, 21, 28, 33], [7, 21, 28, 33], [7, 27], [9, 19], [19, 22, 27, 31], [19, 31, 35, 40], [30, 31, 35, 40], [31, 35, 38, 40], [23, 31, 35, 40], [31, 32, 35, 40], [11, 31, 35, 40], [11, 28, 30, 35], [11], [7, 11], [11], [12, 35, 37, 41], [18, 30, 33, 42], [18, 30, 33, 38], [6, 12, 38, 42], [12, 26, 33, 42], [12, 30, 38, 42], [12, 18, 30, 42], [12, 18, 33], [12, 24, 31, 36], [12, 16, 28, 31], [12, 15], [11, 15, 18], [11, 15, 18, 27], [11, 23, 27, 34], [18, 23, 27, 34], [18, 23, 28], [18, 23, 26, 33], [18, 23, 27], [15, 18, 23, 30], [23, 26, 30, 33], [15, 23, 30], [11, 18, 23, 30], [11, 18, 23], [11, 23], [6, 11], [], [], [11, 15, 27], [11, 15], [15], [15, 23], [11], [3, 11], [3], [3], [], [], [6], [11], [11], [11], [11], [11], [11, 25], [], [], [25], [], [], [], [], [30], [3, 30], [4], [4, 7, 26, 29], [19, 28, 31], [19, 28, 31], [7, 28, 31, 35], [4, 7], [4, 21, 30, 38], [30, 35, 37, 40], [30, 35, 37, 40], [4, 7, 35, 40], [28, 30, 35, 40], [7, 18, 30, 40], [7, 30, 33, 37], [7, 16, 42], [14, 16, 28], [16, 26, 28, 35], [15, 26, 33], [11, 26, 29, 33], [26, 31, 33], [11, 19, 31, 33], [11, 19, 31, 33], [11, 31, 33], [9, 19, 24, 31], [19, 31, 33], [19, 31], [19, 26, 31], [12], [10, 12, 19], [12, 21], [12, 19, 21, 33], [12, 19], [12, 19, 31, 38], [18], [11, 18, 30, 37], [11, 18, 37], [11, 18], [11, 18, 23], [18, 23, 30, 37], [18, 23, 30, 37], [11, 18, 37], [11, 18, 30, 37], [9, 11, 30], [9, 11], [11, 19, 22, 34], [19, 23, 35], [19, 23, 31, 35], [11, 19, 31, 35], [11, 19, 23, 35], [19, 23, 31], [11, 19, 23, 35], [11, 21, 33, 36], [21, 24, 33, 36], [11, 21, 33], [11, 25], [24], [18, 24], [18, 24, 36], [6, 23], [9, 19, 23], [19, 23], [18, 23], [7, 18, 23], [6, 16, 28], [6, 16, 28], [6], [4, 17, 19, 29], [19, 23, 31, 35], [4, 19, 31, 35], [4, 19, 31, 35], [19, 31, 35], [11, 19, 31, 33], [19, 31, 33, 38], [11, 19, 33, 40], [7, 21, 33, 40], [19, 31], [6], [6, 21, 40], [6, 21, 31], [6, 21], [6, 18], [6], [6], [6], [6], [6, 12, 15], [6, 14, 19, 23], [31, 33, 36, 42], [31, 33, 35, 42], [19, 23, 26, 35], [7, 35], [7, 19], [7, 14, 16], [6, 14, 20], [6, 16, 28], [16, 19, 26], [16], [6, 21, 33], [30, 31, 33, 40], [30, 31, 33, 40], [21, 30, 33, 40], [30, 43], [24, 33], [19, 24, 27, 30], [24, 28, 33, 36], [24, 28, 33], [24, 28, 36], [19, 36], [7, 15, 18, 22], [18, 23], [23], [6, 15, 18, 23], [9, 18, 23], [20, 21, 32], [21, 33], [7, 21, 33], [7, 33], [15, 22, 33], [15, 19, 23, 33], [23, 31, 35], [35, 38, 40, 43], [11, 31, 35, 40], [23, 31, 35, 40], [23, 31, 40], [11, 31, 40], [11, 16, 28], [], [11], [11], [11, 26], [25, 33, 35, 42], [26, 30, 33, 42], [12, 26, 33, 42], [12, 26, 33, 42], [26, 30, 33, 42], [12, 26, 33, 42], [12, 26, 33, 42], [12, 24], [12, 24, 28, 35], [12, 16, 28], [12, 15, 23], [15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [11, 15, 23, 35], [15, 23, 35], [15, 23, 35], [15, 23, 35], [11, 18, 23, 35], [11, 18, 35], [15, 18, 23, 35], [11, 23, 35], [15, 23, 35], [], [21], [11, 15], [15], [11, 15], [15], [3], [3, 15], [15], [], [], [30], [2], [], [], [], [], [], [], [21], [], [], [], [1], [5, 8], [8], [5], [26, 29], [4, 6, 18, 29], [4, 18, 28], [4, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [4, 11, 19, 28], [11, 19, 27], [2, 6, 19, 26], [12, 26, 31], [0, 12, 27, 31], [12, 26, 30, 31], [7, 12, 18, 27], [0, 26, 30, 38], [12, 28, 30, 37], [12, 27], [26, 29, 36, 38], [2, 26, 30, 38], [18, 26, 30, 37], [26, 30, 37, 38], [9, 18, 26, 30], [2, 6, 18, 26], [2, 18, 26, 30], [2, 14, 26], [4, 23], [4, 22, 23], [16, 23], [4, 16, 23, 28], [4, 16, 38], [0, 4], [4, 7, 11], [10, 16], [18, 27], [20, 29], [16, 19, 28], [16, 19, 28], [4, 16, 28, 30], [16, 19, 31], [4, 16, 30], [11, 16, 28, 30], [0, 4, 18, 30], [19], [7, 16, 19, 31], [0, 7, 19, 31], [7, 16, 31], [2, 16, 18, 28], [0, 16, 18, 28], [7, 16, 27, 28], [2, 26, 29, 38], [18, 26, 30, 38], [18, 26, 30, 38], [2, 26, 30, 37], [18, 26, 30, 38], [2, 18, 26, 30], [6, 18, 26, 30], [1, 18, 26, 30], [4, 11, 26], [4, 11, 28], [4, 11, 28], [4, 28], [28], [2, 7, 28], [4, 7, 28], [4, 31], [27, 30, 37], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19, 28], [4, 16, 19, 28], [4, 27, 30, 38], [0, 7, 27], [0, 7, 27, 31], [0, 24, 26, 31], [0, 7, 18, 24], [18, 24], [18, 24], [11, 24], [16, 22, 29, 35], [9, 23, 30, 36], [23, 30, 35, 37], [18, 21, 30, 37], [18, 21, 30, 35], [0, 9, 18, 21], [9, 18, 21, 30], [9, 16, 20, 30], [4, 19, 38], [28, 32, 35, 39], [20, 32, 35, 39], [4, 19, 23, 28], [4], [4], [4, 11], [4], [4, 16, 19], [19], [4, 19], [4, 19, 23, 35], [4, 19, 23], [19, 28, 35], [4, 19, 28], [4, 16, 19, 28], [18, 19, 30], [0, 16, 19, 32], [16, 19, 30, 32], [0, 16, 19], [0, 16, 32], [16, 18, 28, 30], [0, 7, 16], [2, 16, 29], [16, 29], [9, 18, 30], [2, 9, 18, 30], [9, 30], [4, 18, 27], [18, 26], [18, 27, 30], [2, 9, 26], [4, 26, 39, 40], [28, 35], [4, 16, 28, 35], [27, 29, 35], [4, 28, 29, 35], [28, 29], [4, 28, 29], [4, 40], [4, 19, 39], [4, 31, 38, 40], [4, 28, 31, 40], [4, 31, 38, 40], [4, 28, 31], [4, 28, 31, 38], [4, 28, 31], [4, 7, 19, 40], [4, 6, 31, 39], [26, 31, 38], [26, 31, 35, 38], [19, 26, 31, 38], [25, 27, 31, 38], [27, 31, 38], [26, 31, 38], [26], [1, 25, 29, 30], [18, 27, 30], [26, 30, 37, 38], [18, 26, 30, 37], [2, 9, 26, 30], [2, 24, 30, 36], [2, 18, 30, 36], [2, 16, 23], [4, 25], [23, 35], [28, 35], [16, 35], [4, 28, 35], [4, 11, 16], [4, 7, 11], [7, 43], [16, 27, 30, 31], [16, 19, 28, 31], [16, 28, 31, 38], [28, 31], [16, 28], [16, 19, 28, 30], [16, 30], [4, 11, 16], [0, 4, 30, 31], [0, 16, 19, 31], [0, 31], [0, 16, 31], [16, 30, 31], [2, 28, 29], [16, 28, 30], [0, 16], [16, 26, 28], [2, 26, 30, 38], [9, 26, 30, 38], [2, 9, 26, 30], [2, 9, 30, 38], [2, 26, 30], [2, 6, 18, 30], [2, 6, 26, 30], [4, 11, 28, 39], [11, 19, 28, 35], [4, 19, 28, 35], [28, 35], [4, 28], [6, 19], [3, 7], [4], [19, 31, 38], [19, 28, 31, 38], [19, 28, 31, 38], [28, 31, 38, 40], [4, 19, 28, 31], [4, 19, 28, 31], [4, 19], [4, 19, 26], [0, 4, 26, 31], [27, 31, 38], [7, 19, 26, 31], [27, 31, 38], [24], [0, 43], [7, 24, 31], [19, 31], [30, 33, 34, 40], [21, 23, 30], [9, 23, 30, 35], [18, 21, 23, 30], [2, 9, 21, 33], [2, 21, 22, 42], [19, 21], [2, 6, 9], [4, 16, 20], [4, 16, 19], [4, 19], [4, 16, 31, 43], [16, 23, 28, 31], [4], [4, 16, 23, 38], [4, 7], [4, 18, 19, 23], [4, 19, 23, 31], [4, 23, 31, 35], [4, 19, 23], [4, 23], [28, 31, 38, 40], [19, 31, 38, 40], [19, 28], [16, 19, 30, 31], [19, 31, 38], [30, 31, 38], [19, 30, 38], [0, 19, 28, 29], [16, 19, 28, 30], [0, 16, 19, 28], [0, 16, 27, 30], [30, 38], [2, 30], [2, 9, 30], [9, 18, 30], [6, 18, 30], [18, 26, 30, 38], [18], [2, 16, 30], [4], [16, 28], [4, 11, 16, 28], [0, 4, 11, 29], [0, 11, 16, 29], [4, 28, 33], [4, 7, 28], [11, 16, 19], [19, 28, 31, 34], [4, 28, 31, 35], [28, 31, 35, 38], [19, 28, 31, 35], [4, 19, 28, 31], [7, 19, 31, 34], [21, 31, 33, 40], [4, 21, 30, 33], [4, 18, 21, 30], [11, 19, 23, 31], [7, 19, 23, 31], [19, 23, 31, 38], [7, 23, 31, 38], [11, 23, 31, 38], [11, 31, 38], [11, 19, 23], [23, 31, 33, 35], [12, 24, 33, 36], [12, 24, 33, 36], [12, 24, 33, 40], [9, 12, 23, 33], [23, 31, 33, 35], [31, 33, 35], [11, 19, 21], [4, 6, 18, 21], [18, 21], [18, 21], [18, 21], [18, 23], [18, 33], [9, 18], [11, 15, 21], [1, 18, 22], [19, 23], [11, 19, 23], [19, 23, 31], [19, 23, 31, 35], [21, 23, 31, 42], [19, 23, 35, 38], [19, 23, 30, 33], [3, 9, 30, 33], [30, 33], [19, 28, 31, 35], [9, 28, 31, 35], [2, 7, 9], [7, 9, 15, 18], [6, 15, 18], [6, 15, 18, 27], [19, 21, 28, 30], [19, 21, 28, 31], [19, 21, 28, 30], [19, 28, 33, 35], [19, 28, 31, 33], [28, 30], [28, 31, 33, 38], [19, 21, 28, 31], [6, 26, 31], [6, 27, 30, 43], [6, 27, 30], [6, 27, 30], [6, 27, 30], [6], [6], [6, 16], [15, 18, 19], [16, 19], [6, 15, 31], [6, 15, 18, 27], [6, 14, 16, 31], [33, 34, 38, 43], [7, 28], [7, 13, 16, 19], [4, 9, 19, 33], [9, 21, 30, 33], [19, 30, 33, 40], [9, 17, 19, 28], [19, 28, 31, 33], [28, 31, 33, 35], [7, 29], [7, 17, 19], [17, 21, 29, 33], [18, 21, 30, 33], [6], [6, 16, 19], [6], [18, 21], [6, 18, 30, 33], [6, 18], [4, 11, 18, 21], [19, 23], [11, 19, 23], [11, 19, 23], [19, 23], [4, 6, 11, 19], [19, 23, 31], [11, 19], [4, 12, 20, 22], [21, 24, 31, 33], [12, 21, 24], [12, 21, 24, 33], [12, 21, 24, 33], [12, 21, 24, 33], [12, 21, 24, 33], [12, 24], [3, 24, 31], [23, 30, 35], [23, 30, 35], [2, 23, 30, 35], [18, 23, 30, 35], [23, 30, 35], [23, 30, 35], [23, 30, 35], [18, 23, 30, 35], [18, 23, 30, 35], [23, 30, 35], [23, 30, 35], [3, 23, 30, 35], [22, 30, 35], [18, 23, 30, 35], [3, 11, 30, 35], [4, 30, 35], [18, 22, 30, 34], [22, 30, 35, 37], [23, 30, 35], [30, 35], [30, 35], [9, 30, 35], [9], [1], [10, 33], [10], [10], [10], [10, 15], [10], [10], [4, 10], [10], [3, 5, 10], [3], [6], [0, 3, 7, 10], [3, 10], [2, 7, 11], [26, 31, 33], [4, 19, 31, 41], [19, 28, 31, 35], [19, 28, 31, 35], [7, 16, 28, 35], [4, 11, 30, 33], [18, 30, 33, 37], [1, 9, 22, 30], [4, 7, 18, 33], [20, 30, 35, 42], [20, 23], [19, 23], [19, 23], [11, 19, 35, 42], [20, 23], [11], [20, 32, 35, 38], [9, 21, 33, 36], [12, 21, 33, 36], [20, 33, 36, 40], [12, 19, 24], [19, 31, 35, 40], [11, 31, 35, 38], [11, 19, 28, 31], [3, 11, 18, 22], [11, 18, 21], [18, 21], [18, 21], [21], [21], [21], [11, 18], [7, 18, 22, 30], [7, 19, 23], [23, 31, 40, 42], [18, 23, 30, 35], [19, 23, 35, 42], [19, 23, 35, 42], [11, 23, 35, 42], [18, 25, 27, 31], [4, 9, 18, 33], [18, 21, 33], [9, 28, 32, 36], [28, 31, 35, 38], [19, 28], [2, 6, 14, 35], [6, 15, 18], [6, 15, 30], [19, 25, 27, 34], [28, 31], [28, 31], [19, 28, 31, 38], [27, 31, 38], [28, 31], [28, 31, 38], [16, 28, 31, 38], [3, 7, 27, 30], [6, 27, 30, 35], [6, 27, 30, 35], [6, 30, 35], [6, 30, 35], [6], [6, 10], [6], [2, 7, 20, 26], [7, 14, 19], [7, 11, 16, 19], [6, 15, 18, 35], [6, 15, 27, 42], [7, 19, 29, 31], [7, 28, 31, 35], [7, 18], [4, 9, 32, 39], [9, 30, 33, 37], [18, 30, 33, 37], [9], [4, 6, 30], [28, 31, 35, 38], [7, 19, 31, 38], [7, 19, 21, 28], [7, 18, 30, 33], [18, 21, 33, 40], [6, 16, 20], [6, 16, 19], [6, 28], [6, 22, 30], [21], [11, 16, 18, 19], [4, 18], [20], [20, 23, 28], [11, 20, 23], [23, 28, 35], [11], [23, 28], [12, 23], [12, 28, 33, 38], [30, 33, 36, 37], [12, 30, 33, 36], [30, 33, 36, 40], [30, 33, 36, 43], [30, 36, 37, 43], [12, 36], [6, 12], [4, 11, 25], [6, 11, 15], [11, 15], [11, 18, 27, 35], [11, 15, 27, 35], [11, 15, 35], [11, 15, 35], [11, 15, 27, 35], [11, 15, 27, 35], [11, 15, 35], [15, 23, 31, 35], [15, 23, 27, 35], [3, 15, 23, 35], [4, 15, 23, 35], [11, 15, 35], [3, 11, 15, 35], [3, 6, 11, 35], [11, 14, 27, 35], [2, 11, 15, 35], [23, 35], [15, 31], [23], [], [6, 11, 21], [5, 10], [3, 10], [3, 9], [3, 10], [10], [2, 9], [2, 10], [3, 10, 35], [3, 5], [3, 10, 12], [12], [9, 12], [6, 35], [6, 42], [4, 18], [4, 18], [4, 18], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [9], [11], [11], [11], [0], [], [], [7], [6, 9], [6, 9], [6, 9], [9], [11], [11], [11], [11], [], [11], [], [], [], [], [11], [0, 12], [0], [0], [0], [1], [0, 12], [23, 30], [23, 30], [11], [7], [6, 9], [6, 9], [6, 9, 16], [6, 9], [], [9], [], [], [], [], [], [9, 16], [10], [11, 18], [18, 23], [24], [11], [11], [11, 18], [11], [11], [7, 21], [6, 9], [6, 9], [6, 9], [7], [7], [6, 16], [6], [6], [6], [6], [6], [7], [7], [7, 23], [7, 16], [], [7, 16], [7, 16], [7, 16], [6], [6], [6], [], [6], [], [], [], [], [], [], [], [], [6], [7], [7, 19], [6], [6], [6], [7], [7], [7, 16], [], [6, 20, 27], [6, 9], [6, 9], [9], [8], [7], [7], [7], [7], [], [8, 21], [6, 9, 16], [6, 9, 21], [7, 11], [7, 11], [], [7, 11, 23], [0, 12, 16, 24], [16, 21], [], [], [11], [7, 11], [7, 11], [7], [], [], [], [], [], [], [30], [11, 30], [0], [0], [0], [0, 9], [0, 9, 12], [0, 12], [0], [0], [], [6, 11], [6, 11, 18], [6, 18], [6, 11], [6, 11], [6, 11], [6], [6, 11], [6, 11], [6], [6, 11], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [11, 30], [12], [25], [12], [0, 12], [], [12], [12], [12], [], [], [], [], [], [11], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []], "tok2midi": {"0": 48, "1": 49, "2": 50, "3": 51, "4": 52, "5": 53, "6": 54, "7": 55, "8": 56, "9": 57, "10": 58, "11": 59, "12": 60, "13": 61, "14": 62, "15": 63, "16": 64, "17": 65, "18": 66, "19": 67, "20": 68, "21": 69, "22": 70, "23": 71, "24": 72, "25": 73, "26": 74, "27": 75, "28": 76, "29": 77, "30": 78, "31": 79, "32": 80, "33": 81, "34": 82, "35": 83, "36": 84, "37": 85, "38": 86, "39": 87, "40": 88, "41": 89, "42": 90, "43": 91}, "n_tokens": 44, "fps": 8, "midi_lo": 48, "midi_hi": 91}
|
piano/samples/48.mp3
ADDED
|
Binary file (25.3 kB). View file
|
|
|
piano/samples/53.mp3
ADDED
|
Binary file (23 kB). View file
|
|
|
piano/samples/60.mp3
ADDED
|
Binary file (20.2 kB). View file
|
|
|
piano/samples/65.mp3
ADDED
|
Binary file (17.3 kB). View file
|
|
|
piano/samples/69.mp3
ADDED
|
Binary file (16.6 kB). View file
|
|
|
piano/samples/74.mp3
ADDED
|
Binary file (15.5 kB). View file
|
|
|
piano/samples/79.mp3
ADDED
|
Binary file (15.6 kB). View file
|
|
|
piano/samples/84.mp3
ADDED
|
Binary file (14.8 kB). View file
|
|
|
piano/samples/89.mp3
ADDED
|
Binary file (14.4 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.15.2
|
| 2 |
+
numpy>=1.24
|
| 3 |
+
torch>=2.2 # the bottom "Mixture of Experts" experiment (lazy-loaded)
|
| 4 |
+
transformers>=4.40 # SpikeWhale config/model + SpikeTokenizer (agents/modmind/)
|
web/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "Quazim0t0's 🍄 Thousand Token Wood Entry"
|
| 3 |
+
emoji: 🍄
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.15.2
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# ⚔️ Modular Mind: Boss Fight
|
| 14 |
+
|
| 15 |
+
A mini **Dark-Souls-style** duel where the boss is controlled by a **Modular Mind** —
|
| 16 |
+
a handful of tiny neural *specialists* that communicate through a **shared latent**
|
| 17 |
+
(a `RecursiveLink` bridge) and a *coordinator* that reads the latent to choose the
|
| 18 |
+
boss's next move. **The brain was trained by self-play reinforcement learning** — its
|
| 19 |
+
tactics emerged from playing thousands of duels, nothing is scripted.
|
| 20 |
+
|
| 21 |
+
You play the **Fire Knight**. Defeat the **Demon Slime**.
|
| 22 |
+
|
| 23 |
+
## How the Modular Mind works
|
| 24 |
+
|
| 25 |
+
This is the [ModularMind-on-V2](https://github.com/your-username/ModularMind) concept at *specialist scale*: instead of one
|
| 26 |
+
monolithic policy, six small networks each handle one concern and talk to each other
|
| 27 |
+
through a latent channel.
|
| 28 |
+
|
| 29 |
+
```
|
| 30 |
+
game state ─▶ ┌──────────────┐ each specialist emits a latent
|
| 31 |
+
│ Aggressor │──┐ (LatentProjection) and, if it OWNS
|
| 32 |
+
│ Stalker │──┤ an action, a "drive" for that action
|
| 33 |
+
│ Survivor │──┤
|
| 34 |
+
│ Baiter │──┤ ┌───────────────┐ ┌─────────────┐
|
| 35 |
+
│ Punisher (M) │──┼─ sum ─▶│ RecursiveLink │─────▶│ Coordinator │─▶ action
|
| 36 |
+
│ Enrage (M) │──┘ │ ReGLU+residual│ shared│ read-out │
|
| 37 |
+
└──────────────┘ └───────────────┘ latent└─────────────┘
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
- **Four action-owning specialists** push their move's score directly:
|
| 41 |
+
**Aggressor → CLEAVE**, **Stalker → APPROACH**, **Survivor → RETREAT**, **Baiter → IDLE**.
|
| 42 |
+
- **Two modulators (M)** — **Punisher** ("the player is open!") and **Enrage**
|
| 43 |
+
("we're low on HP — go berserk") — **own no action**. Their *only* way to affect the
|
| 44 |
+
fight is the latent they write into the shared `RecursiveLink`, which the coordinator
|
| 45 |
+
turns into modulation. So training has to *learn to use the latent channel* — the whole
|
| 46 |
+
point of the architecture.
|
| 47 |
+
|
| 48 |
+
The right-hand panel shows all of this live: each specialist's activity, the shared
|
| 49 |
+
latent bridge, and the coordinator's modulation, for every decision the boss makes.
|
| 50 |
+
|
| 51 |
+
## What emerged from training
|
| 52 |
+
|
| 53 |
+
Trained on a reward that values *dealing damage* and *pressuring in range* over
|
| 54 |
+
playing it safe (landing a cleave ≫ whiffing, and stalling / staying out of range is
|
| 55 |
+
penalised), the boss learned an **aggressive pressure** style:
|
| 56 |
+
|
| 57 |
+
- **closes the distance** when you're far or at mid-range,
|
| 58 |
+
- **cleaves on contact** — once you're in range and it's off cooldown it commits to a
|
| 59 |
+
lunging swing essentially every time (verified: in-range attack-rate ≈ **0.8–1.0**),
|
| 60 |
+
- **retreats only when it can't swing** (mid-cooldown) to reset spacing,
|
| 61 |
+
- **blocks your punish** — a **Defender** specialist raises a guard (negating ~90% of
|
| 62 |
+
your melee) when you swing at it and it can't cleave back,
|
| 63 |
+
- **punishes your recovery** and gets **even more aggressive at low HP** — the Enrage
|
| 64 |
+
modulator raises CLEAVE through the shared latent.
|
| 65 |
+
|
| 66 |
+
It reaches a **~55–65% win rate** against a near-optimal scripted dodger (avg reward
|
| 67 |
+
+8, up from −12 before the reward was tuned for aggression). Against a human it's a
|
| 68 |
+
fair, readable fight: **dodge the red telegraph, then punish the recovery.**
|
| 69 |
+
|
| 70 |
+
> The earlier version of the brain learned a degenerate "space forever, never commit"
|
| 71 |
+
> policy that *technically* won but barely attacked — so the trainer now selects the
|
| 72 |
+
> checkpoint on **win-rate + in-range attack-rate**, and a non-attacking policy can no
|
| 73 |
+
> longer be saved. (`behavior()` in `train.py` measures this directly.)
|
| 74 |
+
|
| 75 |
+
## It learns from your fights (online finetuning)
|
| 76 |
+
|
| 77 |
+
The model is tiny, so a gradient step is microseconds — the boss finetunes from real
|
| 78 |
+
play **on the free CPU**. Each HARD-tier fight is logged (state, action, HP per boss
|
| 79 |
+
decision) and POSTed to `/learn`; the server rebuilds per-decision rewards (damage
|
| 80 |
+
dealt − taken, + kill / − death), computes REINFORCE returns, and takes **one Adam
|
| 81 |
+
step** ([`mm_grad.py`](mm_grad.py), numpy backprop verified against PyTorch to ~1e-8).
|
| 82 |
+
A frozen copy of the sim-trained weights anchors the update so it can't drift into
|
| 83 |
+
nonsense; the adapted weights feed straight back into the live boss.
|
| 84 |
+
|
| 85 |
+
- **On by default, in-memory.** Set `MM_ONLINE=0` to disable.
|
| 86 |
+
- **Persistent across restarts:** add Space secrets `HF_TOKEN` (write) and
|
| 87 |
+
`MM_DATASET_REPO` (e.g. `you/boss-fight-online`) and the adapted weights are pushed
|
| 88 |
+
to / pulled from that Dataset. Only HARD-tier fights train (keeps the data on-policy).
|
| 89 |
+
|
| 90 |
+
## Difficulty = the trained brain's decision-noise
|
| 91 |
+
|
| 92 |
+
The difficulty selector doesn't change the boss's stats — it runs the **same trained
|
| 93 |
+
Modular Mind at a different *mistake-rate*** (`explore` = probability of a random legal
|
| 94 |
+
action). vs a near-optimal scripted dodger:
|
| 95 |
+
|
| 96 |
+
| Tier | Mistake-rate | Boss win-rate | Feel |
|
| 97 |
+
|------|------|------|------|
|
| 98 |
+
| **Easy** | 50% | ~0.35 | erratic, leaves big openings — beatable |
|
| 99 |
+
| **Normal** | 22% | ~0.65 | competent pressure, occasional slip |
|
| 100 |
+
| **Hard** | 4% | ~0.95 | closes in, blocks your punish, relentless |
|
| 101 |
+
|
| 102 |
+
The browser sends the chosen tier with every decision; the server routes it through
|
| 103 |
+
`modular_mind.decide` with that tier's mistake-rate. *(Originally Easy/Normal were
|
| 104 |
+
genuinely-undertrained checkpoints — but once the boss learned to BLOCK it dominates
|
| 105 |
+
the sim almost immediately, so there's no longer a weak checkpoint; decision-noise is
|
| 106 |
+
the controllable, honest dial. `train.py` still snapshots checkpoints if you want them.)*
|
| 107 |
+
|
| 108 |
+
## Controls
|
| 109 |
+
|
| 110 |
+
| Key | Action |
|
| 111 |
+
|-----|--------|
|
| 112 |
+
| ← → | Move |
|
| 113 |
+
| Space | Roll / dodge (i-frames, costs stamina) |
|
| 114 |
+
| J | Attack |
|
| 115 |
+
| K (hold) | **Block** — cuts incoming damage to 20% and drains stamina; if stamina hits 0 your guard **breaks** into a stagger |
|
| 116 |
+
| M / 🔊 | Mute / unmute music + SFX |
|
| 117 |
+
|
| 118 |
+
Background music and combat SFX play during the fight (a random track per fight,
|
| 119 |
+
plus per-action sound effects). Audio is served statically from `audio/`.
|
| 120 |
+
|
| 121 |
+
You begin each fight with a one-time **Aegis shield** (a cyan bubble + the *Aegis*
|
| 122 |
+
HUD bar): for the first few seconds it absorbs **all** damage so you can learn the
|
| 123 |
+
controls. Once it fades it does **not** come back.
|
| 124 |
+
|
| 125 |
+
> Click **Enter the Fog**, then click the game once so it has keyboard focus.
|
| 126 |
+
|
| 127 |
+
## Architecture / files
|
| 128 |
+
|
| 129 |
+
| File | Role |
|
| 130 |
+
|------|------|
|
| 131 |
+
| `app.py` | Gradio Space: serves the game, exposes the trained brain at `/decide` |
|
| 132 |
+
| `modular_mind.py` | **numpy** inference of the trained Modular Mind (no torch at runtime) |
|
| 133 |
+
| `mm_torch.py` | the trainable Modular Mind (specialists + RecursiveLink + coordinator) |
|
| 134 |
+
| `train.py` | self-play **REINFORCE** trainer → `mm_weights.npz` |
|
| 135 |
+
| `mm_grad.py` | pure-numpy forward+backward (REINFORCE gradient), verified vs torch — the online learner |
|
| 136 |
+
| `online.py` | finetunes the HARD brain from real player fights; optional HF-Dataset persistence |
|
| 137 |
+
| `duel_sim.py` | the headless duel simulator (the RL environment) |
|
| 138 |
+
| `features.py` | shared feature/action definitions (single source of truth) |
|
| 139 |
+
| `web/` | the HTML5 canvas game (60fps render; calls `/decide` at decision points) |
|
| 140 |
+
| `audio/` | background music (`mp3/`) and sound effects (`sfx/`), served statically |
|
| 141 |
+
| `serve_local.py` | run the whole thing locally **without gradio** (stdlib + numpy) |
|
| 142 |
+
|
| 143 |
+
The model is **tiny** (~4.5k parameters) and inference is pure numpy, so the Space
|
| 144 |
+
needs only `gradio` + `numpy` and starts instantly.
|
| 145 |
+
|
| 146 |
+
## Run / retrain locally
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
pip install -r requirements.txt # gradio + numpy (runtime)
|
| 150 |
+
python app.py # the Space, locally
|
| 151 |
+
# or, with no gradio at all:
|
| 152 |
+
python serve_local.py # http://localhost:7861
|
| 153 |
+
|
| 154 |
+
# retrain the boss (needs torch):
|
| 155 |
+
pip install torch
|
| 156 |
+
python train.py # -> mm_weights.npz (+ train_log.json)
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
## Credits
|
| 160 |
+
|
| 161 |
+
- **Sprites:** *Fire Knight* and *Demon Slime* free asset packs by **LuizMelo**
|
| 162 |
+
(itch.io). Please check their licenses for your own use.
|
| 163 |
+
- **Audio:** background music tracks (`audio/mp3`) and combat SFX (`audio/sfx`).
|
| 164 |
+
Check the licenses of your audio packs before publishing.
|
| 165 |
+
- **Brain:** the *Modular Mind* concept (latent-communicating specialists via
|
| 166 |
+
`RecursiveLink`), trained here at specialist scale by self-play RL.
|
| 167 |
+
|
| 168 |
+
Built for a HuggingFace hackathon.
|
web/game.css
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Modular Mind: Boss Fight -- dark-souls-ish UI */
|
| 2 |
+
* { box-sizing: border-box; }
|
| 3 |
+
#mm-root {
|
| 4 |
+
--bg:#0d0b10; --panel:#15121b; --edge:#2a2436; --ink:#e8e2d6; --dim:#8a8198;
|
| 5 |
+
--hp:#a01b1b; --hpe:#d23b2f; --php:#c8a24a; --stam:#3a8f5a; --accent:#caa15a;
|
| 6 |
+
font-family: "Trebuchet MS", system-ui, sans-serif;
|
| 7 |
+
color: var(--ink); display:flex; gap:14px; flex-wrap:wrap;
|
| 8 |
+
justify-content:center; align-items:flex-start; padding:8px;
|
| 9 |
+
background: radial-gradient(circle at 50% 0%, #1a1622, #0d0b10 70%);
|
| 10 |
+
border:1px solid var(--edge); border-radius:10px; user-select:none;
|
| 11 |
+
}
|
| 12 |
+
#mm-stage { position:relative; }
|
| 13 |
+
#mm-canvas {
|
| 14 |
+
display:block; background:#000; border:1px solid var(--edge); border-radius:6px;
|
| 15 |
+
image-rendering: pixelated; image-rendering: crisp-edges;
|
| 16 |
+
width:720px; max-width:92vw; height:auto;
|
| 17 |
+
}
|
| 18 |
+
/* health bars overlaid on the stage */
|
| 19 |
+
.mm-bars { position:absolute; left:0; right:0; top:10px; padding:0 18px;
|
| 20 |
+
display:flex; flex-direction:column; gap:8px; pointer-events:none; }
|
| 21 |
+
.mm-bar-row { display:flex; align-items:center; gap:8px; }
|
| 22 |
+
.mm-bar-label { width:96px; font-size:12px; letter-spacing:1px; color:var(--dim);
|
| 23 |
+
text-transform:uppercase; text-shadow:0 1px 2px #000; }
|
| 24 |
+
.mm-bar { flex:1; height:14px; background:#06040a; border:1px solid #000;
|
| 25 |
+
box-shadow:inset 0 0 0 1px #2a2436; position:relative; overflow:hidden; }
|
| 26 |
+
.mm-bar > i { position:absolute; left:0; top:0; bottom:0; display:block;
|
| 27 |
+
transition: width .12s linear; }
|
| 28 |
+
.mm-fill-boss > i { background:linear-gradient(#d23b2f,#7e1414); }
|
| 29 |
+
.mm-fill-php > i { background:linear-gradient(#e3c06a,#9c7a2e); }
|
| 30 |
+
.mm-fill-stam > i { background:linear-gradient(#4fae74,#256b41); height:8px; top:3px; }
|
| 31 |
+
.mm-fill-shield > i { background:linear-gradient(#bff0ff,#3aa6d6); height:8px; top:3px;
|
| 32 |
+
box-shadow:0 0 6px rgba(95,217,255,.7); }
|
| 33 |
+
#mm-shield-row { transition:opacity .4s; }
|
| 34 |
+
.mm-boss-name { text-align:center; font-size:13px; letter-spacing:3px;
|
| 35 |
+
color:var(--hpe); text-transform:uppercase; margin-bottom:2px; text-shadow:0 1px 3px #000;}
|
| 36 |
+
|
| 37 |
+
#mm-mute { position:absolute; top:10px; right:12px; z-index:5; width:34px; height:34px;
|
| 38 |
+
border-radius:6px; border:1px solid var(--edge); background:rgba(13,11,16,.7);
|
| 39 |
+
color:var(--ink); font-size:16px; cursor:pointer; line-height:1; }
|
| 40 |
+
#mm-mute:hover { border-color:var(--accent); }
|
| 41 |
+
|
| 42 |
+
/* centre overlay messages (YOU DIED / VICTORY / start) */
|
| 43 |
+
#mm-overlay { position:absolute; inset:0; display:flex; flex-direction:column;
|
| 44 |
+
align-items:center; justify-content:center; text-align:center; gap:14px;
|
| 45 |
+
background:rgba(4,3,7,.55); backdrop-filter:blur(1px); cursor:pointer; }
|
| 46 |
+
#mm-overlay.hidden { display:none; }
|
| 47 |
+
#mm-big { font-size:52px; letter-spacing:8px; font-weight:bold;
|
| 48 |
+
text-shadow:0 2px 18px #000; }
|
| 49 |
+
#mm-big.died { color:#9a1b1b; }
|
| 50 |
+
#mm-big.win { color:#caa15a; }
|
| 51 |
+
#mm-sub { color:var(--ink); font-size:15px; max-width:520px; line-height:1.5; }
|
| 52 |
+
#mm-start-btn { padding:10px 22px; background:#1d1726; color:var(--accent);
|
| 53 |
+
border:1px solid var(--accent); border-radius:6px; font-size:16px; cursor:pointer;
|
| 54 |
+
letter-spacing:2px; text-transform:uppercase; }
|
| 55 |
+
#mm-start-btn:hover { background:#2a2136; }
|
| 56 |
+
.mm-diff { display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-content:center; }
|
| 57 |
+
.mm-diff-label { font-size:12px; color:var(--dim); letter-spacing:1px; text-transform:uppercase; }
|
| 58 |
+
.mm-diff-btn { display:flex; flex-direction:column; align-items:center; gap:1px;
|
| 59 |
+
padding:6px 14px; background:#161019; color:var(--dim); border:1px solid var(--edge);
|
| 60 |
+
border-radius:6px; cursor:pointer; font-size:14px; letter-spacing:1px; min-width:84px; }
|
| 61 |
+
.mm-diff-btn small { font-size:9px; opacity:.7; letter-spacing:.5px; }
|
| 62 |
+
.mm-diff-btn:hover { border-color:var(--accent); color:var(--ink); }
|
| 63 |
+
.mm-diff-btn.mm-diff-on { background:#2a1f12; color:var(--accent); border-color:var(--accent);
|
| 64 |
+
box-shadow:0 0 8px rgba(202,161,90,.35); }
|
| 65 |
+
.mm-keys { color:var(--dim); font-size:12.5px; line-height:1.9; }
|
| 66 |
+
.mm-keys b { color:var(--ink); background:#241d30; padding:1px 7px; border-radius:4px;
|
| 67 |
+
border:1px solid var(--edge); }
|
| 68 |
+
|
| 69 |
+
/* ---------- Modular Mind telemetry panel ---------- */
|
| 70 |
+
#mm-panel { width:330px; max-width:92vw; background:var(--panel);
|
| 71 |
+
border:1px solid var(--edge); border-radius:8px; padding:12px 13px; }
|
| 72 |
+
#mm-panel h3 { margin:0 0 2px; font-size:14px; letter-spacing:2px; color:var(--accent);
|
| 73 |
+
text-transform:uppercase; }
|
| 74 |
+
#mm-panel .mm-tag { font-size:11px; color:var(--dim); margin-bottom:10px; }
|
| 75 |
+
.mm-decision { display:flex; align-items:center; justify-content:space-between;
|
| 76 |
+
background:#0e0b14; border:1px solid var(--edge); border-radius:6px;
|
| 77 |
+
padding:7px 10px; margin-bottom:10px; }
|
| 78 |
+
.mm-decision .lbl { font-size:11px; color:var(--dim); letter-spacing:1px; }
|
| 79 |
+
.mm-decision .act { font-size:20px; font-weight:bold; letter-spacing:2px; color:#fff; }
|
| 80 |
+
.mm-phase { font-size:11px; padding:2px 8px; border-radius:10px; border:1px solid var(--edge);}
|
| 81 |
+
.mm-phase.p1 { color:#7fb2e6; } .mm-phase.p2 { color:#e6705f; border-color:#7e1414;
|
| 82 |
+
background:#1c0e0e; }
|
| 83 |
+
|
| 84 |
+
.mm-sec-title { font-size:11px; color:var(--dim); letter-spacing:1px;
|
| 85 |
+
text-transform:uppercase; margin:12px 0 6px; }
|
| 86 |
+
.mm-spec { margin-bottom:7px; }
|
| 87 |
+
.mm-spec .top { display:flex; justify-content:space-between; align-items:baseline; }
|
| 88 |
+
.mm-spec .nm { font-size:12.5px; }
|
| 89 |
+
.mm-spec .nm .dot { display:inline-block; width:8px; height:8px; border-radius:50%;
|
| 90 |
+
margin-right:6px; vertical-align:middle; }
|
| 91 |
+
.mm-spec .owns { font-size:10px; color:var(--dim); margin-left:5px; }
|
| 92 |
+
.mm-spec .mod-tag { font-size:9px; color:#e67e22; border:1px solid #5a3a16;
|
| 93 |
+
padding:0 5px; border-radius:8px; margin-left:5px; }
|
| 94 |
+
.mm-spec .val { font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums; }
|
| 95 |
+
.mm-spec .track { height:7px; background:#06040a; border:1px solid #000; margin-top:3px;
|
| 96 |
+
border-radius:3px; overflow:hidden; }
|
| 97 |
+
.mm-spec .track > i { display:block; height:100%; transition:width .15s; }
|
| 98 |
+
.mm-spec.win .nm { color:#fff; font-weight:bold; }
|
| 99 |
+
.mm-spec.win .track { box-shadow:0 0 6px rgba(202,161,90,.6); }
|
| 100 |
+
|
| 101 |
+
.mm-latent { display:flex; gap:2px; align-items:flex-end; height:34px; margin-top:4px;
|
| 102 |
+
background:#0e0b14; border:1px solid var(--edge); border-radius:4px; padding:3px; }
|
| 103 |
+
.mm-latent > i { flex:1; background:linear-gradient(#caa15a,#5a4422); border-radius:1px;
|
| 104 |
+
min-height:1px; transition:height .15s; }
|
| 105 |
+
.mm-flow { font-size:10.5px; color:var(--dim); margin-top:8px; line-height:1.5; }
|
| 106 |
+
.mm-flow b { color:var(--accent); }
|
| 107 |
+
.mm-note { font-size:10px; color:#6f6880; margin-top:10px; border-top:1px solid var(--edge);
|
| 108 |
+
padding-top:8px; line-height:1.5; }
|
web/game.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Modular Mind: Boss Fight -- HTML5 canvas duel.
|
| 2 |
+
* Rendering / physics / animation run at 60fps in the browser. The boss's NEXT
|
| 3 |
+
* ACTION is chosen by the trained Modular Mind (Python) via window.MM_DECIDE,
|
| 4 |
+
* called only at decision points (when the boss is free to commit) -- exactly how
|
| 5 |
+
* a souls boss commits to a move. Combat balance mirrors duel_sim.py so the boss
|
| 6 |
+
* behaves the way it was trained.
|
| 7 |
+
*/
|
| 8 |
+
(function () {
|
| 9 |
+
"use strict";
|
| 10 |
+
|
| 11 |
+
// ---- world / balance (world units; speeds in units/sec) ----------------
|
| 12 |
+
const W = 900, Hgt = 420, GROUND = 372;
|
| 13 |
+
const CFG = {
|
| 14 |
+
BOSS_HP: 100, PLAYER_HP: 100,
|
| 15 |
+
BOSS_MOVE: 190, PLAYER_MOVE: 188, ROLL_SPEED: 230,
|
| 16 |
+
MIN_GAP: 46,
|
| 17 |
+
CLEAVE_REACH: 170, CLEAVE_DMG: 22, CLEAVE_LUNGE: 270,
|
| 18 |
+
CLEAVE_WINDUP: 0.30, CLEAVE_ACTIVE: 0.17, CLEAVE_RECOVER: 0.42, CLEAVE_COOLDOWN: 0.34,
|
| 19 |
+
PATK_REACH: 120, PATK_DMG: 6,
|
| 20 |
+
PATK_WINDUP: 0.13, PATK_ACTIVE: 0.10, PATK_RECOVER: 0.30,
|
| 21 |
+
ROLL_DUR: 0.53, ROLL_IFR_A: 0.10, ROLL_IFR_B: 0.37,
|
| 22 |
+
BLOCK_MIT: 0.2, STAGGER: 0.5,
|
| 23 |
+
BOSS_BLOCK_TIME: 0.4, BOSS_BLOCK_MIT: 0.1, // boss guard: duration + dmg taken multiplier
|
| 24 |
+
STAM_MAX: 100, STAM_ROLL: 28, STAM_ATK: 18, STAM_BLOCK: 24, STAM_REGEN: 26,
|
| 25 |
+
COMMIT_MOVE: 0.30, COMMIT_IDLE: 0.34,
|
| 26 |
+
THINK_MIN: 0.10,
|
| 27 |
+
SHIELD_TIME: 5.0, // one-time "learning grace": absorbs all damage at the
|
| 28 |
+
// start of the fight, then it's gone for good.
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
// ---- assets ------------------------------------------------------------
|
| 32 |
+
const A = window.GAME_ASSETS;
|
| 33 |
+
const imgs = {};
|
| 34 |
+
function loadImg(src) { return new Promise(r => { const i = new Image(); i.onload = () => r(i); i.src = src; }); }
|
| 35 |
+
|
| 36 |
+
// ---- audio (served statically; base injected by the server) ------------
|
| 37 |
+
const AUDIO_BASE = window.MM_AUDIO_BASE || "audio/";
|
| 38 |
+
const SFX = {
|
| 39 |
+
p_attack: "sfx/attack_knight.wav", p_hurt: "sfx/hurt_knight.wav",
|
| 40 |
+
p_roll: "sfx/jump_knight.wav", p_die: "sfx/die_knight.wav",
|
| 41 |
+
shield: "sfx/gem.wav", ui: "sfx/coin.wav",
|
| 42 |
+
b_cleave: "sfx/axe_boss.wav", b_hurt: "sfx/hurt_monster.wav",
|
| 43 |
+
b_die: "sfx/die_boss.wav", roar: "sfx/roar_monster.wav",
|
| 44 |
+
b_block: "sfx/bit_monster.wav",
|
| 45 |
+
};
|
| 46 |
+
const MUSIC = Array.from({ length: 12 }, (_, i) => `mp3/Pixel ${i + 1}.mp3`);
|
| 47 |
+
const sfxBuf = {};
|
| 48 |
+
let musicEl = null, muted = false, audioReady = false;
|
| 49 |
+
const aurl = p => AUDIO_BASE + encodeURI(p);
|
| 50 |
+
|
| 51 |
+
function initAudio() {
|
| 52 |
+
if (audioReady) return; audioReady = true;
|
| 53 |
+
for (const k in SFX) { const a = new Audio(aurl(SFX[k])); a.preload = "auto"; sfxBuf[k] = a; }
|
| 54 |
+
}
|
| 55 |
+
function playSfx(k, vol = 0.6) {
|
| 56 |
+
if (muted || !sfxBuf[k]) return;
|
| 57 |
+
const a = sfxBuf[k].cloneNode(); a.volume = vol; a.play().catch(() => {});
|
| 58 |
+
}
|
| 59 |
+
function startMusic() {
|
| 60 |
+
if (muted) return;
|
| 61 |
+
const track = MUSIC[Math.floor(Math.random() * MUSIC.length)];
|
| 62 |
+
if (musicEl) musicEl.pause();
|
| 63 |
+
musicEl = new Audio(aurl(track));
|
| 64 |
+
musicEl.volume = 0.3;
|
| 65 |
+
musicEl.addEventListener("ended", () => { if (!muted) startMusic(); });
|
| 66 |
+
musicEl.play().catch(() => {});
|
| 67 |
+
}
|
| 68 |
+
function stopMusic() { if (musicEl) { musicEl.pause(); musicEl = null; } }
|
| 69 |
+
function toggleMute() {
|
| 70 |
+
muted = !muted;
|
| 71 |
+
if (muted) { if (musicEl) musicEl.pause(); }
|
| 72 |
+
else if (musicEl) musicEl.play().catch(() => {});
|
| 73 |
+
const b = document.getElementById("mm-mute"); if (b) b.textContent = muted ? "🔇" : "🔊";
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// ---- animation -----------------------------------------------------------
|
| 77 |
+
class Anim {
|
| 78 |
+
constructor(who) { this.man = A[who].manifest; this.img = imgs[who]; this.who = who;
|
| 79 |
+
// native facing of the source art: the knight faces RIGHT, the demon faces LEFT.
|
| 80 |
+
// We flip when the desired world-facing differs from the art's native facing.
|
| 81 |
+
this.native = who === "boss" ? -1 : 1;
|
| 82 |
+
this.name = "idle"; this.t = 0; this.idx = 0; this.done = false; this.loop = true; this.speed = 1; this.rev = false; }
|
| 83 |
+
set(name, loop = true, speed = 1, rev = false) {
|
| 84 |
+
// don't restart an animation that is already playing (it is re-requested
|
| 85 |
+
// every frame by the state machine); a real change of name restarts it.
|
| 86 |
+
if (this.name === name) { this.loop = loop; this.speed = speed; this.rev = rev; return; }
|
| 87 |
+
this.name = name; this.loop = loop; this.speed = speed; this.rev = rev; this.t = 0; this.done = false;
|
| 88 |
+
this.idx = rev ? this.man.anims[name].frames - 1 : 0;
|
| 89 |
+
}
|
| 90 |
+
update(dt) {
|
| 91 |
+
const a = this.man.anims[this.name]; if (!a) return;
|
| 92 |
+
this.t += dt * a.fps * this.speed;
|
| 93 |
+
if (this.t >= 1) {
|
| 94 |
+
const steps = Math.floor(this.t); this.t -= steps;
|
| 95 |
+
this.idx += steps * (this.rev ? -1 : 1);
|
| 96 |
+
if (this.rev) {
|
| 97 |
+
if (this.idx < 0) {
|
| 98 |
+
if (this.loop) this.idx = ((this.idx % a.frames) + a.frames) % a.frames;
|
| 99 |
+
else { this.idx = 0; this.done = true; }
|
| 100 |
+
}
|
| 101 |
+
} else if (this.idx >= a.frames) {
|
| 102 |
+
if (this.loop) this.idx %= a.frames;
|
| 103 |
+
else { this.idx = a.frames - 1; this.done = true; }
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
progress() { const a = this.man.anims[this.name]; return (this.idx + this.t) / a.frames; }
|
| 108 |
+
draw(ctx, x, facing, scale) {
|
| 109 |
+
const m = this.man, a = m.anims[this.name];
|
| 110 |
+
const sx = this.idx * m.frameW, sy = a.row * m.frameH;
|
| 111 |
+
const dw = m.frameW * scale, dh = m.frameH * scale;
|
| 112 |
+
const dy = GROUND - m.footY * scale;
|
| 113 |
+
// translate to the entity's feet/centre, mirror by facing, draw so the
|
| 114 |
+
// frame's centreX maps to local 0 and footY maps to GROUND.
|
| 115 |
+
ctx.save();
|
| 116 |
+
ctx.translate(x, dy);
|
| 117 |
+
// flip when the wanted world-facing differs from the art's native facing
|
| 118 |
+
ctx.scale(facing * this.native < 0 ? -1 : 1, 1);
|
| 119 |
+
ctx.drawImage(this.img, sx, sy, m.frameW, m.frameH, -m.centerX * scale, 0, dw, dh);
|
| 120 |
+
ctx.restore();
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// ---- entities ----------------------------------------------------------
|
| 125 |
+
const boss = { x: 250, hp: CFG.BOSS_HP, facing: 1, anim: null,
|
| 126 |
+
state: "idle", t: 0, cd: 0, think: 0, pending: false, action: "IDLE",
|
| 127 |
+
actT: 0, cleavePhase: "", cleaveHit: false, scale: 1.7, telemetry: null, blockFlash: 0 };
|
| 128 |
+
const player = { x: 650, hp: CFG.PLAYER_HP, stam: CFG.STAM_MAX, facing: -1, anim: null,
|
| 129 |
+
state: "idle", t: 0, atkHit: false, scale: 2.4, shield: 0, shieldFlash: 0 };
|
| 130 |
+
|
| 131 |
+
const keys = {};
|
| 132 |
+
let game = "menu"; // menu | fight | dead | win
|
| 133 |
+
let lastDecision = null;
|
| 134 |
+
let difficulty = "hard"; // easy | normal | hard (which trained brain runs the boss)
|
| 135 |
+
let fightLog = []; // per-decision log sent to the online learner at fight end
|
| 136 |
+
|
| 137 |
+
// ---- helpers -----------------------------------------------------------
|
| 138 |
+
const dist = () => Math.abs(player.x - boss.x);
|
| 139 |
+
// Characters face the way they MOVE while walking/running/rolling, and face
|
| 140 |
+
// their opponent when idle or attacking (so swings connect & telegraphs aim
|
| 141 |
+
// correctly). Called once per frame after the updates, before rendering.
|
| 142 |
+
function updateFacings() {
|
| 143 |
+
const towardBossFromPlayer = boss.x >= player.x ? 1 : -1;
|
| 144 |
+
if (player.state === "run") {
|
| 145 |
+
const left = keys["ArrowLeft"] || keys["a"] || keys["A"];
|
| 146 |
+
const right = keys["ArrowRight"] || keys["d"] || keys["D"];
|
| 147 |
+
if (right && !left) player.facing = 1;
|
| 148 |
+
else if (left && !right) player.facing = -1; // both/neither -> keep
|
| 149 |
+
} else if (player.state === "roll") {
|
| 150 |
+
player.facing = player.rollDir; // roll in the direction it travels
|
| 151 |
+
} else if (player.state !== "dead") {
|
| 152 |
+
player.facing = towardBossFromPlayer; // idle/attack/block/hit face the boss
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// the boss is locked onto the player: it ALWAYS faces you (it never turns its
|
| 156 |
+
// back). Retreat is rendered as a backstep (reversed walk) instead of a turn.
|
| 157 |
+
if (boss.state !== "dead") boss.facing = player.x >= boss.x ? 1 : -1;
|
| 158 |
+
}
|
| 159 |
+
function clampX(o) { o.x = Math.max(40, Math.min(W - 40, o.x)); }
|
| 160 |
+
function enforceGap() {
|
| 161 |
+
// a roll lets the player dodge THROUGH the boss to the other side; the boss's
|
| 162 |
+
// always-face-the-player logic then spins it around. Otherwise bodies don't overlap.
|
| 163 |
+
if (player.state === "roll") return;
|
| 164 |
+
if (dist() < CFG.MIN_GAP) {
|
| 165 |
+
if (player.x < boss.x) player.x = boss.x - CFG.MIN_GAP; else player.x = boss.x + CFG.MIN_GAP;
|
| 166 |
+
clampX(player);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
const phase = () => (boss.hp < CFG.BOSS_HP * 0.5 ? 2 : 1);
|
| 170 |
+
|
| 171 |
+
// ===================== PLAYER =====================
|
| 172 |
+
function playerUpdate(dt) {
|
| 173 |
+
if (player.shield > 0) player.shield = Math.max(0, player.shield - dt);
|
| 174 |
+
if (player.shieldFlash > 0) player.shieldFlash = Math.max(0, player.shieldFlash - dt);
|
| 175 |
+
if (player.state === "dead") { player.anim.set("death", false); return; }
|
| 176 |
+
player.stam = Math.min(CFG.STAM_MAX, player.stam + CFG.STAM_REGEN * dt);
|
| 177 |
+
|
| 178 |
+
// committed states
|
| 179 |
+
if (player.state === "roll") {
|
| 180 |
+
player.t += dt; player.x += player.rollDir * CFG.ROLL_SPEED * dt; clampX(player); enforceGap();
|
| 181 |
+
if (player.t >= CFG.ROLL_DUR) { player.state = "idle"; }
|
| 182 |
+
return;
|
| 183 |
+
}
|
| 184 |
+
if (player.state === "attack") {
|
| 185 |
+
player.t += dt;
|
| 186 |
+
const a0 = CFG.PATK_WINDUP, a1 = CFG.PATK_WINDUP + CFG.PATK_ACTIVE;
|
| 187 |
+
if (player.t >= a0 && player.t < a1 && !player.atkHit) {
|
| 188 |
+
if (dist() <= CFG.PATK_REACH) { hitBoss(CFG.PATK_DMG); player.atkHit = true; }
|
| 189 |
+
}
|
| 190 |
+
if (player.t >= a1 + CFG.PATK_RECOVER) player.state = "idle";
|
| 191 |
+
return;
|
| 192 |
+
}
|
| 193 |
+
if (player.state === "stagger") { player.t += dt; if (player.t >= CFG.STAGGER) player.state = "idle"; return; }
|
| 194 |
+
if (player.state === "hit") { player.t += dt; if (player.anim.done) player.state = "idle"; }
|
| 195 |
+
|
| 196 |
+
// free: read input
|
| 197 |
+
let moving = false;
|
| 198 |
+
const left = keys["ArrowLeft"] || keys["a"] || keys["A"];
|
| 199 |
+
const right = keys["ArrowRight"] || keys["d"] || keys["D"];
|
| 200 |
+
const blocking = keys["k"] || keys["K"];
|
| 201 |
+
if (blocking) { player.state = "block"; }
|
| 202 |
+
else if (player.state === "block") { player.state = "idle"; }
|
| 203 |
+
|
| 204 |
+
if (player.state !== "block") {
|
| 205 |
+
if (left) { player.x -= CFG.PLAYER_MOVE * dt; moving = true; }
|
| 206 |
+
if (right) { player.x += CFG.PLAYER_MOVE * dt; moving = true; }
|
| 207 |
+
clampX(player); enforceGap();
|
| 208 |
+
player.state = moving ? "run" : "idle";
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
function playerAttack() {
|
| 212 |
+
if (["attack", "roll", "stagger", "hit", "dead"].includes(player.state)) return;
|
| 213 |
+
if (player.stam < CFG.STAM_ATK) return;
|
| 214 |
+
player.state = "attack"; player.t = 0; player.atkHit = false; player.stam -= CFG.STAM_ATK;
|
| 215 |
+
playSfx("p_attack", 0.5);
|
| 216 |
+
}
|
| 217 |
+
function playerRoll() {
|
| 218 |
+
if (["roll", "stagger", "hit", "dead"].includes(player.state)) return;
|
| 219 |
+
if (player.stam < CFG.STAM_ROLL) return;
|
| 220 |
+
player.state = "roll"; player.t = 0; player.stam -= CFG.STAM_ROLL;
|
| 221 |
+
playSfx("p_roll", 0.5);
|
| 222 |
+
player.rollDir = -player.facing; // roll backward (away from boss) by default
|
| 223 |
+
if (keys["ArrowLeft"] || keys["a"]) player.rollDir = -1;
|
| 224 |
+
if (keys["ArrowRight"] || keys["d"]) player.rollDir = 1;
|
| 225 |
+
}
|
| 226 |
+
function playerIframe() {
|
| 227 |
+
return player.state === "roll" && player.t >= CFG.ROLL_IFR_A && player.t <= CFG.ROLL_IFR_B;
|
| 228 |
+
}
|
| 229 |
+
function playerRecovering() {
|
| 230 |
+
if (player.state === "stagger") return true;
|
| 231 |
+
if (player.state === "attack" && player.t >= CFG.PATK_WINDUP + CFG.PATK_ACTIVE) return true;
|
| 232 |
+
if (player.state === "roll" && player.t > CFG.ROLL_IFR_B) return true;
|
| 233 |
+
return false;
|
| 234 |
+
}
|
| 235 |
+
function setPlayerAnim() {
|
| 236 |
+
const s = player.state;
|
| 237 |
+
if (s === "dead") player.anim.set("death", false);
|
| 238 |
+
else if (s === "hit") player.anim.set("take_hit", false);
|
| 239 |
+
else if (s === "stagger") player.anim.set("take_hit", false);
|
| 240 |
+
else if (s === "attack") player.anim.set("attack", false, 1.25);
|
| 241 |
+
else if (s === "roll") player.anim.set("roll", false, 1.1);
|
| 242 |
+
else if (s === "block") player.anim.set("defend", true);
|
| 243 |
+
else if (s === "run") player.anim.set("run", true);
|
| 244 |
+
else player.anim.set("idle", true);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// ===================== BOSS =====================
|
| 248 |
+
function hitBoss(dmg) {
|
| 249 |
+
if (boss.state === "dead") return;
|
| 250 |
+
// the boss's guard negates most of an incoming melee (no flinch either)
|
| 251 |
+
if (boss.state === "block") {
|
| 252 |
+
boss.blockFlash = 0.18; playSfx("b_block", 0.5);
|
| 253 |
+
boss.hp = Math.max(0, boss.hp - dmg * CFG.BOSS_BLOCK_MIT);
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
const wasPhase1 = boss.hp >= CFG.BOSS_HP * 0.5;
|
| 257 |
+
boss.hp = Math.max(0, boss.hp - dmg);
|
| 258 |
+
if (boss.hp <= 0) { boss.state = "dead"; boss.anim.set("death", false); playSfx("b_die", 0.8); return; }
|
| 259 |
+
playSfx("b_hurt", 0.5);
|
| 260 |
+
if (wasPhase1 && boss.hp < CFG.BOSS_HP * 0.5) playSfx("roar", 0.8); // phase-2 enrage roar
|
| 261 |
+
// hyperarmor during a cleave: damage lands but no flinch
|
| 262 |
+
if (boss.state !== "cleave") { boss.state = "hit"; boss.t = 0; boss.anim.set("take_hit", false); }
|
| 263 |
+
}
|
| 264 |
+
function hitPlayer(dmg) {
|
| 265 |
+
if (player.state === "dead" || playerIframe()) return;
|
| 266 |
+
if (player.shield > 0) { player.shieldFlash = 0.22; playSfx("shield", 0.5); return; } // grace shield absorbs it
|
| 267 |
+
let d = dmg;
|
| 268 |
+
if (player.state === "block") {
|
| 269 |
+
player.stam -= CFG.STAM_BLOCK;
|
| 270 |
+
if (player.stam <= 0) { player.stam = 0; player.state = "stagger"; player.t = 0; }
|
| 271 |
+
else { d *= CFG.BLOCK_MIT; }
|
| 272 |
+
}
|
| 273 |
+
player.hp = Math.max(0, player.hp - d);
|
| 274 |
+
if (player.hp <= 0) { player.state = "dead"; player.t = 0; player.anim.set("death", false); playSfx("p_die", 0.8); return; }
|
| 275 |
+
playSfx("p_hurt", 0.5);
|
| 276 |
+
if (player.state !== "stagger" && player.state !== "block") { player.state = "hit"; player.t = 0; }
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
function bossState() {
|
| 280 |
+
return {
|
| 281 |
+
arenaW: W, cleaveReach: CFG.CLEAVE_REACH,
|
| 282 |
+
bossX: boss.x, playerX: player.x,
|
| 283 |
+
bossHP: boss.hp / CFG.BOSS_HP, playerHP: player.hp / CFG.PLAYER_HP,
|
| 284 |
+
bossCooldown: boss.cd > 0 ? boss.cd : 0,
|
| 285 |
+
difficulty: difficulty,
|
| 286 |
+
playerAttacking: player.state === "attack",
|
| 287 |
+
playerApproaching: (player.state === "run") && (Math.sign(player.facing) === Math.sign(boss.x - player.x)),
|
| 288 |
+
playerRecovering: playerRecovering(),
|
| 289 |
+
playerBlocking: player.state === "block",
|
| 290 |
+
playerThreat: player.state === "attack" && dist() <= CFG.PATK_REACH + 12,
|
| 291 |
+
};
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
async function requestDecision() {
|
| 295 |
+
boss.pending = true;
|
| 296 |
+
const st = bossState();
|
| 297 |
+
let tel;
|
| 298 |
+
try { tel = await window.MM_DECIDE(st); }
|
| 299 |
+
catch (e) { tel = fallbackBrain(st); }
|
| 300 |
+
boss.pending = false;
|
| 301 |
+
if (boss.state === "dead" || game !== "fight") return;
|
| 302 |
+
boss.telemetry = tel; lastDecision = tel;
|
| 303 |
+
applyBossAction(tel.action);
|
| 304 |
+
// log this decision for the online learner (state + action + HP at decision time)
|
| 305 |
+
fightLog.push({ state: st, action: tel.action, bossHP: st.bossHP, playerHP: st.playerHP });
|
| 306 |
+
updatePanel(tel);
|
| 307 |
+
}
|
| 308 |
+
function applyBossAction(action) {
|
| 309 |
+
boss.action = action; boss.actT = 0;
|
| 310 |
+
if (action === "CLEAVE" && boss.cd <= 0) { startCleave(); }
|
| 311 |
+
else if (action === "APPROACH") { boss.state = "approach"; }
|
| 312 |
+
else if (action === "RETREAT") { boss.state = "retreat"; }
|
| 313 |
+
else if (action === "BLOCK") { startBlock(); }
|
| 314 |
+
else { boss.state = "idle"; }
|
| 315 |
+
}
|
| 316 |
+
function startCleave() {
|
| 317 |
+
boss.state = "cleave"; boss.cleavePhase = "windup"; boss.t = 0; boss.cleaveHit = false;
|
| 318 |
+
const sp = phase() === 2 ? 1.25 : 1.0;
|
| 319 |
+
boss.anim.set("cleave", false, sp);
|
| 320 |
+
playSfx("b_cleave", 0.6);
|
| 321 |
+
}
|
| 322 |
+
function startBlock() {
|
| 323 |
+
boss.state = "block"; boss.t = 0; boss.blockFlash = 0;
|
| 324 |
+
boss.anim.set("idle", true); // no block frame in the sheet -> braced idle + guard FX
|
| 325 |
+
playSfx("b_block", 0.35);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
function bossUpdate(dt) {
|
| 329 |
+
if (boss.cd > 0) boss.cd -= dt;
|
| 330 |
+
if (boss.state === "dead") { boss.anim.set("death", false); return; }
|
| 331 |
+
const ph = phase();
|
| 332 |
+
const moveMul = ph === 2 ? 1.18 : 1.0;
|
| 333 |
+
|
| 334 |
+
if (boss.state === "hit") {
|
| 335 |
+
boss.t += dt; if (boss.anim.done) { boss.state = "idle"; boss.think = CFG.THINK_MIN; }
|
| 336 |
+
boss.anim.set("take_hit", false); return;
|
| 337 |
+
}
|
| 338 |
+
if (boss.state === "block") {
|
| 339 |
+
boss.t += dt; if (boss.blockFlash > 0) boss.blockFlash -= dt;
|
| 340 |
+
boss.anim.set("idle", true);
|
| 341 |
+
if (boss.t >= CFG.BOSS_BLOCK_TIME) { boss.state = "idle"; boss.actT = 0; boss.think = CFG.THINK_MIN; }
|
| 342 |
+
return;
|
| 343 |
+
}
|
| 344 |
+
if (boss.state === "cleave") { cleaveUpdate(dt, ph); return; }
|
| 345 |
+
|
| 346 |
+
if (boss.state === "approach" || boss.state === "retreat") {
|
| 347 |
+
boss.actT += dt;
|
| 348 |
+
const sign = boss.state === "approach" ? 1 : -1;
|
| 349 |
+
const toward = player.x > boss.x ? 1 : -1;
|
| 350 |
+
boss.x += sign * toward * CFG.BOSS_MOVE * moveMul * dt;
|
| 351 |
+
clampX(boss); enforceGap();
|
| 352 |
+
// approach = walk forward; retreat = reversed walk so it backsteps (feet move
|
| 353 |
+
// backward) while still facing the player, instead of moonwalking.
|
| 354 |
+
const backstep = boss.state === "retreat";
|
| 355 |
+
boss.anim.set("walk", true, backstep ? 1.0 : 1.1, backstep);
|
| 356 |
+
if (boss.actT >= CFG.COMMIT_MOVE) tryDecide();
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// idle
|
| 361 |
+
boss.anim.set("idle", true);
|
| 362 |
+
boss.actT += dt;
|
| 363 |
+
if (boss.actT >= CFG.COMMIT_IDLE) tryDecide();
|
| 364 |
+
}
|
| 365 |
+
function tryDecide() {
|
| 366 |
+
if (boss.pending) return;
|
| 367 |
+
if (boss.think > 0) { boss.think -= 1 / 60; return; }
|
| 368 |
+
requestDecision();
|
| 369 |
+
boss.think = CFG.THINK_MIN;
|
| 370 |
+
}
|
| 371 |
+
function cleaveUpdate(dt, ph) {
|
| 372 |
+
boss.t += dt;
|
| 373 |
+
const spMul = ph === 2 ? 1.25 : 1.0;
|
| 374 |
+
const wu = CFG.CLEAVE_WINDUP / spMul, ac = CFG.CLEAVE_ACTIVE / spMul, rc = CFG.CLEAVE_RECOVER / spMul;
|
| 375 |
+
if (boss.t < wu) { boss.cleavePhase = "windup"; }
|
| 376 |
+
else if (boss.t < wu + ac) {
|
| 377 |
+
boss.cleavePhase = "active";
|
| 378 |
+
const toward = player.x > boss.x ? 1 : -1;
|
| 379 |
+
boss.x += toward * CFG.CLEAVE_LUNGE * spMul * dt; clampX(boss); enforceGap();
|
| 380 |
+
if (!boss.cleaveHit && dist() <= CFG.CLEAVE_REACH && !playerIframe()) {
|
| 381 |
+
hitPlayer(CFG.CLEAVE_DMG); boss.cleaveHit = true;
|
| 382 |
+
}
|
| 383 |
+
} else if (boss.t < wu + ac + rc) { boss.cleavePhase = "recover"; }
|
| 384 |
+
else {
|
| 385 |
+
boss.cd = CFG.CLEAVE_COOLDOWN; boss.state = "idle"; boss.actT = 0;
|
| 386 |
+
boss.think = CFG.THINK_MIN;
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// ---- JS fallback brain (only if the Python call fails) -----------------
|
| 391 |
+
function fallbackBrain(s) {
|
| 392 |
+
const inRange = Math.abs(s.playerX - s.bossX) <= s.cleaveReach;
|
| 393 |
+
const ready = !(s.bossCooldown > 0);
|
| 394 |
+
let action = "IDLE";
|
| 395 |
+
if (inRange && ready) action = "CLEAVE";
|
| 396 |
+
else if (!inRange) action = "APPROACH";
|
| 397 |
+
else if (inRange && !ready) action = "RETREAT";
|
| 398 |
+
const mk = a => ({ name: a, owns: null, color: "#888", drive: 0, latent_norm: 0 });
|
| 399 |
+
return { action, phase: s.bossHP < 0.5 ? 2 : 1, trained: false, fallback: true,
|
| 400 |
+
specialists: [], base_drive: {}, modulation: {}, final_drive: {}, probs: {},
|
| 401 |
+
legal: {}, shared_latent: [] };
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
// ===================== RENDER =====================
|
| 405 |
+
function drawArena(ctx) {
|
| 406 |
+
// sky
|
| 407 |
+
const g = ctx.createLinearGradient(0, 0, 0, Hgt);
|
| 408 |
+
g.addColorStop(0, "#241726"); g.addColorStop(0.6, "#160f1c"); g.addColorStop(1, "#0a0710");
|
| 409 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, Hgt);
|
| 410 |
+
// distant pillars
|
| 411 |
+
ctx.fillStyle = "rgba(40,30,52,.6)";
|
| 412 |
+
for (let i = 0; i < 6; i++) { const x = 60 + i * 150; ctx.fillRect(x, 120, 46, GROUND - 120); }
|
| 413 |
+
// ground
|
| 414 |
+
ctx.fillStyle = "#0c0a12"; ctx.fillRect(0, GROUND, W, Hgt - GROUND);
|
| 415 |
+
ctx.fillStyle = "rgba(202,161,90,.10)"; ctx.fillRect(0, GROUND, W, 3);
|
| 416 |
+
// fog
|
| 417 |
+
ctx.fillStyle = "rgba(160,27,27,.05)"; ctx.fillRect(0, GROUND - 40, W, 40);
|
| 418 |
+
}
|
| 419 |
+
function drawShadow(ctx, x, w) {
|
| 420 |
+
ctx.save(); ctx.globalAlpha = .35; ctx.fillStyle = "#000";
|
| 421 |
+
ctx.beginPath(); ctx.ellipse(x, GROUND + 2, w, 7, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();
|
| 422 |
+
}
|
| 423 |
+
function drawTelegraph(ctx) {
|
| 424 |
+
if (boss.state === "cleave" && boss.cleavePhase === "windup") {
|
| 425 |
+
const reach = CFG.CLEAVE_REACH;
|
| 426 |
+
ctx.save();
|
| 427 |
+
ctx.fillStyle = "rgba(210,59,47,.18)";
|
| 428 |
+
const x0 = boss.facing > 0 ? boss.x : boss.x - reach;
|
| 429 |
+
ctx.fillRect(x0, GROUND - 90, reach, 90);
|
| 430 |
+
ctx.strokeStyle = "rgba(210,59,47,.55)"; ctx.lineWidth = 2; ctx.strokeRect(x0, GROUND - 90, reach, 90);
|
| 431 |
+
ctx.restore();
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function drawShield(ctx) {
|
| 436 |
+
if (player.shield <= 0) return;
|
| 437 |
+
const frac = player.shield / CFG.SHIELD_TIME;
|
| 438 |
+
const cx = player.x, cy = GROUND - 52;
|
| 439 |
+
const pulse = 1 + 0.05 * Math.sin(performance.now() / 140);
|
| 440 |
+
const rx = 46 * pulse, ry = 64 * pulse;
|
| 441 |
+
ctx.save();
|
| 442 |
+
// soft fill
|
| 443 |
+
ctx.globalAlpha = 0.10 + 0.16 * frac + (player.shieldFlash > 0 ? 0.4 : 0);
|
| 444 |
+
ctx.fillStyle = "#5fd9ff";
|
| 445 |
+
ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.fill();
|
| 446 |
+
// ring
|
| 447 |
+
ctx.globalAlpha = 0.5 + 0.4 * frac + (player.shieldFlash > 0 ? 0.4 : 0);
|
| 448 |
+
ctx.lineWidth = 2; ctx.strokeStyle = "#bff0ff";
|
| 449 |
+
ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.stroke();
|
| 450 |
+
ctx.restore();
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function drawBossGuard(ctx) {
|
| 454 |
+
if (boss.state !== "block") return;
|
| 455 |
+
const cx = boss.x + boss.facing * 52, cy = GROUND - 70;
|
| 456 |
+
const flash = boss.blockFlash > 0 ? 0.5 : 0;
|
| 457 |
+
ctx.save();
|
| 458 |
+
// a hexagonal guard ward in front of the boss, facing the player
|
| 459 |
+
ctx.translate(cx, cy);
|
| 460 |
+
ctx.strokeStyle = "#ff7a3c"; ctx.fillStyle = "rgba(255,120,60," + (0.12 + flash) + ")";
|
| 461 |
+
ctx.lineWidth = 2.5;
|
| 462 |
+
ctx.beginPath();
|
| 463 |
+
for (let i = 0; i < 6; i++) {
|
| 464 |
+
const a = Math.PI / 2 + i * Math.PI / 3;
|
| 465 |
+
const px = Math.cos(a) * 26 * boss.facing, py = Math.sin(a) * 56;
|
| 466 |
+
i ? ctx.lineTo(px, py) : ctx.moveTo(px, py);
|
| 467 |
+
}
|
| 468 |
+
ctx.closePath(); ctx.fill();
|
| 469 |
+
ctx.globalAlpha = 0.7 + flash; ctx.stroke();
|
| 470 |
+
ctx.restore();
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
function render() {
|
| 474 |
+
const ctx = window.__mmctx;
|
| 475 |
+
drawArena(ctx);
|
| 476 |
+
drawTelegraph(ctx);
|
| 477 |
+
drawShadow(ctx, boss.x, 64); drawShadow(ctx, player.x, 34);
|
| 478 |
+
// draw order by x depth feel: boss behind if further; simple: boss first
|
| 479 |
+
boss.anim.draw(ctx, boss.x, boss.facing, boss.scale);
|
| 480 |
+
drawBossGuard(ctx);
|
| 481 |
+
player.anim.draw(ctx, player.x, player.facing, player.scale);
|
| 482 |
+
drawShield(ctx);
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// ===================== HUD / PANEL =====================
|
| 486 |
+
function setW(id, frac) { const e = document.getElementById(id); if (e) e.style.width = (Math.max(0, Math.min(1, frac)) * 100) + "%"; }
|
| 487 |
+
function updateHUD() {
|
| 488 |
+
setW("mm-hp-boss", boss.hp / CFG.BOSS_HP);
|
| 489 |
+
setW("mm-hp-php", player.hp / CFG.PLAYER_HP);
|
| 490 |
+
setW("mm-stam", player.stam / CFG.STAM_MAX);
|
| 491 |
+
const row = document.getElementById("mm-shield-row");
|
| 492 |
+
if (row) {
|
| 493 |
+
setW("mm-shield", player.shield / CFG.SHIELD_TIME);
|
| 494 |
+
row.style.opacity = player.shield > 0 ? "1" : "0.25";
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
const ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"];
|
| 499 |
+
function updatePanel(t) {
|
| 500 |
+
const dec = document.getElementById("mm-act"); if (dec) dec.textContent = t.action;
|
| 501 |
+
const ph = document.getElementById("mm-phase");
|
| 502 |
+
if (ph) { ph.textContent = "PHASE " + t.phase; ph.className = "mm-phase p" + t.phase; }
|
| 503 |
+
// specialists
|
| 504 |
+
const wrap = document.getElementById("mm-specs"); if (!wrap) return;
|
| 505 |
+
let maxDrive = 0.001;
|
| 506 |
+
(t.specialists || []).forEach(s => { if (s.drive != null) maxDrive = Math.max(maxDrive, Math.abs(s.drive)); });
|
| 507 |
+
let html = "";
|
| 508 |
+
(t.specialists || []).forEach(s => {
|
| 509 |
+
const isMod = s.owns === null;
|
| 510 |
+
const winning = (s.owns === t.action);
|
| 511 |
+
const val = isMod ? s.latent_norm : s.drive;
|
| 512 |
+
const norm = isMod ? Math.min(1, (s.latent_norm || 0) / 3) : Math.max(0, (s.drive || 0) / maxDrive);
|
| 513 |
+
html += `<div class="mm-spec ${winning ? "win" : ""}">
|
| 514 |
+
<div class="top"><span class="nm"><span class="dot" style="background:${s.color}"></span>${s.name}`
|
| 515 |
+
+ (isMod ? `<span class="mod-tag">latent-only</span>` : `<span class="owns">→ ${s.owns}</span>`)
|
| 516 |
+
+ `</span><span class="val">${val == null ? "" : (isMod ? "‖z‖ " : "") + val.toFixed(2)}</span></div>
|
| 517 |
+
<div class="track"><i style="width:${(norm * 100).toFixed(0)}%;background:${s.color}"></i></div></div>`;
|
| 518 |
+
});
|
| 519 |
+
wrap.innerHTML = html;
|
| 520 |
+
// shared latent bars
|
| 521 |
+
const lat = document.getElementById("mm-latent");
|
| 522 |
+
if (lat && t.shared_latent) {
|
| 523 |
+
const mx = Math.max(0.5, ...t.shared_latent.map(Math.abs));
|
| 524 |
+
lat.innerHTML = t.shared_latent.map(v =>
|
| 525 |
+
`<i style="height:${(Math.abs(v) / mx * 100).toFixed(0)}%;opacity:${v < 0 ? .45 : 1}"></i>`).join("");
|
| 526 |
+
}
|
| 527 |
+
const flow = document.getElementById("mm-flow");
|
| 528 |
+
if (flow) {
|
| 529 |
+
const mod = t.modulation || {};
|
| 530 |
+
const cl = mod.CLEAVE || 0, ap = mod.APPROACH || 0;
|
| 531 |
+
flow.innerHTML = t.fallback
|
| 532 |
+
? `<b>offline fallback</b> — Python brain unreachable, using heuristic.`
|
| 533 |
+
: `RecursiveLink → coordinator modulation: <b>CLEAVE ${cl >= 0 ? "+" : ""}${cl.toFixed(2)}</b>, `
|
| 534 |
+
+ `APPROACH ${ap >= 0 ? "+" : ""}${ap.toFixed(2)}. Modulators (Punisher/Enrage) act only here.`;
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// ===================== LOOP =====================
|
| 539 |
+
let last = 0;
|
| 540 |
+
function loop(ts) {
|
| 541 |
+
const dt = Math.min(0.05, (ts - last) / 1000 || 0); last = ts;
|
| 542 |
+
if (game === "fight") {
|
| 543 |
+
playerUpdate(dt); bossUpdate(dt);
|
| 544 |
+
updateFacings();
|
| 545 |
+
setPlayerAnim();
|
| 546 |
+
player.anim.update(dt); boss.anim.update(dt);
|
| 547 |
+
updateHUD();
|
| 548 |
+
if (player.hp <= 0 && game === "fight") endGame(false);
|
| 549 |
+
else if (boss.hp <= 0 && game === "fight" && boss.anim.name === "death" && boss.anim.done) endGame(true);
|
| 550 |
+
else if (boss.hp <= 0 && game === "fight") { /* play death anim out */ }
|
| 551 |
+
} else {
|
| 552 |
+
player.anim.update(dt); boss.anim.update(dt);
|
| 553 |
+
}
|
| 554 |
+
render();
|
| 555 |
+
requestAnimationFrame(loop);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
function endGame(win) {
|
| 559 |
+
if (win) { game = "win"; }
|
| 560 |
+
else { game = "dead"; }
|
| 561 |
+
stopMusic();
|
| 562 |
+
// hand the fight to the online learner (it only trains on HARD-tier fights)
|
| 563 |
+
if (window.MM_LEARN && fightLog.length > 1) {
|
| 564 |
+
try { window.MM_LEARN({ difficulty, steps: fightLog, result: { bossDied: win, playerDied: !win } }); }
|
| 565 |
+
catch (e) { /* best-effort */ }
|
| 566 |
+
}
|
| 567 |
+
showOverlay(win ? "win" : "dead");
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
// ===================== OVERLAY / FLOW =====================
|
| 571 |
+
function showOverlay(kind) {
|
| 572 |
+
const ov = document.getElementById("mm-overlay");
|
| 573 |
+
const big = document.getElementById("mm-big");
|
| 574 |
+
const sub = document.getElementById("mm-sub");
|
| 575 |
+
const btn = document.getElementById("mm-start-btn");
|
| 576 |
+
ov.classList.remove("hidden");
|
| 577 |
+
if (kind === "menu") {
|
| 578 |
+
big.className = ""; big.textContent = "BOSS FIGHT";
|
| 579 |
+
sub.innerHTML = `A Modular Mind controls the <b>Demon Slime</b>. Six tiny specialists vote through a shared latent; the trained brain picks each move. You start with a brief <b style="color:#bff0ff">Aegis shield</b> — a few seconds to learn the controls before it fades. Defeat the boss.`;
|
| 580 |
+
btn.textContent = "Enter the Fog";
|
| 581 |
+
} else if (kind === "dead") {
|
| 582 |
+
big.className = "died"; big.textContent = "YOU DIED";
|
| 583 |
+
sub.innerHTML = `The Modular Mind read you. Watch the panel — punish its <b>recovery</b>, dodge the <b>red telegraph</b>.`;
|
| 584 |
+
btn.textContent = "Try Again";
|
| 585 |
+
} else {
|
| 586 |
+
big.className = "win"; big.textContent = "VICTORY";
|
| 587 |
+
sub.innerHTML = `Demon Slime felled. You out-played a brain trained by self-play reinforcement learning.`;
|
| 588 |
+
btn.textContent = "Fight Again";
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
function startFight() {
|
| 592 |
+
boss.x = 250; boss.hp = CFG.BOSS_HP; boss.state = "idle"; boss.cd = 0; boss.t = 0;
|
| 593 |
+
boss.actT = 0; boss.pending = false; boss.action = "IDLE"; boss.think = 0;
|
| 594 |
+
player.x = 650; player.hp = CFG.PLAYER_HP; player.stam = CFG.STAM_MAX; player.state = "idle"; player.t = 0;
|
| 595 |
+
player.shield = CFG.SHIELD_TIME; player.shieldFlash = 0;
|
| 596 |
+
fightLog = [];
|
| 597 |
+
boss.anim.set("idle", true); player.anim.set("idle", true);
|
| 598 |
+
document.getElementById("mm-overlay").classList.add("hidden");
|
| 599 |
+
game = "fight";
|
| 600 |
+
initAudio(); playSfx("ui", 0.4); playSfx("roar", 0.7); startMusic();
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// ===================== INPUT =====================
|
| 604 |
+
function bindInput() {
|
| 605 |
+
document.addEventListener("keydown", e => {
|
| 606 |
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " "].includes(e.key)) e.preventDefault();
|
| 607 |
+
keys[e.key] = true;
|
| 608 |
+
if (e.key === "m" || e.key === "M") toggleMute();
|
| 609 |
+
if (game !== "fight") return;
|
| 610 |
+
if (e.key === " ") playerRoll();
|
| 611 |
+
if (e.key === "j" || e.key === "J") playerAttack();
|
| 612 |
+
}, { passive: false });
|
| 613 |
+
document.addEventListener("keyup", e => { keys[e.key] = false; });
|
| 614 |
+
document.getElementById("mm-start-btn").addEventListener("click", startFight);
|
| 615 |
+
document.getElementById("mm-overlay").addEventListener("click", e => {
|
| 616 |
+
if (e.target.closest(".mm-diff")) return; // clicking the selector isn't "start"
|
| 617 |
+
if (e.target.id === "mm-start-btn") return;
|
| 618 |
+
if (game !== "fight") startFight();
|
| 619 |
+
});
|
| 620 |
+
const mute = document.getElementById("mm-mute");
|
| 621 |
+
if (mute) mute.addEventListener("click", e => { e.stopPropagation(); toggleMute(); });
|
| 622 |
+
// difficulty selector
|
| 623 |
+
document.querySelectorAll(".mm-diff-btn").forEach(btn => {
|
| 624 |
+
btn.addEventListener("click", e => {
|
| 625 |
+
e.stopPropagation();
|
| 626 |
+
difficulty = btn.dataset.diff;
|
| 627 |
+
document.querySelectorAll(".mm-diff-btn").forEach(b => b.classList.toggle("mm-diff-on", b === btn));
|
| 628 |
+
const tag = document.getElementById("mm-difftag");
|
| 629 |
+
if (tag) tag.textContent = difficulty.toUpperCase();
|
| 630 |
+
});
|
| 631 |
+
});
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
// ===================== BOOT =====================
|
| 635 |
+
async function boot() {
|
| 636 |
+
imgs.boss = await loadImg(A.boss.image);
|
| 637 |
+
imgs.knight = await loadImg(A.knight.image);
|
| 638 |
+
boss.anim = new Anim("boss"); player.anim = new Anim("knight");
|
| 639 |
+
const cv = document.getElementById("mm-canvas");
|
| 640 |
+
cv.width = W; cv.height = Hgt; window.__mmctx = cv.getContext("2d");
|
| 641 |
+
window.__mmctx.imageSmoothingEnabled = false;
|
| 642 |
+
bindInput();
|
| 643 |
+
showOverlay("menu");
|
| 644 |
+
// lightweight debug hook (handy for testing; harmless in normal play)
|
| 645 |
+
window.__mmDebug = {
|
| 646 |
+
get boss() { return boss; }, get player() { return player; },
|
| 647 |
+
get game() { return game; }, get keys() { return keys; },
|
| 648 |
+
hurtBoss: d => hitBoss(d), hurtPlayer: d => hitPlayer(d), faces: () => updateFacings(),
|
| 649 |
+
};
|
| 650 |
+
requestAnimationFrame(loop);
|
| 651 |
+
}
|
| 652 |
+
window.__mmBoot = boot;
|
| 653 |
+
})();
|
web/index.html
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="mm-root">
|
| 2 |
+
<div id="mm-stage">
|
| 3 |
+
<canvas id="mm-canvas" width="900" height="420"></canvas>
|
| 4 |
+
<button id="mm-mute" title="Mute (M)">🔊</button>
|
| 5 |
+
<div class="mm-bars">
|
| 6 |
+
<div class="mm-boss-name">⚔ Demon Slime ⚔</div>
|
| 7 |
+
<div class="mm-bar-row"><span class="mm-bar-label">Boss</span>
|
| 8 |
+
<div class="mm-bar mm-fill-boss"><i id="mm-hp-boss" style="width:100%"></i></div></div>
|
| 9 |
+
<div class="mm-bar-row"><span class="mm-bar-label">Ashen One</span>
|
| 10 |
+
<div class="mm-bar mm-fill-php"><i id="mm-hp-php" style="width:100%"></i></div></div>
|
| 11 |
+
<div class="mm-bar-row"><span class="mm-bar-label">Stamina</span>
|
| 12 |
+
<div class="mm-bar mm-fill-stam"><i id="mm-stam" style="width:100%"></i></div></div>
|
| 13 |
+
<div class="mm-bar-row" id="mm-shield-row"><span class="mm-bar-label">Aegis</span>
|
| 14 |
+
<div class="mm-bar mm-fill-shield"><i id="mm-shield" style="width:100%"></i></div></div>
|
| 15 |
+
</div>
|
| 16 |
+
<div id="mm-overlay">
|
| 17 |
+
<div id="mm-big">BOSS FIGHT</div>
|
| 18 |
+
<div id="mm-sub"></div>
|
| 19 |
+
<div id="mm-diff" class="mm-diff">
|
| 20 |
+
<span class="mm-diff-label">Boss brain:</span>
|
| 21 |
+
<button class="mm-diff-btn" data-diff="easy">Easy<small>partly trained</small></button>
|
| 22 |
+
<button class="mm-diff-btn" data-diff="normal">Normal<small>mid training</small></button>
|
| 23 |
+
<button class="mm-diff-btn mm-diff-on" data-diff="hard">Hard<small>fully trained</small></button>
|
| 24 |
+
</div>
|
| 25 |
+
<button id="mm-start-btn">Enter the Fog</button>
|
| 26 |
+
<div class="mm-keys">
|
| 27 |
+
<b>← →</b> move <b>Space</b> roll/dodge <b>J</b> attack <b>K</b> hold to block
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<div id="mm-panel">
|
| 33 |
+
<h3>Modular Mind</h3>
|
| 34 |
+
<div class="mm-tag">6 specialists · RecursiveLink shared latent · brain: <b id="mm-difftag">HARD</b></div>
|
| 35 |
+
<div class="mm-decision">
|
| 36 |
+
<div><div class="lbl">CURRENT DECISION</div><div class="act" id="mm-act">—</div></div>
|
| 37 |
+
<span class="mm-phase p1" id="mm-phase">PHASE 1</span>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="mm-sec-title">Specialist activity</div>
|
| 40 |
+
<div id="mm-specs"></div>
|
| 41 |
+
<div class="mm-sec-title">Shared latent · RecursiveLink bridge</div>
|
| 42 |
+
<div class="mm-latent" id="mm-latent"></div>
|
| 43 |
+
<div class="mm-flow" id="mm-flow">Awaiting first decision…</div>
|
| 44 |
+
<div class="mm-note">Each move is chosen by a tiny neural Modular Mind. Four specialists own an
|
| 45 |
+
action; two <b>modulators</b> (Punisher, Enrage) own none — they steer the fight only by what
|
| 46 |
+
they write into the shared latent.</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|