feat(aurelio): add Omni-Printing UI, OCR notebook script, and hardware bridge server
This commit is contained in:
parent
108334c85a
commit
9c2b9ddd9d
6 changed files with 195 additions and 11 deletions
|
|
@ -76,7 +76,7 @@
|
||||||
"_disabled": false
|
"_disabled": false
|
||||||
},
|
},
|
||||||
"olhos-de-orpheu": {
|
"olhos-de-orpheu": {
|
||||||
"serverUrl": "https://mcp.portugalfuturista.org/sse",
|
"serverUrl": "http://192.168.0.16:8001/sse",
|
||||||
"_disabled": false
|
"_disabled": false
|
||||||
},
|
},
|
||||||
"memory": {
|
"memory": {
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,23 @@ export function createDataStore() {
|
||||||
};
|
};
|
||||||
break;
|
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) ─────────────────────
|
// ─── Antigravity (Proxmox CDP Bridge) ─────────────────────
|
||||||
case 'antigravityStatus':
|
case 'antigravityStatus':
|
||||||
data = { ...data, antigravity: { ...(data.antigravity || {}), status: msg.status } };
|
data = { ...data, antigravity: { ...(data.antigravity || {}), status: msg.status } };
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,7 @@
|
||||||
setTimeout(() => { printerLoading = false; }, 2000);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
<div class="flex flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
|
|
@ -39,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<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="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="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">
|
<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
|
Configurações Rápidas
|
||||||
</h3>
|
</h3>
|
||||||
<div class="glass-card p-6 rounded-2xl border-white/5 space-y-4">
|
<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
|
Limpar Fila Completa
|
||||||
<DevIcon name="zap" size={14} class="group-hover:text-brand-danger transition-colors" />
|
<DevIcon name="zap" size={14} class="group-hover:text-brand-danger transition-colors" />
|
||||||
</button>
|
</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
|
Página de Teste
|
||||||
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
|
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
|
||||||
</button>
|
</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
|
Abrir Painel CUPS
|
||||||
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
|
<DevIcon name="monitor" size={14} class="group-hover:text-brand-primary transition-colors" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,16 @@ export class ControlCenterPanel {
|
||||||
await ServiceRegistry.getInstance().get(McpServerManager).toggleTool(message.serverName, message.toolName, message.enabled);
|
await ServiceRegistry.getInstance().get(McpServerManager).toggleTool(message.serverName, message.toolName, message.enabled);
|
||||||
this._updateMcpStatus();
|
this._updateMcpStatus();
|
||||||
return;
|
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':
|
case 'openWorkflow':
|
||||||
try {
|
try {
|
||||||
const doc = await vscode.workspace.openTextDocument(message.filePath);
|
const doc = await vscode.workspace.openTextDocument(message.filePath);
|
||||||
|
|
|
||||||
82
hardware_bridge_server.py
Normal file
82
hardware_bridge_server.py
Normal 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
69
scripts/ocr_notebook.py
Normal 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)
|
||||||
Loading…
Reference in a new issue