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
|
||||
},
|
||||
"olhos-de-orpheu": {
|
||||
"serverUrl": "https://mcp.portugalfuturista.org/sse",
|
||||
"serverUrl": "http://192.168.0.16:8001/sse",
|
||||
"_disabled": false
|
||||
},
|
||||
"memory": {
|
||||
|
|
|
|||
|
|
@ -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 } };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
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