Nine1Eight commited on
Commit
e566f33
·
0 Parent(s):

Initial Linux build for VIL encoder

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.svg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ .venv/
7
+ venv/
8
+ .cache/
9
+ .env
README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VIL Encoder
3
+ emoji: ⚙️
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: "4.31.5"
8
+ python_version: "3.10"
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ # VIL Encoder Space
14
+
15
+ Dataset-backed VIL encoder with similarity search and glyphstring sigil render.
16
+
17
+ ## Features
18
+ - Tri-key input: visible / braille / hanzi
19
+ - Learned encoder checkpoint
20
+ - FAISS nearest-neighbor retrieval
21
+ - Deterministic glyphstring sigil render
app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List
5
+
6
+ import faiss
7
+ import gradio as gr
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+
12
+ MODEL_PATH = Path("vil-encoder-v2.pt")
13
+ DATA_PATHS = [
14
+ Path("data/train.jsonl"),
15
+ Path("data/validation.jsonl"),
16
+ Path("data/test.jsonl"),
17
+ ]
18
+ DEVICE = "cpu"
19
+ SEQ_LEN = 64
20
+ EMBED_DIM = 32
21
+
22
+ def encode_triplet(visible: str, braille: str, hanzi: str) -> np.ndarray:
23
+ text = f"{visible}|{braille}|{hanzi}"
24
+ arr = np.array([ord(c) % 256 for c in text], dtype=np.float32)
25
+ if arr.shape[0] < SEQ_LEN:
26
+ arr = np.pad(arr, (0, SEQ_LEN - arr.shape[0]))
27
+ else:
28
+ arr = arr[:SEQ_LEN]
29
+ arr /= 255.0
30
+ return arr.astype(np.float32)
31
+
32
+ class Encoder(nn.Module):
33
+ def __init__(self, input_dim: int = SEQ_LEN, embed_dim: int = EMBED_DIM) -> None:
34
+ super().__init__()
35
+ self.net = nn.Sequential(
36
+ nn.Linear(input_dim, 128),
37
+ nn.ReLU(),
38
+ nn.Linear(128, 64),
39
+ nn.ReLU(),
40
+ nn.Linear(64, embed_dim),
41
+ )
42
+
43
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
44
+ z = self.net(x)
45
+ return nn.functional.normalize(z, dim=-1)
46
+
47
+ def load_dataset() -> List[Dict[str, Any]]:
48
+ rows: List[Dict[str, Any]] = []
49
+ for p in DATA_PATHS:
50
+ if p.exists():
51
+ with p.open("r", encoding="utf-8") as f:
52
+ for line in f:
53
+ line = line.strip()
54
+ if line:
55
+ rows.append(json.loads(line))
56
+ return rows
57
+
58
+ def load_model() -> tuple[Encoder, Dict[str, Any]]:
59
+ model = Encoder()
60
+ status: Dict[str, Any] = {
61
+ "loaded": False,
62
+ "model_path": str(MODEL_PATH),
63
+ "error": None,
64
+ }
65
+
66
+ if not MODEL_PATH.exists():
67
+ status["error"] = f"missing model: {MODEL_PATH}"
68
+ return model.eval(), status
69
+
70
+ try:
71
+ obj = torch.load(MODEL_PATH, map_location=DEVICE)
72
+ if isinstance(obj, dict) and "model_state_dict" in obj:
73
+ model.load_state_dict(obj["model_state_dict"], strict=True)
74
+ elif isinstance(obj, dict):
75
+ model.load_state_dict(obj, strict=False)
76
+ else:
77
+ raise RuntimeError(f"unsupported checkpoint type: {type(obj).__name__}")
78
+ model.eval()
79
+ status["loaded"] = True
80
+ return model, status
81
+ except Exception as e:
82
+ status["error"] = str(e)
83
+ model.eval()
84
+ return model, status
85
+
86
+ DATASET = load_dataset()
87
+ MODEL, MODEL_STATUS = load_model()
88
+
89
+ INDEX = faiss.IndexFlatL2(EMBED_DIM)
90
+ EMBED_MATRIX = None
91
+
92
+ def model_embed(v: str, b: str, h: str) -> np.ndarray:
93
+ vec = encode_triplet(v, b, h)
94
+ x = torch.from_numpy(vec).unsqueeze(0)
95
+ with torch.no_grad():
96
+ z = MODEL(x).cpu().numpy()[0]
97
+ return z.astype(np.float32)
98
+
99
+ def build_index() -> None:
100
+ global EMBED_MATRIX
101
+ if not DATASET or not MODEL_STATUS["loaded"]:
102
+ EMBED_MATRIX = np.zeros((0, EMBED_DIM), dtype=np.float32)
103
+ return
104
+ vectors = []
105
+ for row in DATASET:
106
+ vectors.append(model_embed(row["visible"], row["braille"], row["hanzi"]))
107
+ EMBED_MATRIX = np.stack(vectors).astype(np.float32)
108
+ INDEX.add(EMBED_MATRIX)
109
+
110
+ build_index()
111
+
112
+ def render_sigil(v: str, b: str, h: str) -> str:
113
+ glyphstring = f"{v}{b}{h}"
114
+ locked = f"⊏⚙{glyphstring}⚙⊐"
115
+ svg = f"""
116
+ <svg width="320" height="200" xmlns="http://www.w3.org/2000/svg">
117
+ <rect width="100%" height="100%" fill="black"/>
118
+ <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
119
+ fill="white" font-size="36" font-family="monospace">{locked}</text>
120
+ </svg>
121
+ """
122
+ return svg
123
+
124
+ def nearest(v: str, b: str, h: str, k: int = 5) -> List[Dict[str, Any]]:
125
+ if not DATASET or not MODEL_STATUS["loaded"] or INDEX.ntotal == 0:
126
+ return []
127
+ q = model_embed(v, b, h).reshape(1, -1)
128
+ distances, indices = INDEX.search(q, k)
129
+ out: List[Dict[str, Any]] = []
130
+ for dist, idx in zip(distances[0].tolist(), indices[0].tolist()):
131
+ if idx < 0 or idx >= len(DATASET):
132
+ continue
133
+ row = dict(DATASET[idx])
134
+ row["_distance"] = float(dist)
135
+ out.append(row)
136
+ return out
137
+
138
+ def run_pipeline(visible: str, braille: str, hanzi: str):
139
+ visible = (visible or "").strip()
140
+ braille = (braille or "").strip()
141
+ hanzi = (hanzi or "").strip()
142
+
143
+ if not visible or not braille or not hanzi:
144
+ return {"error": "Provide visible, braille, and hanzi."}, ""
145
+
146
+ if not MODEL_STATUS["loaded"]:
147
+ return {"error": "Model not loaded.", "model_status": MODEL_STATUS}, ""
148
+
149
+ embedding = model_embed(visible, braille, hanzi).tolist()
150
+ matches = nearest(visible, braille, hanzi, k=5)
151
+ svg = render_sigil(visible, braille, hanzi)
152
+
153
+ result = {
154
+ "input": {
155
+ "visible": visible,
156
+ "braille": braille,
157
+ "hanzi": hanzi,
158
+ },
159
+ "embedding": embedding,
160
+ "nearest": matches,
161
+ "glyphstring": f"{visible}{braille}{hanzi}",
162
+ "sigil": f"⊏⚙{visible}{braille}{hanzi}⚙⊐",
163
+ }
164
+ return result, svg
165
+
166
+ def search_visible(query: str):
167
+ query = (query or "").strip()
168
+ if not query:
169
+ return []
170
+ return [row for row in DATASET if query in str(row.get("visible", ""))][:10]
171
+
172
+ with gr.Blocks(title="VIL Encoder — Glyphmatic Inference Engine") as demo:
173
+ gr.Markdown("# VIL Encoder — Glyphmatic Inference Engine")
174
+
175
+ with gr.Tab("Encode"):
176
+ visible = gr.Textbox(label="Visible Canon", placeholder="✶")
177
+ braille = gr.Textbox(label="Invisible Braille", placeholder="⠁")
178
+ hanzi = gr.Textbox(label="Hanzi Context", placeholder="一")
179
+ run_btn = gr.Button("Run")
180
+ result_json = gr.JSON()
181
+ sigil_svg = gr.HTML()
182
+
183
+ run_btn.click(
184
+ fn=run_pipeline,
185
+ inputs=[visible, braille, hanzi],
186
+ outputs=[result_json, sigil_svg],
187
+ )
188
+
189
+ with gr.Tab("Search Dataset"):
190
+ query = gr.Textbox(label="Query Visible", placeholder="✶")
191
+ query_btn = gr.Button("Search")
192
+ query_out = gr.JSON()
193
+ query_btn.click(fn=search_visible, inputs=[query], outputs=[query_out])
194
+
195
+ with gr.Tab("System Info"):
196
+ gr.JSON(
197
+ {
198
+ "device": DEVICE,
199
+ "model_status": MODEL_STATUS,
200
+ "dataset_rows": len(DATASET),
201
+ "index_size": int(INDEX.ntotal),
202
+ }
203
+ )
204
+
205
+ if __name__ == "__main__":
206
+ demo.launch(server_name="0.0.0.0", server_port=7860)
data/test.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
data/train.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
data/validation.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio==4.31.5
2
+ huggingface_hub==0.23.0
3
+ torch==2.3.1
4
+ numpy==1.26.4
5
+ pandas==2.2.2
6
+ faiss-cpu==1.8.0
train_vil_encoder_v2.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import json
3
+ from pathlib import Path
4
+ from typing import List, Tuple
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.optim as optim
10
+ from torch.utils.data import Dataset, DataLoader
11
+
12
+ TRAIN_PATH = Path("data/train.jsonl")
13
+ MODEL_OUT = Path("vil-encoder-v2.pt")
14
+
15
+ SEQ_LEN = 64
16
+ EMBED_DIM = 32
17
+ BATCH_SIZE = 128
18
+ EPOCHS = 12
19
+ LR = 1e-3
20
+ WEIGHT_DECAY = 1e-5
21
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
22
+ SEED = 918
23
+
24
+ torch.manual_seed(SEED)
25
+ np.random.seed(SEED)
26
+
27
+ def encode_triplet(visible: str, braille: str, hanzi: str) -> np.ndarray:
28
+ text = f"{visible}|{braille}|{hanzi}"
29
+ arr = np.array([ord(c) % 256 for c in text], dtype=np.float32)
30
+ if arr.shape[0] < SEQ_LEN:
31
+ arr = np.pad(arr, (0, SEQ_LEN - arr.shape[0]))
32
+ else:
33
+ arr = arr[:SEQ_LEN]
34
+ arr /= 255.0
35
+ return arr
36
+
37
+ def load_rows(path: Path) -> List[dict]:
38
+ rows: List[dict] = []
39
+ with path.open("r", encoding="utf-8") as f:
40
+ for line in f:
41
+ line = line.strip()
42
+ if line:
43
+ rows.append(json.loads(line))
44
+ if not rows:
45
+ raise RuntimeError(f"No rows loaded from {path}")
46
+ return rows
47
+
48
+ class PairDataset(Dataset):
49
+ def __init__(self, rows: List[dict]) -> None:
50
+ self.rows = rows
51
+ self.inputs = np.stack([
52
+ encode_triplet(r["visible"], r["braille"], r["hanzi"]) for r in rows
53
+ ]).astype(np.float32)
54
+
55
+ def __len__(self) -> int:
56
+ return len(self.rows)
57
+
58
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
59
+ anchor = self.inputs[idx]
60
+ pos_idx = (idx + 1) % len(self.inputs)
61
+ positive = self.inputs[pos_idx]
62
+ return torch.from_numpy(anchor), torch.from_numpy(positive)
63
+
64
+ class Encoder(nn.Module):
65
+ def __init__(self, input_dim: int = SEQ_LEN, embed_dim: int = EMBED_DIM) -> None:
66
+ super().__init__()
67
+ self.net = nn.Sequential(
68
+ nn.Linear(input_dim, 128),
69
+ nn.ReLU(),
70
+ nn.Linear(128, 64),
71
+ nn.ReLU(),
72
+ nn.Linear(64, embed_dim),
73
+ )
74
+
75
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
76
+ z = self.net(x)
77
+ return nn.functional.normalize(z, dim=-1)
78
+
79
+ def cosine_pull_loss(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
80
+ return 1.0 - nn.functional.cosine_similarity(a, b).mean()
81
+
82
+ def main() -> None:
83
+ rows = load_rows(TRAIN_PATH)
84
+ dataset = PairDataset(rows)
85
+ loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=False)
86
+
87
+ model = Encoder().to(DEVICE)
88
+ optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)
89
+
90
+ best_loss = float("inf")
91
+ history = []
92
+
93
+ for epoch in range(EPOCHS):
94
+ model.train()
95
+ running = 0.0
96
+ batches = 0
97
+
98
+ for x1, x2 in loader:
99
+ x1 = x1.to(DEVICE)
100
+ x2 = x2.to(DEVICE)
101
+
102
+ z1 = model(x1)
103
+ z2 = model(x2)
104
+
105
+ loss = cosine_pull_loss(z1, z2)
106
+
107
+ optimizer.zero_grad(set_to_none=True)
108
+ loss.backward()
109
+ optimizer.step()
110
+
111
+ running += float(loss.item())
112
+ batches += 1
113
+
114
+ epoch_loss = running / max(1, batches)
115
+ history.append(epoch_loss)
116
+ print(f"epoch={epoch:02d} loss={epoch_loss:.6f}")
117
+
118
+ if epoch_loss < best_loss:
119
+ best_loss = epoch_loss
120
+ checkpoint = {
121
+ "model_state_dict": model.state_dict(),
122
+ "config": {
123
+ "input_dim": SEQ_LEN,
124
+ "embed_dim": EMBED_DIM,
125
+ },
126
+ "history": history,
127
+ }
128
+ torch.save(checkpoint, MODEL_OUT)
129
+
130
+ print(f"saved={MODEL_OUT} best_loss={best_loss:.6f}")
131
+
132
+ if __name__ == "__main__":
133
+ main()
vil-encoder-v2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e193779c9a2db196bc71bdd470cc22b5a8694d2fcd294fc7a986762cfd6fe6d9
3
+ size 77646
vil_dataset_builder.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import hashlib
4
+ import random
5
+ from pathlib import Path
6
+
7
+ OUT_DIR = Path("data")
8
+ SEED = 918
9
+ random.seed(SEED)
10
+
11
+ TRAIN_SIZE = 5000
12
+ VAL_SIZE = 1000
13
+ TEST_SIZE = 1000
14
+
15
+ VISIBLE_GLYPHS = [
16
+ "✶","✷","✸","✹","✺","✻","✼","✽",
17
+ "✾","✿","❀","❁","❂","❃","❄","❅"
18
+ ]
19
+
20
+ BRAILLE_STATES = [
21
+ "⠁","⠃","⠇","⠏","⠟","⠿","⡇","⡿",
22
+ "⡟","⡯","⡷","⡻","⠻","⠽","⠷","⢿"
23
+ ]
24
+
25
+ HANZI_CONTEXT = [
26
+ "一","二","三","四","五","六","七","八",
27
+ "九","十","百","千","万","亿","兆","世"
28
+ ]
29
+
30
+ def compute_digest(visible: str, braille: str, hanzi: str) -> str:
31
+ payload = f"{visible}|{braille}|{hanzi}".encode("utf-8")
32
+ return hashlib.sha3_256(payload).hexdigest()
33
+
34
+ def semantic_weight(visible: str, braille: str, hanzi: str) -> float:
35
+ v = VISIBLE_GLYPHS.index(visible) / max(1, len(VISIBLE_GLYPHS) - 1)
36
+ b = BRAILLE_STATES.index(braille) / max(1, len(BRAILLE_STATES) - 1)
37
+ h = HANZI_CONTEXT.index(hanzi) / max(1, len(HANZI_CONTEXT) - 1)
38
+ return round(0.4 * v + 0.3 * b + 0.3 * h, 6)
39
+
40
+ def generate_row(idx: int) -> dict:
41
+ visible = random.choice(VISIBLE_GLYPHS)
42
+ braille = random.choice(BRAILLE_STATES)
43
+ hanzi = random.choice(HANZI_CONTEXT)
44
+ return {
45
+ "glyph_id": f"glyph_{idx:08d}",
46
+ "visible": visible,
47
+ "braille": braille,
48
+ "hanzi": hanzi,
49
+ "semantic_weight": semantic_weight(visible, braille, hanzi),
50
+ "digest": compute_digest(visible, braille, hanzi),
51
+ "tri_key": {
52
+ "visible_layer": visible,
53
+ "state_layer": braille,
54
+ "context_layer": hanzi,
55
+ },
56
+ }
57
+
58
+ def write_split(path: Path, start: int, size: int) -> None:
59
+ with path.open("w", encoding="utf-8") as f:
60
+ for i in range(size):
61
+ row = generate_row(start + i)
62
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
63
+
64
+ def main() -> None:
65
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
66
+ write_split(OUT_DIR / "train.jsonl", 0, TRAIN_SIZE)
67
+ write_split(OUT_DIR / "validation.jsonl", TRAIN_SIZE, VAL_SIZE)
68
+ write_split(OUT_DIR / "test.jsonl", TRAIN_SIZE + VAL_SIZE, TEST_SIZE)
69
+ print("Built dataset at ./data")
70
+
71
+ if __name__ == "__main__":
72
+ main()