appbinary-inspector 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: appbinary-inspector
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Open-source lazy-card character model plus static binary inspection tools.
5
5
  Author: William Laurento
6
6
  License: MIT
@@ -72,6 +72,29 @@ appbin card-train my-card-model training.txt --steps 500
72
72
  appbin card-generate my-card-model "Hello" --characters 80
73
73
  ```
74
74
 
75
+ ## Text chat interface
76
+
77
+ Open a local chat box after training a model. It binds only to your computer,
78
+ opens in your browser, and never sends prompts to a remote service.
79
+
80
+ ```bash
81
+ appbin chat my-card-model
82
+ ```
83
+
84
+ ## Python import API
85
+
86
+ Use the model from normal Python code without a terminal command interface:
87
+
88
+ ```python
89
+ from appbinary_inspector import appbincardcr
90
+
91
+ mycard = appbincardcr("my-card-model")
92
+ reply = mycard("Explain gravity", output=True)
93
+
94
+ # Or call it in one line:
95
+ reply = appbincardcr(mycard, "Explain gravity", output=True)
96
+ ```
97
+
75
98
  This reduces active model-card memory. It does not make a dense, unmodified
76
99
  70B model fit instantly on a low-VRAM GPU: disk bandwidth and CPU/GPU transfer
77
100
  time become the tradeoff, and models need to be trained for sparse routing.
@@ -57,6 +57,29 @@ appbin card-train my-card-model training.txt --steps 500
57
57
  appbin card-generate my-card-model "Hello" --characters 80
58
58
  ```
59
59
 
60
+ ## Text chat interface
61
+
62
+ Open a local chat box after training a model. It binds only to your computer,
63
+ opens in your browser, and never sends prompts to a remote service.
64
+
65
+ ```bash
66
+ appbin chat my-card-model
67
+ ```
68
+
69
+ ## Python import API
70
+
71
+ Use the model from normal Python code without a terminal command interface:
72
+
73
+ ```python
74
+ from appbinary_inspector import appbincardcr
75
+
76
+ mycard = appbincardcr("my-card-model")
77
+ reply = mycard("Explain gravity", output=True)
78
+
79
+ # Or call it in one line:
80
+ reply = appbincardcr(mycard, "Explain gravity", output=True)
81
+ ```
82
+
60
83
  This reduces active model-card memory. It does not make a dense, unmodified
61
84
  70B model fit instantly on a low-VRAM GPU: disk bandwidth and CPU/GPU transfer
62
85
  time become the tradeoff, and models need to be trained for sparse routing.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "appbinary-inspector"
7
- version = "0.4.0"
7
+ version = "0.6.0"
8
8
  description = "Open-source lazy-card character model plus static binary inspection tools."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,6 +4,7 @@ from .analyzer import inspect_file
4
4
  from .extract import extract_binary_parts, extract_embedded
5
5
  from .tokenizer import WordTokenizer
6
6
  from .cardstack import CardStackConfig, CardStackLM, train_next_character
7
+ from .card_api import Card, appbincardcr
7
8
 
8
- __all__ = ["inspect_file", "extract_embedded", "extract_binary_parts", "WordTokenizer", "CardStackConfig", "CardStackLM", "train_next_character"]
9
- __version__ = "0.4.0"
9
+ __all__ = ["inspect_file", "extract_embedded", "extract_binary_parts", "WordTokenizer", "CardStackConfig", "CardStackLM", "train_next_character", "Card", "appbincardcr"]
10
+ __version__ = "0.6.0"
@@ -0,0 +1,37 @@
1
+ """Small Python API for using a disk-paged CardStack model in normal code."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from .cardstack import CardStackLM
7
+
8
+
9
+ class Card:
10
+ """A callable local text model.
11
+
12
+ Example:
13
+ mycard = appbincardcr("my-card-model")
14
+ reply = mycard("Explain gravity", output=True)
15
+ """
16
+
17
+ def __init__(self, model: str | Path | CardStackLM) -> None:
18
+ self.model = CardStackLM.load(model) if isinstance(model, (str, Path)) else model
19
+
20
+ def __call__(self, text: str, output: bool = False, characters: int = 180, temperature: float = 0.8) -> str:
21
+ reply = self.model.generate(text, new_characters=characters, temperature=temperature)
22
+ if output:
23
+ print(reply)
24
+ return reply
25
+
26
+ def reply(self, text: str, **options) -> str:
27
+ return self(text, **options)
28
+
29
+
30
+ def appbincardcr(mycard: str | Path | CardStackLM | Card, text: str | None = None, output: bool = False, **options):
31
+ """Create or call a CardStack text model with one short function.
32
+
33
+ ``appbincardcr("my-card-model")`` returns a reusable card.
34
+ ``appbincardcr(mycard, "Hello", output=True)`` generates and prints text.
35
+ """
36
+ card = mycard if isinstance(mycard, Card) else Card(mycard)
37
+ return card if text is None else card(text, output=output, **options)
@@ -0,0 +1,67 @@
1
+ """Local browser chat UI for a trained CardStack model."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import threading
6
+ import webbrowser
7
+ from http import HTTPStatus
8
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
+ from pathlib import Path
10
+
11
+ from .cardstack import CardStackLM
12
+
13
+ PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CardStack Chat</title><style>
14
+ :root{--bg:#0a0d13;--panel:#121823;--line:#263142;--text:#f1f5f9;--muted:#9dacbf;--accent:#67e8f9;--user:#193352}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:16px system-ui,-apple-system,Segoe UI,sans-serif}.shell{max-width:920px;min-height:100vh;margin:auto;padding:28px 18px;display:flex;flex-direction:column;gap:20px}header{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:1px solid var(--line);padding-bottom:18px}h1{font-size:22px;margin:0 0 5px}p{margin:0;color:var(--muted)}.badge{font-size:12px;color:var(--accent);border:1px solid #1d6976;border-radius:999px;padding:6px 9px}.chat{flex:1;display:flex;flex-direction:column;gap:14px}.empty{margin:auto;text-align:center;max-width:470px}.empty h2{font-size:24px;margin-bottom:8px}.message{max-width:84%;padding:13px 15px;border:1px solid var(--line);border-radius:14px;white-space:pre-wrap;line-height:1.45}.assistant{align-self:flex-start;background:var(--panel)}.user{align-self:flex-end;background:var(--user)}form{display:flex;gap:10px;position:sticky;bottom:0;background:var(--bg);padding-top:12px}textarea{resize:none;min-height:54px;max-height:180px;flex:1;border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:12px;padding:15px;font:inherit}button{border:0;border-radius:12px;padding:0 18px;background:var(--accent);color:#062027;font-weight:700;cursor:pointer}button:disabled{opacity:.55;cursor:wait}.hint{font-size:12px;color:var(--muted);text-align:center}@media(max-width:560px){.shell{padding:18px 12px}.badge{display:none}.message{max-width:94%}button{padding:0 14px}}</style></head><body><main class="shell"><header><div><h1>CardStack Chat</h1><p>Disk-paged character model · cards load only when routed</p></div><span class="badge">LOCAL ONLY</span></header><section class="chat" id="chat"><div class="empty" id="empty"><h2>Start a conversation</h2><p>This model produces one next character at a time. Quality depends on the text used to train its cards.</p></div></section><form id="form"><textarea id="prompt" aria-label="Message" placeholder="Type a message" required></textarea><button id="send" type="submit">Send</button></form><div class="hint">Enter to send · Shift+Enter for a new line</div></main><script>const chat=document.querySelector('#chat'),empty=document.querySelector('#empty'),form=document.querySelector('#form'),prompt=document.querySelector('#prompt'),send=document.querySelector('#send');function add(kind,text){empty?.remove();const el=document.createElement('div');el.className='message '+kind;el.textContent=text;chat.append(el);el.scrollIntoView({block:'end',behavior:'smooth'});return el}form.addEventListener('submit',async e=>{e.preventDefault();const text=prompt.value.trim();if(!text)return;add('user',text);prompt.value='';send.disabled=true;const reply=add('assistant','Thinking…');try{const r=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:text,characters:180})});const body=await r.json();reply.textContent=body.text||body.error||'No response.'}catch(err){reply.textContent='Could not reach the local model: '+err.message}finally{send.disabled=false;prompt.focus()}});prompt.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();form.requestSubmit()}});prompt.focus();</script></body></html>"""
15
+
16
+
17
+ def serve_chat(model_directory: str | Path, host: str = "127.0.0.1", port: int = 8765, open_browser: bool = True) -> None:
18
+ """Serve a local-only chat box for a disk-paged CardStack model."""
19
+ model = CardStackLM.load(model_directory)
20
+ model_lock = threading.Lock()
21
+
22
+ class Handler(BaseHTTPRequestHandler):
23
+ def _send(self, status: int, content_type: str, body: str) -> None:
24
+ encoded = body.encode("utf-8")
25
+ self.send_response(status)
26
+ self.send_header("Content-Type", f"{content_type}; charset=utf-8")
27
+ self.send_header("Content-Length", str(len(encoded)))
28
+ self.end_headers()
29
+ self.wfile.write(encoded)
30
+
31
+ def do_GET(self) -> None:
32
+ if self.path != "/":
33
+ self._send(HTTPStatus.NOT_FOUND, "text/plain", "Not found")
34
+ return
35
+ self._send(HTTPStatus.OK, "text/html", PAGE)
36
+
37
+ def do_POST(self) -> None:
38
+ if self.path != "/api/generate":
39
+ self._send(HTTPStatus.NOT_FOUND, "application/json", '{"error":"Not found"}')
40
+ return
41
+ try:
42
+ size = int(self.headers.get("Content-Length", "0"))
43
+ request = json.loads(self.rfile.read(min(size, 20_000)))
44
+ prompt = str(request.get("prompt", "")).strip()
45
+ characters = max(1, min(int(request.get("characters", 180)), 500))
46
+ if not prompt:
47
+ raise ValueError("Write a message first.")
48
+ with model_lock:
49
+ text = model.generate(prompt, new_characters=characters)
50
+ self._send(HTTPStatus.OK, "application/json", json.dumps({"text": text}))
51
+ except Exception as error:
52
+ self._send(HTTPStatus.BAD_REQUEST, "application/json", json.dumps({"error": str(error)}))
53
+
54
+ def log_message(self, *_args) -> None:
55
+ return
56
+
57
+ server = ThreadingHTTPServer((host, port), Handler)
58
+ address = f"http://{host}:{port}"
59
+ if open_browser:
60
+ webbrowser.open(address)
61
+ print(f"CardStack Chat is running at {address}. Press Ctrl+C to stop.")
62
+ try:
63
+ server.serve_forever()
64
+ except KeyboardInterrupt:
65
+ pass
66
+ finally:
67
+ server.server_close()
@@ -10,6 +10,7 @@ from .extract import extract_binary_parts
10
10
  from .tokenizer import WordTokenizer
11
11
  from .torch_builder import PRESETS
12
12
  from .cardstack import CardStackConfig, CardStackLM, train_next_character
13
+ from .chat_server import serve_chat
13
14
 
14
15
  def main() -> None:
15
16
  parser = argparse.ArgumentParser(description="Static application-binary inspector. Never executes targets.")
@@ -44,6 +45,10 @@ def main() -> None:
44
45
  card_generate.add_argument("model")
45
46
  card_generate.add_argument("prompt")
46
47
  card_generate.add_argument("--characters", type=int, default=80)
48
+ chat_cmd = commands.add_parser("chat", help="open a local browser chat box for a trained CardStack model")
49
+ chat_cmd.add_argument("model")
50
+ chat_cmd.add_argument("--port", type=int, default=8765)
51
+ chat_cmd.add_argument("--no-browser", action="store_true")
47
52
  args = parser.parse_args()
48
53
  if args.command == "inspect":
49
54
  result = inspect_file(args.file, args.strings)
@@ -67,6 +72,9 @@ def main() -> None:
67
72
  result = {"model": args.model, "steps": args.steps, "loss_samples": losses}
68
73
  elif args.command == "card-generate":
69
74
  result = {"text": CardStackLM.load(args.model).generate(args.prompt, new_characters=args.characters)}
75
+ elif args.command == "chat":
76
+ serve_chat(args.model, port=args.port, open_browser=not args.no_browser)
77
+ return
70
78
  else:
71
79
  result = PRESETS[args.name]
72
80
  print(json.dumps(result, indent=2))