Skip to main content
Meikipop is built around a multi-threaded architecture that enables real-time OCR and dictionary lookups without blocking the user interface. This page explains the core architectural components and how they work together.

Threading model

Meikipop uses a multi-threaded design where each major component runs in its own daemon thread. All threads share a SharedState object that coordinates communication through thread-safe queues and events.

SharedState class

The SharedState class is the central coordination point for all threads:
class SharedState:
    def __init__(self):
        self.running = True

        # events and queues
        self.screenshot_trigger_event = threading.Event()
        self.ocr_queue = LatestValueQueue()
        self.hit_scan_queue = LatestValueQueue()
        self.lookup_queue = LatestValueQueue()

        # screen lock - used by screen manager and popup
        self.screen_lock = threading.RLock()
Key elements:
  • running: Global flag to signal all threads to terminate gracefully
  • screenshot_trigger_event: Signals when a new screenshot should be captured
  • Queues: Custom LatestValueQueue instances that only keep the most recent value
  • screen_lock: Prevents the popup from being included in screenshots

LatestValueQueue

A specialized queue that only stores the most recent value:
class LatestValueQueue():
    def __init__(self):
        self._value = None
        self._lock = threading.Lock()
        self._event = threading.Event()

    def put(self, item):
        with self._lock:
            self._value = item
            self._event.set()

    def get(self):
        self._event.wait()
        with self._lock:
            value = self._value
            self._event.clear()
            return value
This ensures that if processing is slow, only the latest data is processed, preventing queue buildup.

Core components

Meikipop consists of six main threaded components that communicate through the shared state:
1
InputLoop thread
2
Monitors keyboard and mouse input to trigger OCR operations.
3
Responsibilities:
4
  • Polls hotkey state (platform-specific implementations for Windows, Linux, macOS)
  • Tracks mouse position changes
  • Triggers screenshots via screenshot_trigger_event
  • Triggers hit scans via hit_scan_queue
  • 5
    Key code (from src/gui/input.py:153):
    6
    while self.shared_state.running:
        if not config.is_enabled:
            time.sleep(0.1)
            continue
            
        current_mouse_pos = self.mouse_controller.position
        hotkey_is_pressed = self.keyboard_controller.is_hotkey_pressed()
    
        # trigger screenshots + ocr in manual mode
        if hotkey_is_pressed and not hotkey_was_pressed and not config.auto_scan_mode:
            self.shared_state.screenshot_trigger_event.set()
    
        # trigger hit_scans + lookups
        if current_mouse_pos != last_mouse_pos:
            self.shared_state.hit_scan_queue.put((False, None))
    
    7
    ScreenManager thread
    8
    Captures screenshots of the configured screen region.
    9
    Responsibilities:
    10
  • Waits for screenshot_trigger_event
  • Captures screenshots using mss library
  • Implements throttling for auto-scan mode
  • Converts screenshots to PIL images
  • Puts images into ocr_queue
  • 11
    Key code (from src/screenshot/screenmanager.py:34):
    12
    while self.shared_state.running:
        self.shared_state.screenshot_trigger_event.wait()
        self.shared_state.screenshot_trigger_event.clear()
        
        with self.shared_state.screen_lock:
            screenshot = self.take_screenshot()
        
        img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
        self.shared_state.ocr_queue.put(img)
    
    13
    OcrProcessor thread
    14
    Processes screenshots and extracts Japanese text using pluggable OCR providers.
    15
    Responsibilities:
    16
  • Dynamically discovers OCR providers from src/ocr/providers/
  • Loads configured provider (Google Lens V2 by default)
  • Processes images from ocr_queue
  • Puts OCR results (list of paragraphs) into hit_scan_queue
  • 17
    Key code (from src/ocr/ocr.py:31):
    18
    while self.shared_state.running:
        screenshot = self.shared_state.ocr_queue.get()
        
        start_time = time.perf_counter()
        ocr_result = self.ocr_backend.scan(screenshot)
        logger.info(
            f"{self.ocr_backend.NAME} found {len(ocr_result)} paragraphs in {(time.perf_counter() - start_time):.3f}s."
        )
        
        self.shared_state.hit_scan_queue.put((True, ocr_result))
    
    19
    OCR providers are discovered dynamically by scanning src/ocr/providers/ for subdirectories containing classes that inherit from OcrProvider.
    20
    HitScanner thread
    21
    Determines which Japanese text the user’s mouse is hovering over.
    22
    Responsibilities:
    23
  • Receives OCR results from hit_scan_queue
  • Calculates normalized mouse position relative to scan region
  • Performs hit detection on OCR bounding boxes
  • Determines character offset within the hovered word
  • Puts lookup strings into lookup_queue
  • 24
    Key code (from src/ocr/hit_scan.py:20):
    25
    while self.shared_state.running:
        is_ocr_result_updated, new_ocr_result = self.shared_state.hit_scan_queue.get()
        
        if is_ocr_result_updated:
            self.last_ocr_result = new_ocr_result
    
        hit_scan_result = self.hit_scan(self.last_ocr_result) if self.last_ocr_result else None
        self.shared_state.lookup_queue.put(hit_scan_result)
    
    26
    Lookup thread
    27
    Performs dictionary lookups with deconjugation.
    28
    Responsibilities:
    29
  • Loads JMdict dictionary and deconjugation rules
  • Receives lookup strings from lookup_queue
  • Performs incremental lookups with deconjugation
  • Implements priority-based sorting and filtering
  • Caches results (500 most recent lookups)
  • Sends formatted entries to popup window
  • 30
    Key code (from src/dictionary/lookup.py:58):
    31
    while self.shared_state.running:
        hit_result = self.shared_state.lookup_queue.get()
        
        # skip lookup if hit_result didnt change
        if hit_result == self.last_hit_result:
            continue
        self.last_hit_result = hit_result
    
        lookup_result = self.lookup(self.last_hit_result) if self.last_hit_result else None
        self.popup_window.set_latest_data(lookup_result)
    
    33
    Displays dictionary entries near the mouse cursor.
    34
    Responsibilities:
    35
  • Runs in the main Qt thread (not a daemon thread)
  • Polls for new lookup data via QTimer (every 10ms)
  • Renders HTML-formatted dictionary entries
  • Positions itself intelligently near the cursor
  • Acquires screen_lock when visible to prevent self-capture
  • 36
    Key code (from src/gui/popup.py:160):
    37
    def process_latest_data_loop(self):
        latest_data = self.get_latest_data()
        if latest_data and latest_data != self._last_latest_data:
            full_html, new_size = self._calculate_content_and_size_char_count(latest_data)
            self.display_label.setText(full_html)
            self.setFixedSize(new_size)
    
        if self._latest_data and self.input_loop.is_virtual_hotkey_down():
            self.show_popup()
        else:
            self.hide_popup()
    

    TrayIcon (non-threaded)

    The system tray icon provides user interaction: Features:
    • Settings dialog access
    • OCR provider selection
    • Scan mode toggle (manual/auto)
    • Scan area selection (region/screens)
    • Pause/resume functionality
    • Quit application
    Implementation (from src/gui/tray.py:28):
    class TrayIcon(QSystemTrayIcon):
        def __init__(self, screen_manager, ocr_processor, popup_window, input_loop, lookup):
            # Initialize with icon and create context menu
            # Provides access to all major components for configuration
    

    Application lifecycle

    The main application flow (from src/main.py:44):
    1
    Initialize Qt application
    2
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    
    3
    Create SharedState and components
    4
    shared_state = SharedState()
    input_loop = InputLoop(shared_state)
    popup_window = Popup(shared_state, input_loop)
    screen_manager = ScreenManager(shared_state, input_loop)
    lookup = Lookup(shared_state, popup_window)
    ocr_processor = OcrProcessor(shared_state, screen_manager)
    hit_scanner = HitScanner(shared_state, input_loop, screen_manager)
    tray_icon = TrayIcon(screen_manager, ocr_processor, popup_window, input_loop, lookup)
    
    5
    Start all threads
    6
    for t in [lookup, hit_scanner, ocr_processor, screen_manager, input_loop]:
        t.start()
    
    7
    Run Qt event loop
    8
    exit_code = app.exec()
    
    9
    Clean shutdown
    10
    shared_state.running = False
    shared_state.screenshot_trigger_event.set()
    shared_state.ocr_queue.put(None)
    shared_state.hit_scan_queue.put((False, None))
    shared_state.lookup_queue.put(None)
    

    Data flow diagram

    Here’s how data flows through the system:
    InputLoop → screenshot_trigger_event → ScreenManager
        ↓                                        ↓
        ↓                                    ocr_queue
        ↓                                        ↓
        ↓                                  OcrProcessor
        ↓                                        ↓
        ↓                                 hit_scan_queue
        ↓                                        ↓
        └──────→ hit_scan_queue ←──────── HitScanner
    
                  lookup_queue
    
                    Lookup
    
                (direct method call)
    
                     Popup
    
    All threads are daemon threads, meaning they will automatically terminate when the main thread exits.

    Thread safety

    Meikipop uses several strategies to ensure thread safety:
    • LatestValueQueue: Thread-safe queue implementation with internal locking
    • screen_lock: RLock prevents popup from being captured in screenshots
    • _data_lock: Protects popup’s latest data from concurrent access
    • Daemon threads: All worker threads are marked as daemon to ensure clean shutdown
    • Atomic operations: Config changes are atomic and use file-based persistence
    When modifying the architecture, be careful about race conditions between the popup visibility state and screenshot capture. The screen_lock mechanism is critical for preventing the popup from appearing in OCR results.

    Build docs developers (and LLMs) love