VISHAL18for4 commited on
Commit
bb3fbc2
·
verified ·
1 Parent(s): b8e7429

Upload 6 files

Browse files
Files changed (6) hide show
  1. app.py +724 -0
  2. chatbot.py +160 -0
  3. data_fetcher.py +317 -0
  4. image_model.py +363 -0
  5. neural_network.py +439 -0
  6. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import threading, time, json
3
+ from datetime import datetime, UTC
4
+ from collections import deque
5
+ from pathlib import Path
6
+
7
+ from neural_network import LivingNetwork, CATEGORIES as TEXT_CATS, KNOWLEDGE_FILE
8
+ from data_fetcher import DataFetcher
9
+ from image_fetcher import ImageFetcher, IMAGE_DIR, CATEGORIES as IMG_CATS
10
+ from image_model import LivingImageNetwork
11
+ from chatbot import RAGChatbot
12
+
13
+ # ── globals ──────────────────────────────────────────────────────────────────
14
+ network = LivingNetwork()
15
+ fetcher = DataFetcher()
16
+ img_fetcher = ImageFetcher()
17
+ img_network = LivingImageNetwork()
18
+ chatbot = RAGChatbot()
19
+
20
+ app_log = deque(maxlen=300)
21
+ is_alive = False
22
+ img_alive = False
23
+
24
+ def log(msg):
25
+ app_log.appendleft(f"[{datetime.now(UTC).strftime('%H:%M:%S')}] {msg}")
26
+
27
+ log("🧠 Text network ready")
28
+ log("👁️ Image CNN ready")
29
+ kf = network.get_knowledge_file_stats()
30
+ if kf['exists']: log(f"📚 Restored {kf['lines']} articles from knowledge.jsonl")
31
+ if network.epoch: log(f"✅ Text model: epoch {network.epoch:,}")
32
+ if img_network.epoch: log(f"✅ Image CNN: epoch {img_network.epoch:,}")
33
+
34
+ # ── background loops ─────────────────────────────────────────────────────────
35
+ def text_loop():
36
+ global is_alive
37
+ lf = ls = 0
38
+ log("🚀 Text network LIVE")
39
+ while is_alive:
40
+ now = time.time()
41
+ if now - lf >= 40:
42
+ try:
43
+ items = fetcher.fetch_round()
44
+ for it in items:
45
+ network.ingest(it['text'], it['category'], source=it.get('source','web'))
46
+ kf2 = network.get_knowledge_file_stats()
47
+ log(f"📡 +{len(items)} articles | knowledge: {kf2['lines']}")
48
+ lf = now
49
+ except Exception as e:
50
+ log(f"⚠ fetch: {str(e)[:50]}")
51
+ if network.stats['buffer_size'] >= 32 and network.vocab.is_built:
52
+ r = network.train_n_steps(80)
53
+ if r['steps']:
54
+ s = network.stats
55
+ log(f"📝 Epoch {s['epoch']:,} loss {s['loss']} acc {s['accuracy']}%")
56
+ else:
57
+ log(f"⏳ need {max(0,32-network.stats['buffer_size'])} more items")
58
+ if now - ls >= 180:
59
+ network.save_checkpoint(); log("💾 saved"); ls = now
60
+ time.sleep(2)
61
+ log("⏹ text stopped")
62
+
63
+ def image_loop():
64
+ global img_alive
65
+ lf = ls = 0
66
+ log("🚀 Image CNN LIVE")
67
+ while img_alive:
68
+ now = time.time()
69
+ if now - lf >= 60:
70
+ try:
71
+ r = img_fetcher.fetch_round()
72
+ log(f"🖼️ +{r['downloaded']} images | total {r['total']}")
73
+ lf = now
74
+ except Exception as e:
75
+ log(f"⚠ img fetch: {str(e)[:50]}")
76
+ if img_fetcher.get_stats()['total'] >= 16:
77
+ r = img_network.train_n_steps(IMAGE_DIR, 20)
78
+ if r['steps']:
79
+ s = img_network.stats
80
+ log(f"👁️ CNN epoch {s['epoch']:,} loss {s['loss']} acc {s['accuracy']}%")
81
+ else:
82
+ log(f"⏳ img CNN needs {max(0,16-img_fetcher.get_stats()['total'])} more images")
83
+ if now - ls >= 180:
84
+ img_network._save_checkpoint(); log("💾 img saved"); ls = now
85
+ time.sleep(3)
86
+ log("⏹ image stopped")
87
+
88
+ # ── controls ──────────────────────────────────────────────────────────────────
89
+ def start_text():
90
+ global is_alive
91
+ if is_alive: return "⚠ already running"
92
+ is_alive = True
93
+ threading.Thread(target=text_loop, daemon=True).start()
94
+ return "🟢 text network LIVE"
95
+
96
+ def stop_text():
97
+ global is_alive; is_alive = False
98
+ network.save_checkpoint(); return "🔴 stopped + saved"
99
+
100
+ def start_img():
101
+ global img_alive
102
+ if img_alive: return "⚠ already running"
103
+ img_alive = True
104
+ threading.Thread(target=image_loop, daemon=True).start()
105
+ return "🟢 image CNN LIVE"
106
+
107
+ def stop_img():
108
+ global img_alive; img_alive = False
109
+ img_network._save_checkpoint(); return "🔴 stopped + saved"
110
+
111
+ def do_fetch_text():
112
+ items = fetcher.fetch_round()
113
+ for it in items: network.ingest(it['text'], it['category'], source=it.get('source','web'))
114
+ return f"✅ +{len(items)} articles"
115
+
116
+ def do_fetch_img():
117
+ r = img_fetcher.fetch_round()
118
+ return f"✅ +{r['downloaded']} images"
119
+
120
+ def do_train_text(n):
121
+ r = network.train_n_steps(int(n))
122
+ return f"✅ {r['steps']} steps · loss {r['avg_loss']}" if r['steps'] else "⚠ need more data"
123
+
124
+ def do_train_img(n):
125
+ r = img_network.train_n_steps(IMAGE_DIR, int(n))
126
+ return f"✅ {r['steps']} steps · loss {r['avg_loss']}" if r['steps'] else "⚠ need more images"
127
+
128
+ # ── stats fns ─────────────────────────────────────────────────────────────────
129
+ def status_bar():
130
+ s = network.stats; si = img_network.stats
131
+ kf2 = network.get_knowledge_file_stats(); ig = img_fetcher.get_stats()
132
+ return (f"{'🟢' if is_alive else '🔴'} TEXT ep{s['epoch']:,} loss{s['loss']} acc{s['accuracy']}% | "
133
+ f"{'🟢' if img_alive else '🔴'} CNN ep{si['epoch']:,} loss{si['loss']} acc{si['accuracy']}% | "
134
+ f"📚{kf2['lines']} articles 🖼️{ig['total']} images")
135
+
136
+ def text_stats():
137
+ s=network.stats; fs=fetcher.get_stats(); cc=network.category_counts
138
+ hist=network.loss_history[-30:]
139
+ spark=''.join(' ▁▂▃▄▅▆▇█'[min(8,int(((v-min(hist))/(max(hist)-min(hist)+1e-9))*8))] for v in hist) if len(hist)>=2 else '…'
140
+ rows='\n'.join(f"|{c}|{cc.get(c,0)}|" for c in TEXT_CATS)
141
+ return f"""### 📝 Text Network
142
+ |Metric|Value|
143
+ |---|---|
144
+ |Epoch|{s['epoch']:,}|Loss|{s['loss']}|
145
+ |Accuracy|{s['accuracy']}%|Samples|{s['total_samples']:,}|
146
+ |Knowledge|{s['knowledge_count']} articles|Vocab|{s['vocab_size']:,}|
147
+ |LR|{s['lr']}|Buffer|{s['buffer_size']}|
148
+
149
+ Loss `{spark}`
150
+
151
+ ### Sources — {fs['total_fetched']} total
152
+ RSS·{fs['sources']['rss']} Reddit·{fs['sources']['reddit']} Wiki·{fs['sources']['wikipedia']} HN·{fs['sources']['hackernews']}
153
+
154
+ ### Categories
155
+ |Cat|Count|
156
+ |---|---|
157
+ {rows}
158
+ > _{s['last_text']}_"""
159
+
160
+ def img_stats():
161
+ s=img_network.stats; ifs=img_fetcher.get_stats()
162
+ hist=img_network.loss_history[-30:]
163
+ spark=''.join(' ▁▂▃▄▅▆▇█'[min(8,int(((v-min(hist))/(max(hist)-min(hist)+1e-9))*8))] for v in hist) if len(hist)>=2 else '…'
164
+ rows='\n'.join(f"|{c}|{ifs['by_category'].get(c,0)}|" for c in IMG_CATS)
165
+ return f"""### 👁️ Image CNN
166
+ |Metric|Value|
167
+ |---|---|
168
+ |Epoch|{s['epoch']:,}|Loss|{s['loss']}|
169
+ |Accuracy|{s['accuracy']}%|Images|{s['total_images']:,}|
170
+ |On Disk|{ifs['total']}|Disk|{ifs['disk_mb']} MB|
171
+
172
+ Loss `{spark}`
173
+
174
+ ### By Category
175
+ |Cat|Count|
176
+ |---|---|
177
+ {rows}
178
+
179
+ **What each block learns:**
180
+ Block 1 → edges & colours · Block 2 → shapes & textures · Block 3 → objects & parts"""
181
+
182
+ def get_log():
183
+ lines = list(app_log)[:80]
184
+ def color(line):
185
+ if '✅' in line or '🟢' in line: c = '#4ade80'
186
+ elif '⚠' in line or '🔴' in line or 'error' in line.lower(): c = '#f87171'
187
+ elif '🚀' in line or '🤖' in line: c = '#818cf8'
188
+ elif '📰' in line or '🦆' in line or '🟠' in line: c = '#38bdf8'
189
+ elif '💻' in line or '📖' in line: c = '#fb923c'
190
+ elif '🔥' in line or 'loss' in line.lower(): c = '#facc15'
191
+ else: c = '#94a3b8'
192
+ return f'<div style="color:{c};font-family:monospace;font-size:12px;padding:1px 0">{line}</div>'
193
+ rows = ''.join(color(l) for l in lines) or '<div style="color:#64748b;font-style:italic">No logs yet...</div>'
194
+ return f'<div style="background:#0a0f1e;padding:12px;border-radius:8px;max-height:420px;overflow-y:auto;border:1px solid #1e293b">{rows}</div>'
195
+ def get_feed():
196
+ items=fetcher.get_recent_items(10)
197
+ if not items: return "_No data yet_"
198
+ return '\n\n---\n\n'.join(f"**[{i['source'].upper()}]** `{i['category'].upper()}`\n{i['text'][:130]}…" for i in items)
199
+
200
+ def get_knowledge():
201
+ kf2=network.get_knowledge_file_stats(); items=network.get_recent_knowledge(20)
202
+ if not kf2['exists'] or not items: return "### 📭 Empty — start text network"
203
+ rows=[]
204
+ for it in items:
205
+ rows.append(f"**`{it.get('category','?').upper()}`** `{it.get('source','?')}` `{it.get('timestamp','')[:10]}`\n> {it.get('text','')[:130]}…")
206
+ return f"### 📚 {kf2['lines']} articles · {kf2['size_kb']} KB\n\n---\n\n"+'\n\n---\n\n'.join(rows)
207
+
208
+ def predict_text(txt):
209
+ if not txt.strip(): return "Enter text."
210
+ r=network.predict(txt)
211
+ if 'error' in r: return f"⚠ {r['error']}"
212
+ bars=''.join(f"`{c:<15}` {'█'*int(p/5)}{'░'*(20-int(p/5))} **{p:.1f}%**\n\n" for c,p in sorted(r['all_probs'].items(),key=lambda x:-x[1]))
213
+ return f"## → {r['prediction'].upper()}\n**{r['confidence']}% confidence**\n\n{bars}"
214
+
215
+ def predict_img(path):
216
+ if not path: return "Upload image."
217
+ r=img_network.predict_image(path)
218
+ if 'error' in r: return f"⚠ {r['error']}"
219
+ bars=''.join(f"`{c:<15}` {'█'*int(p/5)}{'░'*(20-int(p/5))} **{p:.1f}%**\n\n" for c,p in sorted(r['all_probs'].items(),key=lambda x:-x[1]))
220
+ return f"## → {r['prediction'].upper()}\n**{r['confidence']}% confidence**\n\n{bars}"
221
+
222
+ def chat_fn(msg, history):
223
+ new_history = chatbot.chat(msg, history or [])
224
+ return new_history, "" # also clear the input box
225
+
226
+ # ── 3D VIZ — fully animated with signal pulses ────────────────────────────────
227
+ def build_viz(state_json: str) -> str:
228
+ return """<!DOCTYPE html><html><head><meta charset="UTF-8">
229
+ <style>
230
+ *{margin:0;padding:0;box-sizing:border-box}
231
+ body{background:#030610;overflow:hidden;font-family:monospace}
232
+ #c{width:100vw;height:100vh;display:block}
233
+ #hud{position:fixed;top:12px;left:14px;color:#00f5c4;font-size:11px;line-height:2;pointer-events:none;text-shadow:0 0 8px #00f5c466}
234
+ #tip{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);font-size:10px;color:#2a3a5a}
235
+ </style></head><body>
236
+ <canvas id="c"></canvas>
237
+ <div id="hud">
238
+ <div id="h1">■ LOADING...</div>
239
+ <div id="h2"></div><div id="h3"></div><div id="h4"></div>
240
+ </div>
241
+ <div id="tip">drag to rotate · scroll to zoom · signals travel left→right every 800ms</div>
242
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
243
+ <script>
244
+ // ── initial state from Python ──
245
+ const INIT = """ + state_json + """;
246
+
247
+ // ── renderer ──
248
+ const canvas = document.getElementById('c');
249
+ const renderer = new THREE.WebGLRenderer({canvas, antialias:true, alpha:false});
250
+ renderer.setPixelRatio(Math.min(devicePixelRatio,2));
251
+ renderer.setSize(innerWidth, innerHeight);
252
+ renderer.shadowMap.enabled = true;
253
+
254
+ const scene = new THREE.Scene();
255
+ scene.background = new THREE.Color(0x030610);
256
+ scene.fog = new THREE.FogExp2(0x030610, 0.010);
257
+
258
+ const camera = new THREE.PerspectiveCamera(52, innerWidth/innerHeight, 0.1, 600);
259
+ camera.position.set(0,1,26);
260
+
261
+ // ── lights ──
262
+ scene.add(new THREE.AmbientLight(0x0a1020, 4));
263
+ const kl = new THREE.PointLight(0x00f5c4, 8, 80); kl.position.set(0,10,8); scene.add(kl);
264
+ const bl = new THREE.PointLight(0x5b6fff, 4, 50); bl.position.set(-12,-6,4); scene.add(bl);
265
+ const rl = new THREE.PointLight(0xff6b35, 2, 40); rl.position.set(12,-4,-2); scene.add(rl);
266
+
267
+ // ── orbit ──
268
+ let rX=0.18, rY=0, zoom=26, drag=false, last={x:0,y:0};
269
+ canvas.addEventListener('mousedown', e=>{drag=true;last={x:e.clientX,y:e.clientY}});
270
+ window.addEventListener('mouseup', ()=>drag=false);
271
+ window.addEventListener('mousemove', e=>{
272
+ if(!drag)return;
273
+ rY += (e.clientX-last.x)*0.011;
274
+ rX += (e.clientY-last.y)*0.007;
275
+ rX = Math.max(-1.1, Math.min(1.1, rX));
276
+ last = {x:e.clientX, y:e.clientY};
277
+ });
278
+ canvas.addEventListener('wheel', e=>{zoom=Math.max(7,Math.min(45,zoom+e.deltaY*0.025));e.preventDefault();},{passive:false});
279
+ let lt=null;
280
+ canvas.addEventListener('touchstart',e=>{lt=e.touches[0];},{passive:true});
281
+ canvas.addEventListener('touchmove',e=>{
282
+ if(!lt)return; const t=e.touches[0];
283
+ rY+=(t.clientX-lt.clientX)*0.011; rX+=(t.clientY-lt.clientY)*0.007;
284
+ lt=t; e.preventDefault();
285
+ },{passive:false});
286
+
287
+ // ── starfield ──
288
+ const sg=new THREE.BufferGeometry();
289
+ const sp=new Float32Array(600*3);
290
+ for(let i=0;i<sp.length;i++) sp[i]=(Math.random()-0.5)*120;
291
+ sg.setAttribute('position',new THREE.BufferAttribute(sp,3));
292
+ scene.add(new THREE.Points(sg, new THREE.PointsMaterial({color:0x0d1a33,size:0.06})));
293
+
294
+ // ── build network from state ──
295
+ const group = new THREE.Group();
296
+ scene.add(group);
297
+
298
+ // ── DYNAMIC BRAIN SIZE — grows as the AI learns more ──────────────────────────
299
+ const rawSizes = INIT.layer_sizes || [64,256,128,64,8];
300
+ const kc = (INIT.stats && (INIT.stats.knowledge_count || INIT.stats.total_images)) || 0;
301
+ // Scale: 0 articles = 1 neuron, 10=2, 30=3, 60=4, 100=5, 200=7, 500=10, 1000+=12
302
+ const MAX_N = kc===0 ? 1 : kc<5 ? 2 : kc<15 ? 3 : kc<40 ? 4 : kc<80 ? 5 : kc<150 ? 6 : kc<300 ? 8 : kc<600 ? 10 : 12;
303
+ // Scale all layer sizes proportionally
304
+ const scaleFactor = MAX_N / 12;
305
+ const sizes = rawSizes.map((s,i) => Math.max(1, Math.round(s * scaleFactor)));
306
+ const nL = sizes.length;
307
+ const LGAP = 5.8;
308
+ const startX = -(nL-1)*LGAP/2;
309
+ const acts = INIT.activations || {};
310
+ const aKeys = Object.keys(acts);
311
+
312
+ function getAct(li, ni){
313
+ const k=aKeys[li]; if(!k) return Math.random()*0.4+0.1;
314
+ const a=acts[k];
315
+ return (a&&a[ni]!=null) ? Math.max(0,Math.min(1,a[ni])) : Math.random()*0.3;
316
+ }
317
+
318
+ // Shared geometries
319
+ const nGeo = new THREE.SphereGeometry(0.22, 18, 18);
320
+ const gGeo = new THREE.SphereGeometry(0.34, 8, 8);
321
+
322
+ const LAYER_NAMES = ['INPUT','HIDDEN-1','HIDDEN-2','HIDDEN-3','OUTPUT',
323
+ 'CONV-1','CONV-2','FC-1','FC-2'];
324
+
325
+ // Store neuron meshes for animation
326
+ const neurons = []; // neurons[layerIdx] = [{mesh, glowMesh, baseAct, fireTimer, pos}]
327
+ const connections = []; // {from, to, line, fromIdx, toIdx, lFromIdx, lToIdx}
328
+
329
+ for(let li=0;li<nL;li++){
330
+ const show = Math.min(sizes[li], MAX_N);
331
+ const yGap = Math.min(1.9, 11/show);
332
+ const yStart= -(show-1)*yGap/2;
333
+ const layer = [];
334
+
335
+ for(let ni=0;ni<show;ni++){
336
+ const act = getAct(li,ni);
337
+ const x = startX + li*LGAP;
338
+ const y = yStart + ni*yGap;
339
+ const z = (Math.random()-0.5)*0.5;
340
+
341
+ // Neuron colour based on activation
342
+ const r=Math.floor(act*240), g=Math.floor(act*200), b=Math.floor(60+act*100);
343
+ const col = new THREE.Color(`rgb(${r},${g},${b})`);
344
+
345
+ const mat = new THREE.MeshStandardMaterial({
346
+ color: col, emissive: col,
347
+ emissiveIntensity: 0.3+act*1.8,
348
+ roughness:0.2, metalness:0.9,
349
+ });
350
+ const mesh = new THREE.Mesh(nGeo, mat);
351
+ mesh.position.set(x,y,z);
352
+ group.add(mesh);
353
+
354
+ // Glow halo
355
+ const gMat = new THREE.MeshBasicMaterial({
356
+ color:0x00f5c4, transparent:true,
357
+ opacity: act>0.4?(act-0.4)*0.5:0,
358
+ wireframe:true
359
+ });
360
+ const gMesh = new THREE.Mesh(gGeo, gMat);
361
+ gMesh.position.set(x,y,z);
362
+ group.add(gMesh);
363
+
364
+ layer.push({mesh, gMesh, baseAct:act, fireTimer:0, pos:new THREE.Vector3(x,y,z)});
365
+ }
366
+
367
+ // Layer label
368
+ const lc=document.createElement('canvas');
369
+ lc.width=256; lc.height=40;
370
+ const lx=lc.getContext('2d');
371
+ lx.fillStyle='rgba(0,245,196,0.5)';
372
+ lx.font='bold 18px monospace'; lx.textAlign='center';
373
+ lx.fillText(LAYER_NAMES[li]||`L${li}`, 128, 28);
374
+ const lbl = new THREE.Mesh(
375
+ new THREE.PlaneGeometry(2.6,0.45),
376
+ new THREE.MeshBasicMaterial({map:new THREE.CanvasTexture(lc),transparent:true,side:THREE.DoubleSide})
377
+ );
378
+ lbl.position.set(startX+li*LGAP, yStart-2.0, 0);
379
+ group.add(lbl);
380
+ neurons.push(layer);
381
+ }
382
+
383
+ // Build connections between adjacent layers
384
+ let connDrawn = 0;
385
+ const MAX_CONN = 160;
386
+ for(let li=0;li<nL-1&&connDrawn<MAX_CONN;li++){
387
+ const fromL=neurons[li], toL=neurons[li+1];
388
+ for(let fi=0;fi<fromL.length&&connDrawn<MAX_CONN;fi++){
389
+ for(let ti=0;ti<toL.length&&connDrawn<MAX_CONN;ti++){
390
+ const sig=(fromL[fi].baseAct+toL[ti].baseAct)/2;
391
+ if(sig<0.05&&Math.random()>0.3) continue;
392
+ const positive=Math.random()>0.35;
393
+ const col=positive?0x00f5c4:0xff6b35;
394
+ const pts=[fromL[fi].pos.clone(), toL[ti].pos.clone()];
395
+ const geo=new THREE.BufferGeometry().setFromPoints(pts);
396
+ const mat=new THREE.LineBasicMaterial({color:col,transparent:true,opacity:0.04+sig*0.35});
397
+ const line=new THREE.Line(geo,mat);
398
+ group.add(line);
399
+ connections.push({from:fromL[fi],to:toL[ti],line,mat,lFromIdx:li,lToIdx:li+1,fi,ti});
400
+ connDrawn++;
401
+ }
402
+ }
403
+ }
404
+
405
+ // ── SIGNAL PARTICLE SYSTEM ────────────────────────────────────────────────────
406
+ // This is what makes the network look ALIVE — glowing orbs traveling between neurons
407
+ const signals = [];
408
+ const sigGeo = new THREE.SphereGeometry(0.12, 8, 8);
409
+
410
+ function spawnSignal(fromNeuron, toNeuron, color=0x00f5c4){
411
+ const mat = new THREE.MeshBasicMaterial({color, transparent:true, opacity:0.95});
412
+ const mesh = new THREE.Mesh(sigGeo, mat);
413
+ mesh.position.copy(fromNeuron.pos);
414
+ scene.add(mesh); // add to scene not group so it stays in world space
415
+
416
+ // Trail
417
+ const trailMat = new THREE.MeshBasicMaterial({color, transparent:true, opacity:0.4});
418
+ const trail = new THREE.Mesh(new THREE.SphereGeometry(0.07,6,6), trailMat);
419
+ scene.add(trail);
420
+
421
+ signals.push({
422
+ mesh, trail, mat, trailMat,
423
+ from: fromNeuron.pos.clone(),
424
+ to: toNeuron.pos.clone(),
425
+ toNeuron,
426
+ t: 0,
427
+ speed: 0.028 + Math.random()*0.015,
428
+ color,
429
+ });
430
+ }
431
+
432
+ function fireNeuron(layerIdx, neuronIdx, cascade=true){
433
+ if(layerIdx>=neurons.length || neuronIdx>=neurons[layerIdx].length) return;
434
+ const n = neurons[layerIdx][neuronIdx];
435
+ n.fireTimer = 1.0;
436
+
437
+ // Spawn signals to next layer
438
+ if(cascade && layerIdx < nL-1){
439
+ const nextL = neurons[layerIdx+1];
440
+ const count = Math.min(nextL.length, 2+Math.floor(Math.random()*3));
441
+ const shuffled = [...nextL].sort(()=>Math.random()-0.5).slice(0, count);
442
+ shuffled.forEach(target => {
443
+ spawnSignal(n, target, layerIdx===0?0x00f5c4:0x5b9fff);
444
+ });
445
+ }
446
+ }
447
+
448
+ // Every 800ms: fire random input neurons → cascade through the network
449
+ let fireTimer = 0;
450
+ function triggerFiring(){
451
+ if(neurons.length===0) return;
452
+ const inputLayer = neurons[0];
453
+ const count = 1+Math.floor(Math.random()*3);
454
+ for(let i=0;i<count;i++){
455
+ const ni = Math.floor(Math.random()*inputLayer.length);
456
+ fireNeuron(0, ni, true);
457
+ }
458
+ }
459
+
460
+ // ── LOSS HISTORY GRAPH ───────────────────────────────────────────────────────
461
+ (function(){
462
+ const hist = INIT.loss_history||[];
463
+ if(hist.length<3) return;
464
+ const gc=document.createElement('canvas');
465
+ gc.width=200; gc.height=50;
466
+ gc.style.cssText='position:fixed;bottom:14px;right:14px;border:1px solid #1a2240;border-radius:6px;background:rgba(11,15,30,0.8)';
467
+ const ctx=gc.getContext('2d');
468
+ const mn=Math.min(...hist), mx=Math.max(...hist), rng=mx-mn||1;
469
+ // fill
470
+ ctx.beginPath();
471
+ hist.forEach((v,i)=>{const x=(i/(hist.length-1))*200,y=50-4-((v-mn)/rng)*42; i===0?ctx.moveTo(x,50):ctx.lineTo(x,y)});
472
+ ctx.lineTo(200,50); ctx.closePath();
473
+ ctx.fillStyle='rgba(0,245,196,0.08)'; ctx.fill();
474
+ // line
475
+ ctx.beginPath();
476
+ hist.forEach((v,i)=>{const x=(i/(hist.length-1))*200,y=50-4-((v-mn)/rng)*42; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y)});
477
+ ctx.strokeStyle='#00f5c4'; ctx.lineWidth=1.5; ctx.stroke();
478
+ document.body.appendChild(gc);
479
+ const lb=document.createElement('div');
480
+ lb.style.cssText='position:fixed;bottom:68px;right:14px;font-size:9px;color:#2a3a5a;font-family:monospace';
481
+ lb.textContent='LOSS ↓'; document.body.appendChild(lb);
482
+ })();
483
+
484
+ // ── HUD ──────────────────────────────────────────────────────────────────────
485
+ const st = INIT.stats||{};
486
+ const isImg = INIT.type==='cnn';
487
+ const kCount = st.knowledge_count || st.total_images || 0;
488
+ document.getElementById('h1').textContent = (isImg?'👁️ IMAGE CNN':'📝 TEXT NETWORK')+' — LIVE';
489
+ document.getElementById('h2').textContent = `EPOCH ${(st.epoch||0).toLocaleString()}`;
490
+ document.getElementById('h3').textContent = `LOSS ${st.loss||'—'} ACC ${st.accuracy||'—'}%`;
491
+ document.getElementById('h4').textContent = `BRAIN ${kCount} ${isImg?'images':'articles'} · ${sizes.map((s,i)=>Math.min(s,MAX_N)).join('→')} neurons`;
492
+
493
+ // ── ANIMATE ──────────────────────────────────────────────────────────────────
494
+ let frame = 0;
495
+ const clock = new THREE.Clock();
496
+
497
+ function animate(){
498
+ requestAnimationFrame(animate);
499
+ const dt = clock.getDelta();
500
+ const elaps = clock.getElapsedTime();
501
+ frame++;
502
+
503
+ // ① Auto-fire every 800ms
504
+ fireTimer += dt;
505
+ if(fireTimer > 0.8){
506
+ triggerFiring();
507
+ fireTimer = 0;
508
+ }
509
+
510
+ // ② Update neurons — pulse and fire glow
511
+ neurons.forEach((layer, li)=>{
512
+ layer.forEach((n, ni)=>{
513
+ // Base sinusoidal pulse — each neuron breathes at its own rate
514
+ const phase = li*0.7 + ni*0.3;
515
+ const pulse = Math.sin(elaps*2.1+phase)*0.15 + Math.sin(elaps*0.9+phase*2)*0.08;
516
+ const act = Math.max(0, Math.min(1, n.baseAct + pulse + n.fireTimer*0.6));
517
+
518
+ const r=Math.floor(act*240), g=Math.floor(act*200), b=Math.floor(60+act*100);
519
+ n.mesh.material.emissive.setRGB(r/255, g/255, b/255);
520
+ n.mesh.material.emissiveIntensity = 0.3 + act*2.2;
521
+ n.mesh.scale.setScalar(1 + n.fireTimer*0.6 + pulse*0.05);
522
+
523
+ // Glow halo
524
+ n.gMesh.material.opacity = n.fireTimer>0 ? n.fireTimer*0.7 : Math.max(0,(act-0.45)*0.4);
525
+ n.gMesh.scale.setScalar(1 + n.fireTimer*1.0);
526
+
527
+ // Decay fire
528
+ if(n.fireTimer > 0) n.fireTimer = Math.max(0, n.fireTimer - dt*1.8);
529
+ });
530
+ });
531
+
532
+ // ③ Update connection lines — flash when signals pass
533
+ connections.forEach(conn=>{
534
+ const sig=(conn.from.baseAct+conn.to.baseAct)/2;
535
+ const flash = conn.from.fireTimer*0.5;
536
+ conn.mat.opacity = Math.min(0.9, 0.04 + sig*0.3 + flash);
537
+ });
538
+
539
+ // ④ Move signal particles
540
+ for(let i=signals.length-1;i>=0;i--){
541
+ const s=signals[i];
542
+ s.t += s.speed;
543
+ if(s.t>=1){
544
+ scene.remove(s.mesh); scene.remove(s.trail);
545
+ signals.splice(i,1);
546
+ // Fire destination neuron (next layer cascade)
547
+ s.toNeuron.fireTimer = 0.8;
548
+ // If not at output, cascade further
549
+ const lIdx = neurons.findIndex(l=>l.includes(s.toNeuron));
550
+ if(lIdx>=0 && lIdx<nL-1){
551
+ const nextL=neurons[lIdx+1];
552
+ if(Math.random()>0.3){
553
+ const t2=nextL[Math.floor(Math.random()*nextL.length)];
554
+ spawnSignal(s.toNeuron, t2, 0x5b9fff);
555
+ }
556
+ }
557
+ } else {
558
+ s.mesh.position.lerpVectors(s.from, s.to, s.t);
559
+ s.trail.position.lerpVectors(s.from, s.to, Math.max(0,s.t-0.08));
560
+ // Fade out near end
561
+ s.mat.opacity = s.t<0.8 ? 0.95 : (1-s.t)*4.75;
562
+ s.trailMat.opacity = s.t<0.8 ? 0.35 : (1-s.t)*1.75;
563
+ }
564
+ }
565
+
566
+ // ⑤ Camera orbit + gentle network bob
567
+ const autoY = elaps * 0.12;
568
+ camera.position.x = Math.sin(rY + autoY)*zoom;
569
+ camera.position.y = Math.sin(rX)*zoom*0.45 + 1;
570
+ camera.position.z = Math.cos(rY + autoY)*zoom;
571
+ camera.lookAt(0, 0, 0);
572
+
573
+ kl.intensity = 7 + Math.sin(elaps*1.5)*2;
574
+ bl.intensity = 3 + Math.cos(elaps*2.3)*1;
575
+ kl.position.x = Math.sin(elaps*0.4)*6;
576
+ group.position.y = Math.sin(elaps*0.6)*0.15;
577
+
578
+ renderer.render(scene, camera);
579
+ }
580
+ animate();
581
+
582
+ window.addEventListener('resize',()=>{
583
+ camera.aspect=innerWidth/innerHeight;
584
+ camera.updateProjectionMatrix();
585
+ renderer.setSize(innerWidth,innerHeight);
586
+ });
587
+ </script></body></html>"""
588
+
589
+ def get_text_viz():
590
+ s = network.get_viz_state(); s['type']='text'
591
+ h = build_viz(json.dumps(s))
592
+ e = h.replace('"','&quot;').replace('\n','&#10;')
593
+ return f'<iframe srcdoc="{e}" style="width:100%;height:650px;border:none;border-radius:12px"></iframe>'
594
+
595
+ def get_img_viz():
596
+ s = img_network.get_viz_state()
597
+ h = build_viz(json.dumps(s))
598
+ e = h.replace('"','&quot;').replace('\n','&#10;')
599
+ return f'<iframe srcdoc="{e}" style="width:100%;height:650px;border:none;border-radius:12px"></iframe>'
600
+
601
+ # ── UI ────────────────────────────────────────────────────────────────────────
602
+ with gr.Blocks(title="Living Neural Network") as demo:
603
+ gr.Markdown("# 🧠 Living Neural Network\n*Two AIs training on live internet data — one reads, one looks*")
604
+
605
+ sbar = gr.Textbox(label="", value=status_bar(), interactive=False, lines=2)
606
+
607
+ with gr.Tabs():
608
+
609
+ # CONTROL
610
+ with gr.Tab("🎛️ Control"):
611
+ gr.Markdown("### 📝 Text Network")
612
+ with gr.Row():
613
+ gr.Button("▶ Start Text", variant="primary").click(start_text, outputs=gr.Textbox(label="",lines=1,interactive=False))
614
+ gr.Button("⏹ Stop Text", variant="stop" ).click(stop_text, outputs=gr.Textbox(label="",lines=1,interactive=False))
615
+ gr.Button("📡 Fetch Text" ).click(do_fetch_text, outputs=gr.Textbox(label="",lines=1,interactive=False))
616
+ with gr.Row():
617
+ ts=gr.Slider(10,500,100,step=10,label="Steps")
618
+ gr.Button("🔥 Train Text").click(do_train_text,inputs=ts,outputs=gr.Textbox(label="",lines=1,interactive=False))
619
+ gr.Markdown("---\n### 👁️ Image CNN")
620
+ with gr.Row():
621
+ gr.Button("▶ Start Images",variant="primary").click(start_img, outputs=gr.Textbox(label="",lines=1,interactive=False))
622
+ gr.Button("⏹ Stop Images",variant="stop" ).click(stop_img, outputs=gr.Textbox(label="",lines=1,interactive=False))
623
+ gr.Button("🖼️ Fetch Images" ).click(do_fetch_img, outputs=gr.Textbox(label="",lines=1,interactive=False))
624
+ with gr.Row():
625
+ is_=gr.Slider(5,100,20,step=5,label="Steps")
626
+ gr.Button("🔥 Train CNN").click(do_train_img,inputs=is_,outputs=gr.Textbox(label="",lines=1,interactive=False))
627
+
628
+ # STATS
629
+ with gr.Tab("📊 Stats"):
630
+ with gr.Row():
631
+ tmd=gr.Markdown(value=text_stats())
632
+ imd=gr.Markdown(value=img_stats())
633
+ lbox=gr.HTML(value=get_log(),label="Live Logs")
634
+
635
+ # 3D TEXT VIZ
636
+ with gr.Tab("🔮 Text Network 3D"):
637
+ gr.Markdown("**Neurons fire & signals travel left→right in real time.** Drag=rotate Scroll=zoom")
638
+ tvb=gr.Button("🔄 Refresh",variant="primary")
639
+ tvo=gr.HTML('<div style="height:650px;background:#030610;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#00f5c4;font-family:monospace;font-size:14px">Click 🔄 Refresh to load the 3D network</div>')
640
+ tvb.click(get_text_viz,outputs=tvo)
641
+
642
+ # 3D IMAGE VIZ
643
+ with gr.Tab("👁️ Image CNN 3D"):
644
+ gr.Markdown("**Conv layers visualised in 3D.** Each block learns progressively deeper features.")
645
+ ivb=gr.Button("🔄 Refresh",variant="primary")
646
+ ivo=gr.HTML('<div style="height:650px;background:#030610;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#00f5c4;font-family:monospace;font-size:14px">Click 🔄 Refresh to load CNN visualization</div>')
647
+ ivb.click(get_img_viz,outputs=ivo)
648
+
649
+ # CHAT
650
+ with gr.Tab("💬 Chat with my AI"):
651
+ gr.Markdown(
652
+ "### Talk to your AI — it answers from what it has actually learned\n"
653
+ "The more articles it collects, the smarter the answers.\n"
654
+ "Type `stats` to see what it knows. Type `help` for tips."
655
+ )
656
+ chatbox = gr.Chatbot(label="", height=480)
657
+ with gr.Row():
658
+ msg_in = gr.Textbox(label="", placeholder="Ask anything — e.g. 'What's happening in AI?'", scale=5)
659
+ send_b = gr.Button("Send", variant="primary", scale=1)
660
+ send_b.click(chat_fn, inputs=[msg_in, chatbox], outputs=[chatbox, msg_in])
661
+ msg_in.submit(chat_fn, inputs=[msg_in, chatbox], outputs=[chatbox, msg_in])
662
+ gr.Examples(
663
+ [["What's happening in AI and technology?"],
664
+ ["Tell me about recent science discoveries"],
665
+ ["What's in the news about sports?"],
666
+ ["stats"],["help"]],
667
+ inputs=msg_in
668
+ )
669
+
670
+ # KNOWLEDGE
671
+ with gr.Tab("📚 Knowledge Base"):
672
+ gr.Markdown("Everything the AI has read — saved to `knowledge.jsonl` instantly")
673
+ kmd=gr.Markdown(value=get_knowledge())
674
+
675
+ # DATA FEED
676
+ with gr.Tab("📰 Data Feed"):
677
+ fmd=gr.Markdown(value=get_feed())
678
+
679
+ # PREDICT TEXT
680
+ with gr.Tab("🔍 Classify Text"):
681
+ gr.Markdown("Test your trained text model")
682
+ pt=gr.Textbox(label="Text",lines=3,placeholder="Scientists discover...")
683
+ pb=gr.Button("Classify",variant="primary")
684
+ po=gr.Markdown()
685
+ pb.click(predict_text,inputs=pt,outputs=po)
686
+
687
+ # PREDICT IMAGE
688
+ with gr.Tab("🖼️ Classify Image"):
689
+ gr.Markdown("Test your trained image CNN")
690
+ pi=gr.Image(label="Upload image",type="filepath")
691
+ pib=gr.Button("Classify",variant="primary")
692
+ pio=gr.Markdown()
693
+ pib.click(predict_img,inputs=pi,outputs=pio)
694
+
695
+ # timers
696
+ t3=gr.Timer(3); t5=gr.Timer(5); t8=gr.Timer(8); t12=gr.Timer(12)
697
+ t3.tick(status_bar,outputs=sbar)
698
+ t3.tick(get_log,outputs=lbox)
699
+ t5.tick(text_stats,outputs=tmd)
700
+ t5.tick(img_stats,outputs=imd)
701
+ t8.tick(get_knowledge,outputs=kmd)
702
+ t8.tick(get_feed,outputs=fmd)
703
+ t12.tick(get_text_viz,outputs=tvo)
704
+ t12.tick(get_img_viz,outputs=ivo)
705
+
706
+ # ── AUTO-START: both networks begin training immediately on startup ──────────
707
+ def _auto_start():
708
+ import time as _t
709
+ _t.sleep(3) # give Gradio time to finish starting
710
+ try:
711
+ start_text()
712
+ log("🤖 Auto-started TEXT network")
713
+ except Exception as e:
714
+ log(f"⚠ Auto-start text error: {e}")
715
+ try:
716
+ start_img()
717
+ log("🤖 Auto-started IMAGE network")
718
+ except Exception as e:
719
+ log(f"⚠ Auto-start image error: {e}")
720
+
721
+ threading.Thread(target=_auto_start, daemon=True).start()
722
+
723
+ if __name__=="__main__":
724
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True, ssr_mode=False)
chatbot.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, re, math, os, random
2
+ from collections import Counter, defaultdict
3
+
4
+ KNOWLEDGE_FILE = 'knowledge.jsonl'
5
+ STOPWORDS = {
6
+ 'a','an','the','is','it','in','on','at','to','for','of','and','or','but',
7
+ 'was','are','were','be','been','have','has','had','do','does','did','will',
8
+ 'would','could','should','may','might','that','this','these','those','with',
9
+ 'from','by','as','not','also','than','then','so','if','when','what','how',
10
+ 'who','which','its','their','our','your','my','his','her','we','they','he',
11
+ 'she','you','i','me','him','us','them','said','says',
12
+ }
13
+
14
+ def tokenize(text):
15
+ text = re.sub(r'[^\w\s]', ' ', text.lower())
16
+ return [w for w in text.split() if w not in STOPWORDS and len(w) > 2]
17
+
18
+ class KnowledgeBase:
19
+ def __init__(self):
20
+ self.docs = []; self.doc_tokens = []; self.idf = {}; self.last_count = 0
21
+ self._load()
22
+
23
+ def _load(self):
24
+ if not os.path.exists(KNOWLEDGE_FILE): return
25
+ new_docs = []
26
+ try:
27
+ with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
28
+ for line in f:
29
+ line = line.strip()
30
+ if line:
31
+ try: new_docs.append(json.loads(line))
32
+ except: pass
33
+ except: return
34
+ if len(new_docs) == self.last_count: return
35
+ self.docs = new_docs; self.last_count = len(new_docs)
36
+ self.doc_tokens = [tokenize(d.get('text','')) for d in self.docs]
37
+ N = len(self.docs)
38
+ df = defaultdict(int)
39
+ for tokens in self.doc_tokens:
40
+ for w in set(tokens): df[w] += 1
41
+ self.idf = {w: math.log((N+1)/(c+1))+1 for w,c in df.items()}
42
+
43
+ def search(self, query, top_k=5):
44
+ self._load()
45
+ if not self.docs: return []
46
+ q = tokenize(query)
47
+ if not q: return []
48
+ def score(dt):
49
+ tf = Counter(dt); n = len(dt)
50
+ return sum((tf[w]/n)*self.idf.get(w,0) for w in q if w in tf)
51
+ ranked = sorted(range(len(self.docs)), key=lambda i: -score(self.doc_tokens[i]))
52
+ return [(self.docs[i], score(self.doc_tokens[i])) for i in ranked[:top_k] if score(self.doc_tokens[i]) > 0]
53
+
54
+ def get_stats(self):
55
+ self._load()
56
+ return {
57
+ 'total': len(self.docs),
58
+ 'categories': dict(Counter(d.get('category','other') for d in self.docs)),
59
+ 'sources': dict(Counter(d.get('source','unknown') for d in self.docs)),
60
+ }
61
+
62
+ class RAGChatbot:
63
+ def __init__(self):
64
+ self.kb = KnowledgeBase()
65
+
66
+ def _build_answer(self, query, results, stats):
67
+ total = stats['total']
68
+ if total == 0:
69
+ return ("My brain is empty right now 🧠 — I haven't read any articles yet.\n\n"
70
+ "Go to **Control → ▶ Start Text** to feed me internet data!")
71
+
72
+ if not results:
73
+ return (f"I've read **{total} articles** so far, but I haven't learned anything about that topic yet.\n\n"
74
+ f"My current knowledge covers: **{', '.join(list(stats['categories'].keys())[:5])}**.\n\n"
75
+ "Keep training me and I'll learn more over time! 📚")
76
+
77
+ # Build answer from best matching sentences
78
+ q_tokens = set(tokenize(query))
79
+ seen, parts = set(), []
80
+
81
+ for doc, sc in results[:3]:
82
+ text = doc.get('text','')
83
+ cat = doc.get('category','general').upper()
84
+ src = doc.get('source','?').upper()
85
+ sents = [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if len(s.strip()) > 40]
86
+ scored = sorted(
87
+ [(len(q_tokens & set(tokenize(s))), s) for s in sents if s not in seen],
88
+ reverse=True
89
+ )[:2]
90
+ for _, s in scored: seen.add(s)
91
+ if scored:
92
+ snippet = ' '.join(s for _, s in scored if _>0) or scored[0][1]
93
+ parts.append(f"📰 **[{cat} / {src}]**\n{snippet}")
94
+ elif len(text) > 40:
95
+ parts.append(f"📰 **[{cat} / {src}]**\n{text[:250]}...")
96
+
97
+ if not parts:
98
+ return f"I found some related articles ({total} total) but couldn't extract a clear answer. Try rephrasing!"
99
+
100
+ intro = random.choice([
101
+ f"Based on what I've learned ({total} articles read):\n\n",
102
+ f"Here's what I know from my training data:\n\n",
103
+ f"From {total} articles I've read:\n\n",
104
+ ])
105
+ return intro + '\n\n'.join(parts)
106
+
107
+ def chat(self, user_message, history):
108
+ history = list(history or [])
109
+ msg = user_message.strip()
110
+ if not msg: return history
111
+
112
+ lower = msg.lower()
113
+ stats = self.kb.get_stats()
114
+
115
+ if lower in ('hi','hello','hey','sup','yo','hiya','howdy','helo'):
116
+ total = stats['total']
117
+ if total == 0:
118
+ bot_reply = ("Hey! 👋 I'm your AI — but I haven't learned anything yet.\n\n"
119
+ "Go to **Control → ▶ Start Text** to feed me internet articles!")
120
+ else:
121
+ bot_reply = (f"Hey! 👋 I'm your AI — I've read **{total} articles** so far!\n\n"
122
+ f"Ask me anything about what I've learned. My strongest topics: "
123
+ f"**{', '.join(list(stats['categories'].keys())[:4])}**.")
124
+
125
+ elif any(p in lower for p in ('how are you','how r you',"what's up",'whats up')):
126
+ bot_reply = "I'm learning and growing! 🤖 The more articles I read, the smarter I get. Ask me something!"
127
+
128
+ elif any(p in lower for p in ('who are you','what are you','what can you do','tell me about yourself')):
129
+ bot_reply = (
130
+ "I'm a **Living Neural Network** — an AI that learns from real internet articles in real time! 🧠\n\n"
131
+ f"I've read **{stats['total']} articles** so far covering: "
132
+ f"{', '.join(list(stats['categories'].keys())[:5]) or 'nothing yet'}.\n\n"
133
+ "I only know what I've actually been trained on — no pre-built knowledge. "
134
+ "The more you train me, the smarter I become. Like a brain growing from scratch!"
135
+ )
136
+
137
+ elif lower in ('stats','status','what do you know','knowledge'):
138
+ lines = '\n'.join(f" • **{k}**: {v}" for k,v in sorted(stats['categories'].items(), key=lambda x: -x[1]))
139
+ srcs = ', '.join(f"{k}({v})" for k,v in stats['sources'].items())
140
+ bot_reply = (f"## 🧠 My Knowledge\n\n**Total articles:** {stats['total']}\n\n"
141
+ f"**By topic:**\n{lines or ' • nothing yet'}\n\n"
142
+ f"**Sources:** {srcs or 'none yet'}")
143
+
144
+ elif lower in ('help','?','commands'):
145
+ bot_reply = ("## 💡 How to use me\n\n"
146
+ "Just ask me anything — I'll search what I've learned!\n\n"
147
+ "**Try:** *What's happening in AI?* · *Tell me about climate change* · *Who is Elon Musk?*\n\n"
148
+ "**Commands:** `stats` · `help`\n\n"
149
+ "⚡ The more I train, the more I know!")
150
+
151
+ elif any(p in lower for p in ('thanks','thank you','thx','ty')):
152
+ bot_reply = "You're welcome! 😊 Keep training me!"
153
+
154
+ else:
155
+ results = self.kb.search(msg, top_k=5)
156
+ bot_reply = self._build_answer(msg, results, stats)
157
+
158
+ history.append({"role": "user", "content": user_message})
159
+ history.append({"role": "assistant", "content": bot_reply})
160
+ return history
data_fetcher.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ data_fetcher.py — Fetches real live data from the internet.
3
+ Sources: RSS, Reddit, Wikipedia, HackerNews, DuckDuckGo Search,
4
+ DEV.to, GitHub Trending, Google News RSS, Al Jazeera, Guardian, NPR, arXiv
5
+ """
6
+
7
+ import requests, feedparser, random, re, time, json
8
+ from datetime import datetime
9
+ from collections import deque
10
+ from urllib.parse import quote_plus
11
+
12
+ HEADERS = {
13
+ 'User-Agent': 'Mozilla/5.0 (compatible; LivingNeuralNetwork/2.0; educational)',
14
+ 'Accept-Language': 'en-US,en;q=0.9',
15
+ }
16
+
17
+ # ─── RSS FEEDS ────────────────────────────────────────────────────────────────
18
+ RSS_FEEDS = {
19
+ 'technology': [
20
+ 'https://feeds.feedburner.com/TechCrunch',
21
+ 'https://feeds.arstechnica.com/arstechnica/index',
22
+ 'https://www.wired.com/feed/rss',
23
+ 'https://hnrss.org/frontpage',
24
+ 'https://www.theverge.com/rss/index.xml',
25
+ 'https://feeds.feedburner.com/venturebeat/SZYF',
26
+ 'https://dev.to/feed',
27
+ 'https://thenextweb.com/feed/',
28
+ ],
29
+ 'science': [
30
+ 'https://www.sciencedaily.com/rss/all.xml',
31
+ 'https://www.newscientist.com/feed/home/',
32
+ 'https://rss.nytimes.com/services/xml/rss/nyt/Science.xml',
33
+ 'https://www.nature.com/nature.rss',
34
+ 'http://export.arxiv.org/rss/cs.AI',
35
+ 'http://export.arxiv.org/rss/cs.LG',
36
+ 'https://phys.org/rss-feed/breaking/',
37
+ ],
38
+ 'world': [
39
+ 'https://feeds.bbci.co.uk/news/world/rss.xml',
40
+ 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml',
41
+ 'https://www.aljazeera.com/xml/rss/all.xml',
42
+ 'https://feeds.npr.org/1004/rss.xml',
43
+ 'https://www.theguardian.com/world/rss',
44
+ 'https://feeds.reuters.com/reuters/worldNews',
45
+ ],
46
+ 'sports': [
47
+ 'https://feeds.bbci.co.uk/sport/rss.xml',
48
+ 'https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml',
49
+ 'https://www.espn.com/espn/rss/news',
50
+ ],
51
+ 'business': [
52
+ 'https://feeds.bbci.co.uk/news/business/rss.xml',
53
+ 'https://rss.nytimes.com/services/xml/rss/nyt/Business.xml',
54
+ 'https://feeds.bloomberg.com/markets/news.rss',
55
+ 'https://www.theguardian.com/business/rss',
56
+ 'https://feeds.reuters.com/reuters/businessNews',
57
+ ],
58
+ 'health': [
59
+ 'https://feeds.bbci.co.uk/news/health/rss.xml',
60
+ 'https://rss.nytimes.com/services/xml/rss/nyt/Health.xml',
61
+ 'https://www.theguardian.com/society/health/rss',
62
+ 'https://feeds.npr.org/1128/rss.xml',
63
+ ],
64
+ 'entertainment': [
65
+ 'https://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml',
66
+ 'https://rss.nytimes.com/services/xml/rss/nyt/Arts.xml',
67
+ 'https://www.theguardian.com/culture/rss',
68
+ 'https://variety.com/feed/',
69
+ ],
70
+ 'ai': [
71
+ 'http://export.arxiv.org/rss/cs.AI',
72
+ 'http://export.arxiv.org/rss/cs.LG',
73
+ 'https://hnrss.org/frontpage',
74
+ 'https://feeds.feedburner.com/TechCrunch',
75
+ ],
76
+ }
77
+
78
+ REDDIT_FEEDS = [
79
+ ('technology', 'https://www.reddit.com/r/technology/top.json?limit=25&t=day'),
80
+ ('science', 'https://www.reddit.com/r/science/top.json?limit=25&t=day'),
81
+ ('world', 'https://www.reddit.com/r/worldnews/top.json?limit=25&t=day'),
82
+ ('sports', 'https://www.reddit.com/r/sports/top.json?limit=25&t=day'),
83
+ ('business', 'https://www.reddit.com/r/business/top.json?limit=25&t=day'),
84
+ ('health', 'https://www.reddit.com/r/Health/top.json?limit=25&t=day'),
85
+ ('entertainment', 'https://www.reddit.com/r/movies/top.json?limit=25&t=day'),
86
+ ('ai', 'https://www.reddit.com/r/MachineLearning/top.json?limit=25&t=day'),
87
+ ('ai', 'https://www.reddit.com/r/artificial/top.json?limit=25&t=day'),
88
+ ('science', 'https://www.reddit.com/r/askscience/top.json?limit=25&t=day'),
89
+ ('technology', 'https://www.reddit.com/r/programming/top.json?limit=25&t=day'),
90
+ ('world', 'https://www.reddit.com/r/geopolitics/top.json?limit=25&t=day'),
91
+ ]
92
+
93
+ # Topics to actively search on DuckDuckGo
94
+ DDG_SEARCH_TOPICS = [
95
+ ('technology', 'latest AI artificial intelligence news'),
96
+ ('technology', 'tech industry news today'),
97
+ ('science', 'science discovery research breakthrough'),
98
+ ('world', 'world news today'),
99
+ ('business', 'business economy markets news'),
100
+ ('health', 'health medicine research news'),
101
+ ('ai', 'machine learning deep learning news'),
102
+ ('ai', 'ChatGPT OpenAI Anthropic Google AI'),
103
+ ('world', 'politics international relations'),
104
+ ('science', 'space NASA astronomy discovery'),
105
+ ('technology', 'cybersecurity data breach'),
106
+ ('entertainment', 'movies music entertainment news'),
107
+ ]
108
+
109
+ WIKIPEDIA_API = 'https://en.wikipedia.org/api/rest_v1/page/random/summary'
110
+ WIKI_SEARCH = 'https://en.wikipedia.org/api/rest_v1/page/summary/{}'
111
+ HN_TOP_API = 'https://hacker-news.firebaseio.com/v0/topstories.json'
112
+ HN_ITEM_API = 'https://hacker-news.firebaseio.com/v0/item/{}.json'
113
+
114
+ # ─── UTILS ────────────────────────────────────────────────────────────────────
115
+ def clean(text: str, max_chars: int = 700) -> str:
116
+ if not text: return ''
117
+ text = re.sub(r'<[^>]+>', ' ', text)
118
+ text = re.sub(r'http\S+', '', text)
119
+ text = re.sub(r'[^\w\s.,!?;:\'\-–—]', ' ', text)
120
+ text = re.sub(r'\s+', ' ', text).strip()
121
+ return text[:max_chars]
122
+
123
+ def make_item(text, category, source, extra=None):
124
+ text = clean(text)
125
+ if len(text) < 30: return None
126
+ return {
127
+ 'text': text,
128
+ 'category': category,
129
+ 'source': source,
130
+ 'timestamp': datetime.utcnow().isoformat(),
131
+ **(extra or {}),
132
+ }
133
+
134
+ # ─── FETCHERS ─────────────────────────────────────────────────────────────────
135
+ def fetch_rss(category: str, url: str) -> list:
136
+ items = []
137
+ try:
138
+ feed = feedparser.parse(url)
139
+ for entry in feed.entries[:15]:
140
+ title = entry.get('title', '')
141
+ summary = entry.get('summary', entry.get('description', ''))
142
+ text = f"{title}. {summary}"
143
+ item = make_item(text, category, 'rss', {'feed': url.split('/')[2]})
144
+ if item: items.append(item)
145
+ except Exception: pass
146
+ return items
147
+
148
+ def fetch_reddit(category: str, url: str) -> list:
149
+ items = []
150
+ try:
151
+ r = requests.get(url, headers=HEADERS, timeout=10)
152
+ r.raise_for_status()
153
+ posts = r.json().get('data', {}).get('children', [])
154
+ for post in posts:
155
+ d = post.get('data', {})
156
+ title = d.get('title', '')
157
+ selftext = d.get('selftext', '')
158
+ text = f"{title}. {selftext}"
159
+ item = make_item(text, category, 'reddit',
160
+ {'subreddit': d.get('subreddit',''), 'score': d.get('score',0)})
161
+ if item: items.append(item)
162
+ except Exception: pass
163
+ return items
164
+
165
+ def fetch_duckduckgo(query: str, category: str) -> list:
166
+ """Search DuckDuckGo and extract text snippets — no API key needed."""
167
+ items = []
168
+ try:
169
+ url = f"https://html.duckduckgo.com/html/?q={quote_plus(query)}"
170
+ r = requests.get(url, headers={**HEADERS, 'Accept': 'text/html'}, timeout=12)
171
+ # Extract result snippets
172
+ snippets = re.findall(r'class="result__snippet"[^>]*>(.*?)</[as]>', r.text, re.DOTALL)
173
+ titles = re.findall(r'class="result__a"[^>]*>(.*?)</a>', r.text, re.DOTALL)
174
+ for i, snippet in enumerate(snippets[:8]):
175
+ title = titles[i] if i < len(titles) else query
176
+ title = re.sub(r'<[^>]+>', '', title).strip()
177
+ snippet = re.sub(r'<[^>]+>', '', snippet).strip()
178
+ text = f"{title}. {snippet}"
179
+ item = make_item(text, category, 'duckduckgo', {'query': query})
180
+ if item: items.append(item)
181
+ except Exception: pass
182
+ return items
183
+
184
+ def fetch_wikipedia_random() -> dict | None:
185
+ try:
186
+ r = requests.get(WIKIPEDIA_API, headers=HEADERS, timeout=10)
187
+ r.raise_for_status()
188
+ data = r.json()
189
+ title = data.get('title', '')
190
+ text = f"{title}. {data.get('extract','')}"
191
+ return make_item(text, 'other', 'wikipedia', {'title': title})
192
+ except Exception: return None
193
+
194
+ def fetch_wikipedia_topic(topic: str, category: str) -> dict | None:
195
+ """Fetch Wikipedia article for a specific topic."""
196
+ try:
197
+ url = WIKI_SEARCH.format(quote_plus(topic))
198
+ r = requests.get(url, headers=HEADERS, timeout=10)
199
+ r.raise_for_status()
200
+ data = r.json()
201
+ title = data.get('title', '')
202
+ text = f"{title}. {data.get('extract','')}"
203
+ return make_item(text, category, 'wikipedia', {'title': title})
204
+ except Exception: return None
205
+
206
+ def fetch_hackernews(n: int = 8) -> list:
207
+ items = []
208
+ try:
209
+ r = requests.get(HN_TOP_API, headers=HEADERS, timeout=10)
210
+ r.raise_for_status()
211
+ ids = r.json()[:50]
212
+ chosen = random.sample(ids, min(n, len(ids)))
213
+ for sid in chosen:
214
+ try:
215
+ sr = requests.get(HN_ITEM_API.format(sid), headers=HEADERS, timeout=6)
216
+ story = sr.json()
217
+ title = story.get('title', '')
218
+ body = story.get('text', '')
219
+ item = make_item(f"{title}. {body}", 'technology', 'hackernews',
220
+ {'score': story.get('score', 0)})
221
+ if item: items.append(item)
222
+ time.sleep(0.08)
223
+ except Exception: continue
224
+ except Exception: pass
225
+ return items
226
+
227
+ # ─── MAIN FETCHER ─────────────────────────────────────────────────────────────
228
+ class DataFetcher:
229
+ def __init__(self):
230
+ self.total_fetched = 0
231
+ self.source_counts = {
232
+ 'rss': 0, 'reddit': 0, 'wikipedia': 0,
233
+ 'hackernews': 0, 'duckduckgo': 0
234
+ }
235
+ self.recent_items = deque(maxlen=100)
236
+ self.log = deque(maxlen=300)
237
+ self._ddg_idx = 0 # rotate through DDG search topics
238
+
239
+ def _log(self, msg: str):
240
+ ts = datetime.utcnow().strftime('%H:%M:%S')
241
+ entry = f"[{ts}] {msg}"
242
+ self.log.appendleft(entry)
243
+ return entry
244
+
245
+ def fetch_round(self) -> list:
246
+ """Fetch one full round from ALL sources."""
247
+ all_items = []
248
+
249
+ # 1. RSS — pick 2 random categories
250
+ for _ in range(2):
251
+ try:
252
+ cat = random.choice(list(RSS_FEEDS.keys()))
253
+ url = random.choice(RSS_FEEDS[cat])
254
+ items = fetch_rss(cat, url)
255
+ all_items.extend(items)
256
+ self.source_counts['rss'] += len(items)
257
+ self._log(f"📰 RSS [{cat.upper()}] +{len(items)} ← {url.split('/')[2]}")
258
+ except Exception as e:
259
+ self._log(f"⚠ RSS error: {e}")
260
+
261
+ # 2. Reddit
262
+ try:
263
+ cat, url = random.choice(REDDIT_FEEDS)
264
+ items = fetch_reddit(cat, url)
265
+ all_items.extend(items)
266
+ self.source_counts['reddit'] += len(items)
267
+ sub = url.split('/r/')[1].split('/')[0]
268
+ self._log(f"🟠 Reddit [r/{sub}] +{len(items)}")
269
+ except Exception as e:
270
+ self._log(f"⚠ Reddit error: {e}")
271
+
272
+ # 3. DuckDuckGo search (rotate through topics)
273
+ try:
274
+ cat, query = DDG_SEARCH_TOPICS[self._ddg_idx % len(DDG_SEARCH_TOPICS)]
275
+ self._ddg_idx += 1
276
+ items = fetch_duckduckgo(query, cat)
277
+ all_items.extend(items)
278
+ self.source_counts['duckduckgo'] += len(items)
279
+ self._log(f"🦆 DuckDuckGo [{cat.upper()}] \"{query[:40]}\" +{len(items)}")
280
+ except Exception as e:
281
+ self._log(f"⚠ DDG error: {e}")
282
+
283
+ # 4. Wikipedia (random + topic)
284
+ try:
285
+ item = fetch_wikipedia_random()
286
+ if item:
287
+ all_items.append(item)
288
+ self.source_counts['wikipedia'] += 1
289
+ self._log(f"📖 Wikipedia (random): {item['text'][:50]}…")
290
+ except Exception as e:
291
+ self._log(f"⚠ Wikipedia error: {e}")
292
+
293
+ # 5. HackerNews (every other round)
294
+ if random.random() < 0.5:
295
+ try:
296
+ items = fetch_hackernews(6)
297
+ all_items.extend(items)
298
+ self.source_counts['hackernews'] += len(items)
299
+ self._log(f"💻 HackerNews +{len(items)}")
300
+ except Exception as e:
301
+ self._log(f"⚠ HN error: {e}")
302
+
303
+ for item in all_items:
304
+ self.recent_items.appendleft(item)
305
+ self.total_fetched += len(all_items)
306
+ self._log(f"✅ Round complete — {len(all_items)} new items | total: {self.total_fetched}")
307
+ return all_items
308
+
309
+ def get_stats(self) -> dict:
310
+ return {
311
+ 'total_fetched': self.total_fetched,
312
+ 'sources': dict(self.source_counts),
313
+ 'recent_log': list(self.log)[:30],
314
+ }
315
+
316
+ def get_recent_items(self, n: int = 20) -> list:
317
+ return list(self.recent_items)[:n]
image_model.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ image_model.py — A real CNN (Convolutional Neural Network) built from scratch in PyTorch.
3
+
4
+ Architecture:
5
+ Input (3 x 64 x 64 image)
6
+ → Conv2d(3, 32, 3) + BatchNorm + ReLU + MaxPool → 32 x 31 x 31
7
+ → Conv2d(32, 64, 3) + BatchNorm + ReLU + MaxPool → 64 x 14 x 14
8
+ → Conv2d(64,128, 3) + BatchNorm + ReLU + MaxPool → 128 x 6 x 6
9
+ → Flatten → Linear(128*6*6, 512) → ReLU → Dropout
10
+ → Linear(512, 128) → ReLU
11
+ → Linear(128, 8 categories)
12
+
13
+ This CNN learns to LOOK at images the same way your text network learns to READ text.
14
+ """
15
+
16
+ import torch
17
+ import threading
18
+ _CNN_LOCK = threading.Lock()
19
+ import numpy as np
20
+ import os
21
+ import json
22
+ from datetime import datetime, UTC
23
+ from pathlib import Path
24
+ from collections import defaultdict
25
+
26
+ try:
27
+ from PIL import Image
28
+ PIL_AVAILABLE = True
29
+ except ImportError:
30
+ PIL_AVAILABLE = False
31
+
32
+ IMG_SIZE = 64 # resize all images to 64x64 (small = faster on CPU)
33
+ CATEGORIES = [
34
+ 'nature', 'technology', 'science', 'people',
35
+ 'animals', 'food', 'sports', 'architecture'
36
+ ]
37
+
38
+ CNN_CHECKPOINT = 'cnn_checkpoint.pt'
39
+ CNN_STATS_FILE = 'cnn_stats.json'
40
+
41
+
42
+ # ── IMAGE PREPROCESSING ───────────────────────────────────────────────────────
43
+ def load_image_tensor(path: str, size: int = IMG_SIZE):
44
+ """Load image from disk → normalised float tensor (3, size, size)."""
45
+ if not PIL_AVAILABLE:
46
+ raise RuntimeError("Pillow not installed. Add 'Pillow' to requirements.txt")
47
+ img = Image.open(path).convert('RGB')
48
+ img = img.resize((size, size), Image.BILINEAR)
49
+ arr = np.array(img, dtype=np.float32) / 255.0
50
+ # Normalize with ImageNet mean/std (works well even for non-ImageNet data)
51
+ mean = np.array([0.485, 0.456, 0.406])
52
+ std = np.array([0.229, 0.224, 0.225])
53
+ arr = (arr - mean) / std
54
+ return torch.tensor(arr).permute(2, 0, 1) # HWC → CHW
55
+
56
+
57
+ # ── CNN MODEL ─────────────────────────────────────────────────────────────────
58
+ class ImageCNN(nn.Module):
59
+ """
60
+ A real Convolutional Neural Network.
61
+ Learns to detect edges → shapes → textures → objects, layer by layer.
62
+ Each Conv2d layer is looking for patterns the previous layer found.
63
+ """
64
+
65
+ def __init__(self, num_classes: int = 8):
66
+ super().__init__()
67
+ self.num_classes = num_classes
68
+
69
+ # Convolutional feature extractor
70
+ self.features = nn.Sequential(
71
+ # Block 1 — learns basic edges and colours
72
+ nn.Conv2d(3, 32, kernel_size=3, padding=1),
73
+ nn.BatchNorm2d(32),
74
+ nn.ReLU(),
75
+ nn.Conv2d(32, 32, kernel_size=3, padding=1),
76
+ nn.BatchNorm2d(32),
77
+ nn.ReLU(),
78
+ nn.MaxPool2d(2, 2), # 64→32
79
+ nn.Dropout2d(0.1),
80
+
81
+ # Block 2 — learns corners, curves, textures
82
+ nn.Conv2d(32, 64, kernel_size=3, padding=1),
83
+ nn.BatchNorm2d(64),
84
+ nn.ReLU(),
85
+ nn.Conv2d(64, 64, kernel_size=3, padding=1),
86
+ nn.BatchNorm2d(64),
87
+ nn.ReLU(),
88
+ nn.MaxPool2d(2, 2), # 32→16
89
+ nn.Dropout2d(0.15),
90
+
91
+ # Block 3 — learns complex shapes and object parts
92
+ nn.Conv2d(64, 128, kernel_size=3, padding=1),
93
+ nn.BatchNorm2d(128),
94
+ nn.ReLU(),
95
+ nn.Conv2d(128, 128, kernel_size=3, padding=1),
96
+ nn.BatchNorm2d(128),
97
+ nn.ReLU(),
98
+ nn.MaxPool2d(2, 2), # 16→8
99
+ nn.Dropout2d(0.2),
100
+ )
101
+
102
+ # Classifier head
103
+ self.classifier = nn.Sequential(
104
+ nn.Flatten(),
105
+ nn.Linear(128 * 8 * 8, 512),
106
+ nn.ReLU(),
107
+ nn.Dropout(0.4),
108
+ nn.Linear(512, 128),
109
+ nn.ReLU(),
110
+ nn.Linear(128, num_classes),
111
+ )
112
+
113
+ # Activation tracking for visualization
114
+ self._activations = {}
115
+ self._register_hooks()
116
+ self._initialize_weights()
117
+
118
+ def _initialize_weights(self):
119
+ for m in self.modules():
120
+ if isinstance(m, nn.Conv2d):
121
+ nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
122
+ elif isinstance(m, nn.BatchNorm2d):
123
+ nn.init.constant_(m.weight, 1)
124
+ nn.init.constant_(m.bias, 0)
125
+ elif isinstance(m, nn.Linear):
126
+ nn.init.xavier_normal_(m.weight)
127
+ nn.init.constant_(m.bias, 0)
128
+
129
+ def _register_hooks(self):
130
+ def make_hook(name):
131
+ def hook(module, inp, out):
132
+ if isinstance(out, torch.Tensor):
133
+ v = out.detach().float()
134
+ if v.dim() > 1:
135
+ v = v.mean(0)
136
+ if v.dim() > 1:
137
+ v = v.mean(-1).mean(-1) # spatial mean for conv layers
138
+ self._activations[name] = v[:16].tolist()
139
+ return hook
140
+ for i, layer in enumerate(self.features):
141
+ layer.register_forward_hook(make_hook(f'conv_{i}'))
142
+ for i, layer in enumerate(self.classifier):
143
+ layer.register_forward_hook(make_hook(f'fc_{i}'))
144
+
145
+ def forward(self, x):
146
+ x = self.features(x)
147
+ return self.classifier(x)
148
+
149
+ def get_activations(self):
150
+ return dict(self._activations)
151
+
152
+ def get_feature_maps(self, x):
153
+ """Return intermediate feature maps for visualization."""
154
+ maps = {}
155
+ for i, layer in enumerate(self.features):
156
+ x = layer(x)
157
+ if isinstance(layer, nn.ReLU):
158
+ maps[f'relu_{i}'] = x.detach()
159
+ return maps
160
+
161
+
162
+ # ── LIVING IMAGE NETWORK ──────────────────────────────────────────────────────
163
+ class LivingImageNetwork:
164
+ """
165
+ Wraps the CNN with training loop, data loading, and stats.
166
+ Trains on images downloaded by ImageFetcher.
167
+ """
168
+
169
+ def __init__(self):
170
+ self.model = ImageCNN(num_classes=len(CATEGORIES))
171
+ self.optimizer = optim.Adam(self.model.parameters(), lr=0.001, weight_decay=1e-4)
172
+ self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=50, gamma=0.8)
173
+ self.criterion = nn.CrossEntropyLoss()
174
+
175
+ self.epoch = 0
176
+ self.total_images = 0
177
+ self.loss_history = []
178
+ self.acc_history = []
179
+ self.category_counts = defaultdict(int)
180
+
181
+ self.stats = {
182
+ 'epoch': 0,
183
+ 'loss': '—',
184
+ 'accuracy': '—',
185
+ 'total_images': 0,
186
+ 'lr': 0.001,
187
+ 'last_image': '(none yet)',
188
+ 'status': 'idle',
189
+ }
190
+
191
+ self._load_checkpoint()
192
+
193
+ # ── DATA LOADING ──────────────────────────────────────────────────────────
194
+ def _load_batch(self, image_dir: Path, batch_size: int = 16):
195
+ """
196
+ Load a random batch of images from image_data/ folder.
197
+ Returns (tensor_batch, label_batch) or None if not enough images.
198
+ """
199
+ if not PIL_AVAILABLE:
200
+ return None
201
+
202
+ all_paths = []
203
+ for cat_idx, cat in enumerate(CATEGORIES):
204
+ cat_dir = image_dir / cat
205
+ if cat_dir.exists():
206
+ for p in cat_dir.iterdir():
207
+ if p.suffix.lower() in ('.jpg', '.jpeg', '.png', '.webp'):
208
+ all_paths.append((str(p), cat_idx))
209
+
210
+ if len(all_paths) < batch_size:
211
+ return None
212
+
213
+ import random
214
+ batch_paths = random.sample(all_paths, batch_size)
215
+ tensors, labels = [], []
216
+
217
+ for path, label in batch_paths:
218
+ try:
219
+ t = load_image_tensor(path)
220
+ tensors.append(t)
221
+ labels.append(label)
222
+ self.category_counts[CATEGORIES[label]] += 1
223
+ except Exception:
224
+ continue
225
+
226
+ if not tensors:
227
+ return None
228
+
229
+ return torch.stack(tensors), torch.tensor(labels, dtype=torch.long)
230
+
231
+ # ── TRAINING ──────────────────────────────────────────────────────────────
232
+ def train_step(self, image_dir: Path, batch_size: int = 16):
233
+ """One training step — load images, forward pass, backprop."""
234
+ batch = self._load_batch(image_dir, batch_size)
235
+ if batch is None:
236
+ return None
237
+
238
+ x, y = batch
239
+ x = x.float() # ensure float32
240
+ with _CNN_LOCK:
241
+ self.model.train()
242
+ self.model.zero_grad(set_to_none=True)
243
+ logits = self.model(x)
244
+ loss = self.criterion(logits, y)
245
+ loss.backward()
246
+ for p in self.model.parameters():
247
+ if p.grad is not None:
248
+ p.grad.data.clamp_(-1.0, 1.0)
249
+ self.optimizer.step()
250
+ loss_val = loss.detach().item()
251
+ acc = (logits.detach().argmax(1) == y).float().mean().item()
252
+
253
+ self.epoch += 1
254
+ self.total_images += len(x)
255
+ self.scheduler.step()
256
+
257
+ self.loss_history.append(round(loss_val, 5))
258
+ self.acc_history.append(round(acc, 4))
259
+ if len(self.loss_history) > 500:
260
+ self.loss_history = self.loss_history[-500:]
261
+ self.acc_history = self.acc_history[-500:]
262
+
263
+ last_path = batch[0] # just the paths string
264
+ self.stats.update({
265
+ 'epoch': self.epoch,
266
+ 'loss': round(loss_val, 4),
267
+ 'accuracy': round(acc * 100, 1),
268
+ 'total_images': self.total_images,
269
+ 'lr': round(self.optimizer.param_groups[0]['lr'], 7),
270
+ })
271
+
272
+ if self.epoch % 20 == 0:
273
+ self._save_checkpoint()
274
+ self._write_stats()
275
+ return loss_val
276
+
277
+ def train_n_steps(self, image_dir: Path, n: int = 20):
278
+ losses = []
279
+ for _ in range(n):
280
+ l = self.train_step(image_dir)
281
+ if l is not None:
282
+ losses.append(l)
283
+ return {
284
+ 'steps': len(losses),
285
+ 'avg_loss': round(sum(losses)/len(losses), 5) if losses else None,
286
+ }
287
+
288
+ # ── INFERENCE ─────────────────────────────────────────────────────────────
289
+ def predict_image(self, image_path: str) -> dict:
290
+ """Predict category of a single image."""
291
+ if not PIL_AVAILABLE:
292
+ return {'error': 'Pillow not installed'}
293
+ try:
294
+ t = load_image_tensor(image_path).unsqueeze(0)
295
+ self.model.eval()
296
+ with torch.no_grad():
297
+ logits = self.model(t)
298
+ probs = torch.softmax(logits, dim=1)[0].tolist()
299
+ pred = int(logits.argmax(1).item())
300
+ return {
301
+ 'prediction': CATEGORIES[pred],
302
+ 'confidence': round(probs[pred] * 100, 1),
303
+ 'all_probs': {c: round(p*100, 2) for c, p in zip(CATEGORIES, probs)},
304
+ }
305
+ except Exception as e:
306
+ return {'error': str(e)}
307
+
308
+ # ── VIZ STATE ─────────────────────────────────────────────────────────────
309
+ def get_viz_state(self) -> dict:
310
+ self.model.eval()
311
+ with torch.no_grad():
312
+ dummy = torch.zeros(1, 3, IMG_SIZE, IMG_SIZE)
313
+ self.model(dummy)
314
+ return {
315
+ 'layer_sizes': [3, 32, 64, 128, 512, 128, len(CATEGORIES)],
316
+ 'activations': self.model.get_activations(),
317
+ 'loss_history': self.loss_history[-100:],
318
+ 'acc_history': self.acc_history[-100:],
319
+ 'stats': self.stats,
320
+ 'type': 'cnn',
321
+ }
322
+
323
+ # ── PERSISTENCE ───────────────────────────────────────────────────────────
324
+ def _save_checkpoint(self):
325
+ try:
326
+ torch.save({
327
+ 'model': self.model.state_dict(),
328
+ 'optimizer': self.optimizer.state_dict(),
329
+ 'epoch': self.epoch,
330
+ 'total_images': self.total_images,
331
+ 'loss_history': self.loss_history,
332
+ 'acc_history': self.acc_history,
333
+ 'category_counts': dict(self.category_counts),
334
+ }, CNN_CHECKPOINT)
335
+ except Exception:
336
+ pass
337
+
338
+ def _load_checkpoint(self):
339
+ if not os.path.exists(CNN_CHECKPOINT):
340
+ return
341
+ try:
342
+ ck = torch.load(CNN_CHECKPOINT, map_location='cpu')
343
+ self.model.load_state_dict(ck['model'])
344
+ self.optimizer.load_state_dict(ck['optimizer'])
345
+ self.epoch = ck.get('epoch', 0)
346
+ self.total_images = ck.get('total_images', 0)
347
+ self.loss_history = ck.get('loss_history', [])
348
+ self.acc_history = ck.get('acc_history', [])
349
+ self.category_counts = defaultdict(int, ck.get('category_counts', {}))
350
+ if self.epoch > 0:
351
+ self.stats.update({
352
+ 'epoch': self.epoch,
353
+ 'total_images': self.total_images,
354
+ })
355
+ except Exception:
356
+ pass
357
+
358
+ def _write_stats(self):
359
+ try:
360
+ with open(CNN_STATS_FILE, 'w') as f:
361
+ json.dump(self.stats, f, indent=2)
362
+ except Exception:
363
+ pass
neural_network.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ neural_network.py — Real PyTorch neural network built from scratch.
3
+
4
+ KEY CHANGES:
5
+ - Every item ingested is IMMEDIATELY written to knowledge.jsonl
6
+ - On startup, knowledge.jsonl is read back → data_buffer is restored
7
+ - Model checkpoint auto-saves every 30 training epochs
8
+ - training_stats.json written after every training step (human-readable)
9
+ """
10
+
11
+ import torch
12
+ import threading
13
+ _TEXT_LOCK = threading.Lock()
14
+ import numpy as np
15
+ import os
16
+ import json
17
+ import re
18
+ from datetime import datetime, UTC
19
+ from collections import Counter, defaultdict
20
+
21
+ # ─── FILES ON DISK ────────────────────────────────────────────────────────────
22
+ KNOWLEDGE_FILE = 'knowledge.jsonl' # Every article/text the AI has seen
23
+ CHECKPOINT_FILE = 'model_checkpoint.pt' # PyTorch weights + optimizer state
24
+ STATS_FILE = 'training_stats.json' # Human-readable live stats
25
+
26
+ # ─── CATEGORIES ───────────────────────────────────────────────────────────────
27
+ CATEGORIES = ['technology', 'science', 'world', 'sports',
28
+ 'business', 'health', 'entertainment', 'other']
29
+
30
+ # ─── VOCABULARY ───────────────────────────────────────────────────────────────
31
+ STOPWORDS = {
32
+ 'a','an','the','is','it','in','on','at','to','for','of','and','or','but',
33
+ 'was','are','were','be','been','have','has','had','do','does','did','will',
34
+ 'would','could','should','may','might','that','this','these','those','with',
35
+ 'from','by','as','not','also','than','then','so','if','when','what','how',
36
+ 'who','which','its','their','our','your','my','his','her','we','they','he',
37
+ 'she','you','i','me','him','us','them','said','says','new','one','two',
38
+ }
39
+
40
+ class Vocabulary:
41
+ def __init__(self, max_size=10000):
42
+ self.word2idx = {'<PAD>': 0, '<UNK>': 1}
43
+ self.idx2word = {0: '<PAD>', 1: '<UNK>'}
44
+ self.word_counts = Counter()
45
+ self.max_size = max_size
46
+ self.is_built = False
47
+
48
+ def update(self, text: str):
49
+ self.word_counts.update(self._tokenize(text))
50
+
51
+ def build(self):
52
+ top = self.word_counts.most_common(self.max_size - 2)
53
+ self.word2idx = {'<PAD>': 0, '<UNK>': 1}
54
+ self.idx2word = {0: '<PAD>', 1: '<UNK>'}
55
+ for i, (word, _) in enumerate(top):
56
+ idx = i + 2
57
+ self.word2idx[word] = idx
58
+ self.idx2word[idx] = word
59
+ self.is_built = True
60
+
61
+ def encode(self, text: str, max_len: int = 64) -> list:
62
+ words = self._tokenize(text)[:max_len]
63
+ ids = [self.word2idx.get(w, 1) for w in words]
64
+ ids += [0] * (max_len - len(ids))
65
+ return ids
66
+
67
+ def _tokenize(self, text: str) -> list:
68
+ text = text.lower()
69
+ text = re.sub(r'[^\w\s]', ' ', text)
70
+ return [w for w in text.split() if w not in STOPWORDS and len(w) > 2]
71
+
72
+ def __len__(self):
73
+ return len(self.word2idx)
74
+
75
+
76
+ # ─── MODEL ────────────────────────────────────────────────────────────────────
77
+ class TextClassifier(nn.Module):
78
+ def __init__(self, vocab_size=10002, embed_dim=64,
79
+ hidden=[256, 128, 64], num_classes=8):
80
+ super().__init__()
81
+ self.embed_dim = embed_dim
82
+ self.hidden_dims = hidden
83
+ self.num_classes = num_classes
84
+
85
+ self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
86
+ nn.init.normal_(self.embedding.weight, 0, 0.1)
87
+
88
+ layers = []
89
+ in_dim = embed_dim
90
+ for h in hidden:
91
+ layers += [nn.Linear(in_dim, h), nn.LayerNorm(h),
92
+ nn.ReLU(), nn.Dropout(0.25)]
93
+ in_dim = h
94
+ layers.append(nn.Linear(in_dim, num_classes))
95
+ self.net = nn.Sequential(*layers)
96
+
97
+ self._activations = {}
98
+ self._register_hooks()
99
+
100
+ def _register_hooks(self):
101
+ def make_hook(name):
102
+ def hook(module, inp, out):
103
+ if isinstance(out, torch.Tensor):
104
+ v = out.detach().float()
105
+ if v.dim() > 1:
106
+ v = v.mean(0)
107
+ self._activations[name] = v[:32].tolist()
108
+ return hook
109
+ for i, layer in enumerate(self.net):
110
+ layer.register_forward_hook(make_hook(f'net.{i}'))
111
+
112
+ def forward(self, x):
113
+ emb = self.embedding(x)
114
+ mask = (x != 0).float().unsqueeze(-1)
115
+ pooled = (emb * mask).sum(1) / mask.sum(1).clamp(min=1)
116
+ return self.net(pooled)
117
+
118
+ def get_activations(self) -> dict:
119
+ return dict(self._activations)
120
+
121
+ def get_weight_info(self) -> dict:
122
+ info = {}
123
+ for name, param in self.named_parameters():
124
+ if 'weight' in name and param.dim() == 2:
125
+ w = param.detach().float().numpy()
126
+ r, c = min(w.shape[0], 16), min(w.shape[1], 16)
127
+ info[name] = {
128
+ 'shape': list(w.shape),
129
+ 'mean_abs': float(np.mean(np.abs(w))),
130
+ 'std': float(np.std(w)),
131
+ 'sample': w[:r, :c].tolist(),
132
+ }
133
+ return info
134
+
135
+
136
+ # ─── LIVING NETWORK ───────────────────────────────────────────────────────────
137
+ class LivingNetwork:
138
+ """
139
+ The brain. Wraps the PyTorch model with:
140
+ - knowledge.jsonl → persistent record of everything it has read
141
+ - model_checkpoint.pt → saved weights (restored on restart)
142
+ - training_stats.json → live stats readable by the UI
143
+ """
144
+
145
+ AUTO_SAVE_EVERY = 30 # Save checkpoint every N training epochs
146
+
147
+ def __init__(self):
148
+ self.vocab = Vocabulary()
149
+ self.model = TextClassifier()
150
+ self.optimizer = optim.Adam(self.model.parameters(), lr=0.001, weight_decay=1e-5)
151
+ self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
152
+ self.optimizer, mode='min', patience=20, factor=0.5, min_lr=1e-5)
153
+ self.criterion = nn.CrossEntropyLoss()
154
+
155
+ self.epoch = 0
156
+ self.total_samples = 0
157
+ self.data_buffer = [] # (text, label_int) — in-memory training pool
158
+ self.loss_history = []
159
+ self.acc_history = []
160
+ self.category_counts = defaultdict(int)
161
+ self.knowledge_count = 0 # Total articles ever ingested
162
+
163
+ self.stats = {
164
+ 'epoch': 0,
165
+ 'loss': '—',
166
+ 'accuracy': '—',
167
+ 'total_samples': 0,
168
+ 'lr': 0.001,
169
+ 'buffer_size': 0,
170
+ 'knowledge_count': 0,
171
+ 'last_text': '(nothing yet)',
172
+ 'vocab_size': 2,
173
+ 'status': 'idle',
174
+ }
175
+
176
+ # Load checkpoint first, then restore knowledge buffer
177
+ self._load_checkpoint()
178
+ self._load_knowledge()
179
+
180
+ # ── KNOWLEDGE FILE ────────────────────────────────────────────────────────
181
+ def _write_knowledge(self, text: str, category: str, source: str = 'unknown'):
182
+ """Append one learned item to knowledge.jsonl immediately."""
183
+ record = {
184
+ 'text': text,
185
+ 'category': category,
186
+ 'source': source,
187
+ 'timestamp': datetime.now(UTC).isoformat(),
188
+ 'epoch_at_ingestion': self.epoch,
189
+ }
190
+ try:
191
+ with open(KNOWLEDGE_FILE, 'a', encoding='utf-8') as f:
192
+ f.write(json.dumps(record, ensure_ascii=False) + '\n')
193
+ self.knowledge_count += 1
194
+ except Exception:
195
+ pass
196
+
197
+ def _load_knowledge(self):
198
+ """On startup: read knowledge.jsonl and rebuild data_buffer + vocab."""
199
+ if not os.path.exists(KNOWLEDGE_FILE):
200
+ return
201
+ loaded = 0
202
+ try:
203
+ with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
204
+ for line in f:
205
+ line = line.strip()
206
+ if not line:
207
+ continue
208
+ try:
209
+ rec = json.loads(line)
210
+ text = rec.get('text', '')
211
+ cat = rec.get('category', 'other')
212
+ label = CATEGORIES.index(cat) if cat in CATEGORIES else 7
213
+ if len(text) > 20:
214
+ self.data_buffer.append((text, label))
215
+ self.vocab.update(text)
216
+ self.category_counts[cat] += 1
217
+ loaded += 1
218
+ except Exception:
219
+ continue
220
+ self.knowledge_count = loaded
221
+ if loaded > 0:
222
+ self.vocab.build()
223
+ # Trim buffer if huge
224
+ if len(self.data_buffer) > 5000:
225
+ self.data_buffer = self.data_buffer[-4000:]
226
+ except Exception:
227
+ pass
228
+ self.stats['knowledge_count'] = self.knowledge_count
229
+ self.stats['buffer_size'] = len(self.data_buffer)
230
+ self.stats['vocab_size'] = len(self.vocab)
231
+
232
+ def get_knowledge_file_stats(self) -> dict:
233
+ """Return stats about the knowledge file for the UI."""
234
+ if not os.path.exists(KNOWLEDGE_FILE):
235
+ return {'exists': False, 'lines': 0, 'size_kb': 0}
236
+ size = os.path.getsize(KNOWLEDGE_FILE)
237
+ lines = 0
238
+ try:
239
+ with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
240
+ lines = sum(1 for l in f if l.strip())
241
+ except Exception:
242
+ pass
243
+ return {'exists': True, 'lines': lines, 'size_kb': round(size / 1024, 1)}
244
+
245
+ def get_recent_knowledge(self, n: int = 20) -> list:
246
+ """Return last N items from knowledge.jsonl for display."""
247
+ if not os.path.exists(KNOWLEDGE_FILE):
248
+ return []
249
+ lines = []
250
+ try:
251
+ with open(KNOWLEDGE_FILE, 'r', encoding='utf-8') as f:
252
+ all_lines = [l.strip() for l in f if l.strip()]
253
+ for line in reversed(all_lines[-n:]):
254
+ try:
255
+ lines.append(json.loads(line))
256
+ except Exception:
257
+ pass
258
+ except Exception:
259
+ pass
260
+ return lines
261
+
262
+ # ── INGEST ────────────────────────────────────────────────────────────────
263
+ def ingest(self, text: str, category: str, source: str = 'unknown'):
264
+ """
265
+ Add text to training buffer AND write to knowledge.jsonl immediately.
266
+ This is how the AI 'remembers' what it has learned.
267
+ """
268
+ cleaned = text.strip()
269
+ if len(cleaned) < 20:
270
+ return
271
+ label = CATEGORIES.index(category) if category in CATEGORIES else 7
272
+
273
+ # ① Write to disk first — never lose this
274
+ self._write_knowledge(cleaned, category, source)
275
+
276
+ # ② Add to in-memory training buffer
277
+ self.data_buffer.append((cleaned, label))
278
+ self.vocab.update(cleaned)
279
+ self.category_counts[category] += 1
280
+
281
+ # Rebuild vocab every 25 items
282
+ if len(self.data_buffer) % 25 == 0:
283
+ self.vocab.build()
284
+
285
+ # Keep buffer bounded (disk has the full history)
286
+ if len(self.data_buffer) > 5000:
287
+ self.data_buffer = self.data_buffer[-4000:]
288
+
289
+ self.stats.update({
290
+ 'buffer_size': len(self.data_buffer),
291
+ 'vocab_size': len(self.vocab),
292
+ 'knowledge_count': self.knowledge_count,
293
+ })
294
+
295
+ # ── TRAINING ──────────────────────────────────────────────────────────────
296
+ def train_step(self, batch_size: int = 32) -> float | None:
297
+ if len(self.data_buffer) < batch_size or not self.vocab.is_built:
298
+ return None
299
+ with _TEXT_LOCK:
300
+ self.model.train()
301
+ idx = np.random.choice(len(self.data_buffer), batch_size, replace=False)
302
+ batch = [self.data_buffer[i] for i in idx]
303
+ texts, labels = zip(*batch)
304
+
305
+ x = torch.tensor([self.vocab.encode(t) for t in texts], dtype=torch.long)
306
+ y = torch.tensor(list(labels), dtype=torch.long)
307
+
308
+ self.model.zero_grad(set_to_none=True)
309
+ logits = self.model(x)
310
+ loss = self.criterion(logits, y)
311
+ loss.backward()
312
+ for p in self.model.parameters():
313
+ if p.grad is not None:
314
+ p.grad.data.clamp_(-1.0, 1.0)
315
+ self.optimizer.step()
316
+ loss_val = loss.detach().item()
317
+ acc = (logits.detach().argmax(1) == y).float().mean().item()
318
+
319
+ self.epoch += 1
320
+ self.total_samples += batch_size
321
+ self.scheduler.step(loss_val)
322
+
323
+ self.loss_history.append(round(loss_val, 5))
324
+ self.acc_history.append(round(acc, 4))
325
+ if len(self.loss_history) > 500:
326
+ self.loss_history = self.loss_history[-500:]
327
+ self.acc_history = self.acc_history[-500:]
328
+
329
+ self.stats.update({
330
+ 'epoch': self.epoch,
331
+ 'loss': round(loss_val, 4),
332
+ 'accuracy': round(acc * 100, 1),
333
+ 'total_samples': self.total_samples,
334
+ 'lr': round(self.optimizer.param_groups[0]['lr'], 7),
335
+ 'buffer_size': len(self.data_buffer),
336
+ 'last_text': texts[0][:120],
337
+ 'vocab_size': len(self.vocab),
338
+ 'knowledge_count': self.knowledge_count,
339
+ })
340
+
341
+ # Auto-save checkpoint every N epochs
342
+ if self.epoch % self.AUTO_SAVE_EVERY == 0:
343
+ self.save_checkpoint()
344
+
345
+ # Always write stats file so UI can read without waiting
346
+ self._write_stats_file()
347
+
348
+ return loss_val
349
+
350
+ def train_n_steps(self, n: int = 50) -> dict:
351
+ losses = []
352
+ for _ in range(n):
353
+ l = self.train_step()
354
+ if l is not None:
355
+ losses.append(l)
356
+ return {
357
+ 'steps': len(losses),
358
+ 'avg_loss': round(sum(losses) / len(losses), 5) if losses else None,
359
+ }
360
+
361
+ # ── INFERENCE ───────────────────────────────────────────���─────────────────
362
+ def predict(self, text: str) -> dict:
363
+ if not self.vocab.is_built or not text.strip():
364
+ return {'error': 'Model not ready — start the network and let it train first'}
365
+ self.model.eval()
366
+ with torch.no_grad():
367
+ x = torch.tensor([self.vocab.encode(text)], dtype=torch.long)
368
+ logits = self.model(x)
369
+ probs = torch.softmax(logits, dim=1)[0].tolist()
370
+ pred = int(logits.argmax(1).item())
371
+ return {
372
+ 'prediction': CATEGORIES[pred],
373
+ 'confidence': round(probs[pred] * 100, 1),
374
+ 'all_probs': {c: round(p * 100, 2) for c, p in zip(CATEGORIES, probs)},
375
+ }
376
+
377
+ # ── VIZ STATE ─────────────────────────────────────────────────────────────
378
+ def get_viz_state(self) -> dict:
379
+ self.model.eval()
380
+ with torch.no_grad():
381
+ dummy = torch.zeros(1, 64, dtype=torch.long)
382
+ self.model(dummy)
383
+ return {
384
+ 'layer_sizes': [self.model.embed_dim] + self.model.hidden_dims + [self.model.num_classes],
385
+ 'activations': self.model.get_activations(),
386
+ 'weights': self.model.get_weight_info(),
387
+ 'loss_history': self.loss_history[-100:],
388
+ 'acc_history': self.acc_history[-100:],
389
+ 'stats': self.stats,
390
+ 'category_counts':dict(self.category_counts),
391
+ }
392
+
393
+ # ── PERSISTENCE ───────────────────────────────────────────────────────────
394
+ def save_checkpoint(self):
395
+ try:
396
+ torch.save({
397
+ 'model': self.model.state_dict(),
398
+ 'optimizer': self.optimizer.state_dict(),
399
+ 'epoch': self.epoch,
400
+ 'total_samples': self.total_samples,
401
+ 'loss_history': self.loss_history,
402
+ 'acc_history': self.acc_history,
403
+ 'vocab_word2idx': self.vocab.word2idx,
404
+ 'category_counts': dict(self.category_counts),
405
+ 'stats': self.stats,
406
+ }, CHECKPOINT_FILE)
407
+ return True
408
+ except Exception:
409
+ return False
410
+
411
+ def _load_checkpoint(self):
412
+ if not os.path.exists(CHECKPOINT_FILE):
413
+ return False
414
+ try:
415
+ ck = torch.load(CHECKPOINT_FILE, map_location='cpu')
416
+ self.model.load_state_dict(ck['model'])
417
+ self.optimizer.load_state_dict(ck['optimizer'])
418
+ self.epoch = ck.get('epoch', 0)
419
+ self.total_samples = ck.get('total_samples', 0)
420
+ self.loss_history = ck.get('loss_history', [])
421
+ self.acc_history = ck.get('acc_history', [])
422
+ self.category_counts = defaultdict(int, ck.get('category_counts', {}))
423
+ self.stats = ck.get('stats', self.stats)
424
+ w2i = ck.get('vocab_word2idx', {})
425
+ if w2i:
426
+ self.vocab.word2idx = w2i
427
+ self.vocab.idx2word = {v: k for k, v in w2i.items()}
428
+ self.vocab.is_built = len(w2i) > 2
429
+ return True
430
+ except Exception:
431
+ return False
432
+
433
+ def _write_stats_file(self):
434
+ """Write human-readable stats to training_stats.json for easy debugging."""
435
+ try:
436
+ with open(STATS_FILE, 'w') as f:
437
+ json.dump({**self.stats, 'loss_last10': self.loss_history[-10:]}, f, indent=2)
438
+ except Exception:
439
+ pass
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.5.0
2
+ feedparser==6.0.11
3
+ requests==2.31.0
4
+ beautifulsoup4==4.12.3
5
+ numpy>=1.26.4