Files
nanocode/web-opencv/server.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

195 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""
OpenCV WASM Test Server
Serves static files + API endpoints:
GET /api/version - Run opencv_version.js, return version string
POST /api/awb - Run Gray World AWB via awb_static.wasm
"""
import http.server
import json
import os
import subprocess
import sys
import urllib.parse
import base64
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8910
DIR = os.path.dirname(os.path.abspath(__file__))
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == '/api/version':
self.handle_version()
else:
super().do_GET()
def do_POST(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == '/api/awb':
self.handle_awb()
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'{"success": false, "output": "Not found"}')
def handle_version(self):
"""Run opencv_version.wasm via Node.js (using locateFile wrapper)"""
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
js_path = os.path.join(DIR, 'opencv_version.js')
lib_dir = os.path.join(DIR, 'lib')
# Node.js wrapper with locateFile config
wrapper_script = f"""
const mod = require({json.dumps(js_path)});
const path = require('path');
const fs = require('fs');
mod.locateFile = function(fp, prefix) {{
const lib = {json.dumps(lib_dir)};
if (fp.includes('.so')) {{
const exact = path.join(lib, fp);
if (fs.existsSync(exact)) return exact;
const base = fp.replace(/\\.\\d+\\.\\d+\\.\\d+$/, '').replace(/\\.\\d+$/, '');
const noVer = path.join(lib, base);
if (fs.existsSync(noVer)) return noVer;
}}
return path.join(prefix, fp);
}};
"""
try:
result = subprocess.run(
['node', '-e', wrapper_script],
capture_output=True,
text=True,
timeout=30,
)
output = result.stdout.strip() or result.stderr.strip()
response = {
'success': result.returncode == 0,
'output': output,
'returncode': result.returncode
}
except subprocess.TimeoutExpired:
response = {'success': False, 'output': 'Timeout', 'returncode': -1}
except Exception as e:
response = {'success': False, 'output': str(e), 'returncode': -1}
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
def handle_awb(self):
"""Run Gray World AWB via awb_static.wasm
POST body: JSON with {"image": "<base64_of_input_png>"}
Response: JSON with {"success": true, "image": "<base64_of_output_png>",
"metadata": "<stderr_output>"}
"""
content_length = int(self.headers.get('Content-Length', 0))
if content_length == 0:
self.send_error_response('Empty request body')
return
body = self.rfile.read(content_length)
try:
data = json.loads(body)
except json.JSONDecodeError:
self.send_error_response('Invalid JSON')
return
input_b64 = data.get('image', '')
if not input_b64:
self.send_error_response('Missing image field')
return
# Validate base64 image
try:
raw = base64.b64decode(input_b64)
if raw[:4] != b'\x89PNG':
self.send_error_response('Only PNG images are supported')
return
except Exception:
self.send_error_response('Invalid base64 image')
return
js_path = os.path.join(DIR, 'awb_static.js')
# Node.js wrapper - runs AWB with locateFile (not needed for static build)
wrapper_script = f"""
const mod = require({json.dumps(js_path)});
mod.noInitialRun = true;
mod.preRun = function() {{
const buf = Buffer.from(process.argv[1], 'base64');
mod.FS.mkdirTree('/data');
mod.FS.writeFile('/data/input.png', new Uint8Array(buf));
}};
mod.onRuntimeInitialized = function() {{
try {{
mod.callMain([]);
const outputBytes = mod.FS.readFile('/data/output.png');
process.stdout.write(Buffer.from(outputBytes).toString('base64'));
}} catch (e) {{
process.stderr.write('ERR:' + (e.message || e));
process.exit(1);
}}
}};
"""
try:
result = subprocess.run(
['node', '-e', wrapper_script, input_b64],
capture_output=True,
text=False, # binary mode - preserve base64 encoding
timeout=60,
)
stdout_b64 = result.stdout.decode('utf-8', errors='replace').strip()
stderr_txt = result.stderr.decode('utf-8', errors='replace').strip()
response = {
'success': result.returncode == 0 and len(stdout_b64) > 50,
'image': stdout_b64,
'metadata': stderr_txt,
'returncode': result.returncode,
}
except subprocess.TimeoutExpired:
response = {'success': False, 'error': 'Timeout processing image'}
except Exception as e:
response = {'success': False, 'error': str(e)}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
def send_error_response(self, msg):
self.send_response(400)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(
{'success': False, 'error': msg}, ensure_ascii=False
).encode('utf-8'))
def log_message(self, fmt, *args):
sys.stderr.write(f'[server] {args[0]} {args[1]} {args[2]}\n')
if __name__ == '__main__':
http.server.SimpleHTTPRequestHandler.extensions_map.update({
'.wasm': 'application/wasm',
'.so': 'application/octet-stream',
})
server = http.server.HTTPServer(('0.0.0.0', PORT), Handler)
print(f'🚀 OpenCV WASM Test Server running at http://0.0.0.0:{PORT}')
print(f' 📁 Static files: {DIR}')
print(f' 📡 API: /api/version | POST /api/awb')
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
print('\n👋 Server stopped')