feat(aurelio): add Omni-Printing UI, OCR notebook script, and hardware bridge server

This commit is contained in:
AI Agent 2026-05-12 12:30:43 +01:00
parent 108334c85a
commit 9c2b9ddd9d
6 changed files with 195 additions and 11 deletions

View file

@ -76,7 +76,7 @@
"_disabled": false
},
"olhos-de-orpheu": {
"serverUrl": "https://mcp.portugalfuturista.org/sse",
"serverUrl": "http://192.168.0.16:8001/sse",
"_disabled": false
},
"memory": {

View file

@ -103,6 +103,23 @@ export function createDataStore() {
};
break;
case 'mcp:result':
try {
if (msg.tool === 'list_printers') {
const parsed = JSON.parse(msg.result.content[0].text);
data = { ...data, printers: { ...(data.printers || {}), list: parsed } };
} else if (msg.tool === 'list_print_jobs') {
const parsed = JSON.parse(msg.result.content[0].text);
data = { ...data, printers: { ...(data.printers || {}), jobs: parsed } };
}
} catch (e) {
console.error("Failed to parse mcp:result for", msg.tool, e);
}
break;
case 'mcp:error':
console.error("MCP Tool Error:", msg.tool, msg.error);
break;
// ─── Antigravity (Proxmox CDP Bridge) ─────────────────────
case 'antigravityStatus':
data = { ...data, antigravity: { ...(data.antigravity || {}), status: msg.status } };

View file

@ -15,12 +15,7 @@
setTimeout(() => { printerLoading = false; }, 2000);
}
const printerFleet = [
{ name: 'HP Photosmart C4280', type: 'Photo Printer', icon: 'printer', status: 'Online' },
{ name: 'Brother QL-700', type: 'Label Printer', icon: 'layers', status: 'Online' },
{ name: 'POS-8030A', type: 'Thermal Receipt', icon: 'scroll', status: 'Online' },
{ name: 'Ricoh MP C2003', type: 'Office Copier', icon: 'server', status: 'Online' }
];
</script>
<div class="flex flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
@ -39,7 +34,7 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{#each printerFleet as printer}
{#each printerList as printer}
<div class="glass-card p-5 rounded-2xl border-white/5 hover:border-brand-primary/30 transition-all group">
<div class="flex items-start justify-between mb-4">
<div class="p-3 rounded-xl bg-white/5 text-white/60 group-hover:bg-brand-primary/20 group-hover:text-brand-primary transition-all">
@ -103,15 +98,26 @@
Configurações Rápidas
</h3>
<div class="glass-card p-6 rounded-2xl border-white/5 space-y-4">
<button class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group">
<button onclick={() => {
postMessage('mcp:call', { server: 'Olhos de Orpheu', tool: 'clear_print_queue', args: {} });
setTimeout(refreshPrinters, 1000);
}}
class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group">
Limpar Fila Completa
<DevIcon name="zap" size={14} class="group-hover:text-brand-danger transition-colors" />
</button>
<button class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group">
<button onclick={() => {
if (printerList.length > 0) {
postMessage('mcp:call', { server: 'Olhos de Orpheu', tool: 'print_test_page', args: { printer_id: printerList[0].id } });
}
}}
class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group"
disabled={printerList.length === 0}>
Página de Teste
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
</button>
<button class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group">
<button onclick={() => window.open('http://192.168.0.38:631', '_blank')}
class="w-full py-3 rounded-xl bg-white/5 border border-white/5 text-xs font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all text-left px-4 flex items-center justify-between group">
Abrir Painel CUPS
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
</button>

View file

@ -170,6 +170,16 @@ export class ControlCenterPanel {
await ServiceRegistry.getInstance().get(McpServerManager).toggleTool(message.serverName, message.toolName, message.enabled);
this._updateMcpStatus();
return;
case 'mcp:call':
try {
const manager = ServiceRegistry.getInstance().get(McpServerManager);
const result = await manager.callTool(message.server, message.tool, message.args);
this._panel.webview.postMessage({ command: 'mcp:result', tool: message.tool, result });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'mcp:error', tool: message.tool, error: e.message });
vscode.window.showErrorMessage(`MCP call failed for ${message.tool}: ${e.message}`);
}
return;
case 'openWorkflow':
try {
const doc = await vscode.workspace.openTextDocument(message.filePath);

82
hardware_bridge_server.py Normal file
View file

@ -0,0 +1,82 @@
import http.server
import socketserver
import json
import subprocess
import logging
PORT = 8010
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class HardwareBridgeHandler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
if self.path == '/api/printers/clean':
self.run_command(['hp-clean', '-i'])
elif self.path == '/api/printers/align':
self.run_command(['hp-align', '-i'])
elif self.path == '/api/printers/firmware':
self.run_command(['hp-firmware', '-n'])
elif self.path == '/api/printers/reset':
# Cancel all jobs
subprocess.run(['cancel', '-a'], check=False)
# Restart CUPS
subprocess.run(['systemctl', 'restart', 'cups'], check=False)
# Enable all printers (parsing lpstat -a)
try:
result = subprocess.run(['lpstat', '-a'], capture_output=True, text=True)
for line in result.stdout.splitlines():
printer = line.split()[0]
subprocess.run(['cupsenable', printer], check=False)
subprocess.run(['cupsaccept', printer], check=False)
except Exception as e:
logging.error(f"Failed to enable printers: {e}")
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "success", "message": "Hard reset completed"}).encode())
else:
self.send_response(404)
self.end_headers()
def run_command(self, cmd):
try:
logging.info(f"Executing: {cmd}")
# we use -i usually, but we need to run non-interactively if possible.
# actually hp-clean might need specific printer -p
content_length = int(self.headers.get('Content-Length', 0))
post_data = b""
if content_length > 0:
post_data = self.rfile.read(content_length)
printer_id = None
if post_data:
try:
data = json.loads(post_data.decode('utf-8'))
printer_id = data.get('printer_id')
except:
pass
if printer_id:
cmd.extend(['-p', printer_id])
result = subprocess.run(cmd, capture_output=True, text=True)
response = {
"status": "success" if result.returncode == 0 else "error",
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode
}
self.send_response(200 if result.returncode == 0 else 500)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response).encode())
except Exception as e:
logging.error(f"Error executing {cmd}: {e}")
self.send_response(500)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode())
if __name__ == "__main__":
with socketserver.TCPServer(("", PORT), HardwareBridgeHandler) as httpd:
logging.info(f"Hardware Bridge Server listening on port {PORT}")
httpd.serve_forever()

69
scripts/ocr_notebook.py Normal file
View file

@ -0,0 +1,69 @@
import os
import json
import base64
from google import genai
from google.genai import types
def ocr_scan_to_obsidian(image_path: str, obsidian_vault_path: str):
"""Uses Gemini to OCR a scanned notebook page and save it to Obsidian."""
print(f"Starting OCR on {image_path}...")
# Initialize the client
client = genai.Client()
# Read the image
with open(image_path, "rb") as image_file:
image_bytes = image_file.read()
print("Uploading image to Gemini...")
prompt = """
You are an expert transcription assistant.
Carefully read the handwritten or printed notes in this scanned image.
Convert the contents to high-quality Markdown.
Preserve headings, bullet points, and any structural elements.
If there are diagrams, describe them briefly.
Output ONLY the markdown text.
"""
response = client.models.generate_content(
model='gemini-2.5-pro',
contents=[
prompt,
types.Part.from_bytes(
data=image_bytes,
mime_type='image/png',
),
]
)
markdown_content = response.text.strip()
# Create the obsidian note
note_title = "Scanned Notebook Page.md"
note_path = os.path.join(obsidian_vault_path, note_title)
print(f"Saving to {note_path}...")
# Add metadata frontmatter
final_content = f"""---
source: scan
lineage_image: {image_path}
tags: [scanned, notebook]
---
{markdown_content}
"""
# Ensure vault exists
os.makedirs(obsidian_vault_path, exist_ok=True)
with open(note_path, "w") as f:
f.write(final_content)
print("Successfully ingested scanned notebook to Obsidian.")
if __name__ == "__main__":
scan_image = "/tmp/scan.png"
vault = "/home/fcunha/desasossego"
ocr_scan_to_obsidian(scan_image, vault)