OCR-Viewer (#1)
Co-authored-by: nathan <nathan.kehler@gmail.com> Reviewed-on: #1
This commit is contained in:
152
launch_viewer.py
Normal file
152
launch_viewer.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
P44 OCR Viewer launcher.
|
||||
Starts a local HTTP server at port 5044 and opens the viewer in the default browser.
|
||||
Run from any directory; the server root is always the project folder (this file's location).
|
||||
If port 5044 is already in use (server already running), just opens the browser tab.
|
||||
"""
|
||||
import http.server
|
||||
import webbrowser
|
||||
import threading
|
||||
import socket
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
PORT = 5044
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(ROOT, 'viewer', 'data')
|
||||
|
||||
|
||||
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
"""Serve files from ROOT, suppress request logs, disable browser caching."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=ROOT, **kwargs)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
pass # silence per-request noise
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
super().end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
"""Handle POST /save-flags, /save-pass2-flags, /save-corrections, /clear-all-data.
|
||||
All endpoints accept an optional ?diary=ID query param to use per-diary files.
|
||||
"""
|
||||
parsed = urlparse(self.path)
|
||||
qs = parse_qs(parsed.query)
|
||||
diary = qs.get('diary', ['unknown'])[0]
|
||||
# Sanitize: only allow alphanumeric, hyphens, underscores
|
||||
diary = ''.join(c for c in diary if c.isalnum() or c in '-_')
|
||||
if not diary:
|
||||
diary = 'unknown'
|
||||
|
||||
length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400); self.end_headers()
|
||||
return
|
||||
|
||||
if parsed.path == '/save-flags':
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
dest = os.path.join(DATA_DIR, 'flags-' + diary + '.json')
|
||||
with open(dest, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
|
||||
elif parsed.path == '/save-corrections':
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
dest = os.path.join(DATA_DIR, 'corrections-' + diary + '.json')
|
||||
existing = {}
|
||||
if os.path.exists(dest):
|
||||
try:
|
||||
with open(dest, 'r', encoding='utf-8') as f:
|
||||
existing = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
page = str(data.get('page', ''))
|
||||
correction = data.get('correction')
|
||||
if correction:
|
||||
existing[page] = correction
|
||||
else:
|
||||
existing.pop(page, None)
|
||||
with open(dest, 'w', encoding='utf-8') as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
|
||||
elif parsed.path == '/save-pass2-flags':
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
dest = os.path.join(DATA_DIR, 'pass2_flags-' + diary + '.json')
|
||||
try:
|
||||
with open(dest, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except Exception:
|
||||
self.send_response(500); self.end_headers()
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
|
||||
elif parsed.path == '/clear-all-data':
|
||||
for prefix in ['flags-', 'corrections-', 'pass2_flags-']:
|
||||
fpath = os.path.join(DATA_DIR, prefix + diary + '.json')
|
||||
if os.path.exists(fpath):
|
||||
try:
|
||||
os.remove(fpath)
|
||||
except Exception:
|
||||
pass
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
|
||||
else:
|
||||
self.send_response(404); self.end_headers()
|
||||
|
||||
|
||||
def port_in_use(port):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(('localhost', port)) == 0
|
||||
|
||||
|
||||
def start_server():
|
||||
server = http.server.HTTPServer(('localhost', PORT), QuietHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
url = 'http://localhost:{}/viewer/p44-landing.html'.format(PORT)
|
||||
|
||||
if port_in_use(PORT):
|
||||
print('\n P44 OCR Viewer (server already running)')
|
||||
print(' URL: {}\n'.format(url))
|
||||
webbrowser.open(url)
|
||||
else:
|
||||
t = threading.Thread(target=start_server, daemon=True)
|
||||
t.start()
|
||||
print('\n P44 OCR Viewer')
|
||||
print(' Serving : {}'.format(ROOT))
|
||||
print(' URL : {}\n'.format(url))
|
||||
webbrowser.open(url)
|
||||
|
||||
print('Press Enter (or Ctrl-C) to stop the server.\n')
|
||||
try:
|
||||
input()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print('Server stopped.')
|
||||
sys.exit(0)
|
||||
|
||||
Reference in New Issue
Block a user