Collected sources and patterns will appear here. Add from search or the patterns library.
Scan a QR code to chat with your home's local LLM (Ollama/LM Studio) from any phone: auto-discovers the running server, tunnels it securely, no cloud accounts or port forwarding.
1. **Start your local LLM engine** (e.g., `ollama run llama3`) and confirm local queries work. 2. **Launch the Bridge**: `python bridge.py` 3. **Verify Terminal Logs**: `[+] Found Ollama running on port 11434!` (or your engine), tunnel creation message, and an ASCII QR code rendering in the terminal. 4. **Scan and Connect**: Point your phone camera at the QR code, tap the link (e.g., `https://xxxx.lhr.life`). The phone browser should load the Home Private AI UI showing the detected engine. 5. **Chat**: Send a message from the phone; verify the prompt resolves on the home machine and the response returns to the phone.
# Private AI Bridge — Tap Your Home LLM From Anywhere via QR Code
## Overview
This capability allows anyone in a household to access powerful local AI models (such as Ollama or LM Studio) running on a home computer directly from their mobile devices (iOS/Android). By combining auto-discovery of local LLM servers with an on-the-fly secure reverse tunnel, the system generates a scanable QR code on the host machine. Scanning this QR code opens a private, secure web chat interface on any phone, routing traffic back to the home LLM without requiring dynamic DNS, firewall port forwarding, or cloud API keys.
Designed specifically for non-technical households who want the privacy of local AI with the convenience of mobile access.
---
## Architecture
```
[Local LLM] (Ollama/LM Studio)
│
▼ (Port auto-discovered: e.g., 11434)
┌──────────────────────────────────────────┐
│ Private AI Bridge (Local Web Host) │ <--- Glue Web UI (Port 4000)
└──────────────────────────────────────────┘
│
▼ (Local Port 4000)
┌──────────────────────────────────────────┐
│ QR-Paired Reverse Tunneling Atom │
└──────────────────────────────────────────┘
│
▼ (Encrypted SSH Tunnel)
[Secure Public URL] (localhost.run)
│
▼ (Scan QR Code)
[Mobile Web Browser] (Phone/Tablet)
```
### Data Flow & Wiring
1. **Discovery**: The `local LLM service auto-discovery` atom probes loopback ports on the host system to find active LLM servers (e.g., Ollama on `11434`, LM Studio on `1234`).
2. **Bridge Web Server (Glue)**: A lightweight local web server starts on the host machine (default port `4000`). It hosts a mobile-friendly chat UI and acts as a CORS-compliant proxy to the auto-discovered LLM API.
3. **Tunneling**: The `QR-paired reverse tunneling` atom consumes the local port (`4000`) of the Bridge Web Server and establishes a secure outbound SSH tunnel to a public relay (`localhost.run`).
4. **QR Generation**: The tunneling atom extracts the public HTTPS URL from the tunnel connection and converts it into a terminal-friendly QR code.
5. **Client Connection**: A mobile device scans the QR code, opening the secure HTTPS tunnel link. The mobile browser loads the chat interface, sending prompts through the secure tunnel back to the local LLM.
### Gaps Handled by Glue Code
* **Web UI Proxy**: The raw LLM APIs do not serve a consumer-friendly chat website directly and typically lack CORS configurations for cross-device mobile browsers. The glue code runs a lightweight Python web server that serves a chat UI and proxies API requests locally.
---
## Components
### local LLM service auto-discovery [read]
* **Description**: Probes common local ports to detect running LLM engines and their API flavors (Ollama, OpenAI-compatible, etc.).
* **Interface**: `Null -> List<InferenceEndpoint>`
* **Source**: `openyak/openyak`
### QR-paired reverse tunneling [write]
* **Description**: Establishes an outbound reverse SSH tunnel to expose a local port securely to the internet, and outputs a QR code containing the dynamic URL.
* **Interface**: `LocalPort -> QRCodeImage`
* **Source**: `openyak/openyak`
---
## Build
### Prerequisites
* Python 3.8+
* SSH client (standard on macOS, Linux, Windows 10/11)
* A local LLM runner installed and running (Ollama or LM Studio)
### Step 1: Install Dependencies
```bash
pip install qrcode pillow requests
```
### Step 2: Create the Bridge Script (`bridge.py`)
```python
import sys
import socket
import http.server
import socketserver
import threading
import json
import subprocess
import time
import urllib.request
import qrcode
LLM_PORTS = {
11434: "Ollama",
1234: "LM Studio",
8000: "vLLM / Local API",
5001: "Text Generation WebUI"
}
BRIDGE_PORT = 4000
detected_api_url = None
detected_engine = None
# --- ATOM 1: Local LLM Service Auto-Discovery ---
def discover_llm():
global detected_api_url, detected_engine
print("[*] Probing local ports for LLM services...")
for port, engine in LLM_PORTS.items():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
result = s.connect_ex(('127.0.0.1', port))
if result == 0:
detected_engine = engine
detected_api_url = f"http://127.0.0.1:{port}"
print(f"[+] Found {engine} running on port {port}!")
return
print("[-] No active LLM server detected. Defaulting to Ollama (127.0.0.1:11434).")
detected_engine = "Ollama (Fallback)"
detected_api_url = "http://127.0.0.1:11434"
# --- GLUE: HTML Chat UI ---
HTML_UI = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Private Home AI</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #121212; color: #e0e0e0; margin: 0; padding: 0; display: flex; flex-direction: column; height: 100vh; }
header { background: #1e1e1e; padding: 15px; text-align: center; font-weight: bold; border-bottom: 1px solid #333; }
#chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
.msg { max-width: 80%; padding: 12px 16px; border-radius: 18px; line-height: 1.4; }
.user { background: #007aff; color: white; align-self: flex-end; border-bottom-right-radius: 2px; }
.ai { background: #2a2a2c; align-self: flex-start; border-bottom-left-radius: 2px; }
form { display: flex; padding: 15px; background: #1e1e1e; border-top: 1px solid #333; }
input { flex: 1; background: #2a2a2c; border: none; padding: 12px; border-radius: 20px; color: white; font-size: 16px; outline: none; }
button { background: #007aff; border: none; color: white; padding: 10px 20px; border-radius: 20px; margin-left: 10px; cursor: pointer; font-size: 16px; }
</style>
</head>
<body>
<header>Home Private AI (Engine: {engine})</header>
<div id="chat">
<div class="msg ai">Hello! I am your private home AI. Ask me anything.</div>
</div>
<form id="chat-form">
<input type="text" id="msg-input" placeholder="Type your message here..." autocomplete="off" required>
<button type="submit">Send</button>
</form>
<script>
const form = document.getElementById('chat-form');
const input = document.getElementById('msg-input');
const chat = document.getElementById('chat');
form.onsubmit = async (e) => {
e.preventDefault();
const text = input.value.trim();
if(!text) return;
addMessage(text, 'user');
input.value = '';
const aiMsg = addMessage("Thinking...", 'ai');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
const data = await response.json();
aiMsg.innerText = data.reply;
} catch (err) {
aiMsg.innerText = "Error: Could not reach home AI.";
}
chat.scrollTop = chat.scrollHeight;
};
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `msg ${sender}`;
div.innerText = text;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
return div;
}
</script>
</body>
</html>
"""
# --- GLUE: Web UI & Proxy Server ---
class BridgeHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(HTML_UI.replace("{engine}", detected_engine).encode('utf-8'))
else:
self.send_error(404)
def do_POST(self):
if self.path == "/api/chat":
content_length = int(self.headers['Content-Length'])
post_data = json.loads(self.rfile.read(content_length))
user_message = post_data.get("message")
reply = self.relay_to_llm(user_message)
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"reply": reply}).encode('utf-8'))
def relay_to_llm(self, text):
is_ollama_direct = "11434" in detected_api_url
if is_ollama_direct:
url = f"{detected_api_url}/api/generate"
payload = json.dumps({"model": "llama3", "prompt": text, "stream": False})
else:
url = f"{detected_api_url}/v1/chat/completions"
payload = json.dumps({
"messages": [{"role": "user", "content": text}],
"stream": False
})
try:
req = urllib.request.Request(url, data=payload.encode('utf-8'), headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=15) as response:
res_data = json.loads(response.read().decode('utf-8'))
if is_ollama_direct:
return res_data.get("response", "Empty response.")
else:
return res_data['choices'][0]['message']['content']
except Exception as e:
return f"Error proxying request to LLM ({e}). Make sure your model is downloaded and running."
def start_web_server():
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", BRIDGE_PORT), BridgeHTTPRequestHandler) as httpd:
print(f"[*] Local UI Proxy serving at http://localhost:{BRIDGE_PORT}")
httpd.serve_forever()
# --- ATOM 2: QR-paired reverse tunneling [write] ---
def setup_tunnel_and_qr():
print("[*] Launching secure reverse tunnel via localhost.run...")
cmd = ["ssh", "-R", f"80:localhost:{BRIDGE_PORT}", "nob@localhost.run"]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
tunnel_url = None
for line in iter(process.stdout.readline, ''):
if "lhr.life" in line or "localhost.run" in line:
parts = line.split()
for part in parts:
if "http" in part:
tunnel_url = part.strip()
break
if tunnel_url:
break
if not tunnel_url:
print("[!] Failed to establish tunnel. Make sure SSH is configured and you have internet access.")
sys.exit(1)
print(f"\n[+] Tunnel Securely Created: {tunnel_url}")
print("[*] Scan this QR code with your phone to open your Home Private AI:")
qr = qrcode.QRCode()
qr.add_data(tunnel_url)
qr.make()
qr.print_ascii(invert=True)
print(f"\nLink: {tunnel_url}")
print("Press Ctrl+C to stop the bridge and disconnect.")
if __name__ == "__main__":
try:
discover_llm()
web_thread = threading.Thread(target=start_web_server, daemon=True)
web_thread.start()
time.sleep(1)
setup_tunnel_and_qr()
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[-] Shutting down Private AI Bridge. Goodbye!")
sys.exit(0)
```
---
## Acceptance Check
1. **Start your local LLM engine** (e.g., `ollama run llama3`) and confirm local queries work.
2. **Launch the Bridge**: `python bridge.py`
3. **Verify Terminal Logs**: `[+] Found Ollama running on port 11434!` (or your engine), tunnel creation message, and an ASCII QR code rendering in the terminal.
4. **Scan and Connect**: Point your phone camera at the QR code, tap the link (e.g., `https://xxxx.lhr.life`). The phone browser should load the Home Private AI UI showing the detected engine.
5. **Chat**: Send a message from the phone; verify the prompt resolves on the home machine and the response returns to the phone.Generated with Gerolamo — competitive technical intelligence
gerolamo.org