Photo-based book cataloger with AI identification. Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend; vanilla JS SPA; OpenAI-compatible plugin system for boundary detection, text recognition, and archive search.
109 lines
4.1 KiB
Python
109 lines
4.1 KiB
Python
"""Logic package: plugin dispatch orchestration and public re-exports."""
|
|
|
|
import asyncio
|
|
import dataclasses
|
|
from typing import Any
|
|
|
|
import plugins as plugin_registry
|
|
from errors import InvalidPluginEntityError, PluginNotFoundError, PluginTargetMismatchError
|
|
from models import PluginLookupResult
|
|
from logic.archive import run_archive_searcher, run_archive_searcher_bg
|
|
from logic.batch import archive_executor, batch_executor, batch_state, process_book_sync, run_batch
|
|
from logic.boundaries import book_spine_source, bounds_for_index, run_boundary_detector, shelf_source
|
|
from logic.identification import (
|
|
AI_FIELDS,
|
|
apply_ai_result,
|
|
build_query,
|
|
compute_status,
|
|
dismiss_field,
|
|
run_book_identifier,
|
|
run_text_recognizer,
|
|
save_user_fields,
|
|
)
|
|
from logic.images import prep_img_b64, crop_save, serve_crop
|
|
|
|
__all__ = [
|
|
"AI_FIELDS",
|
|
"apply_ai_result",
|
|
"archive_executor",
|
|
"batch_executor",
|
|
"batch_state",
|
|
"book_spine_source",
|
|
"bounds_for_index",
|
|
"build_query",
|
|
"compute_status",
|
|
"crop_save",
|
|
"dismiss_field",
|
|
"dispatch_plugin",
|
|
"process_book_sync",
|
|
"run_archive_searcher",
|
|
"run_archive_searcher_bg",
|
|
"run_batch",
|
|
"run_book_identifier",
|
|
"run_boundary_detector",
|
|
"run_text_recognizer",
|
|
"save_user_fields",
|
|
"serve_crop",
|
|
"shelf_source",
|
|
"prep_img_b64",
|
|
]
|
|
|
|
|
|
async def dispatch_plugin(
|
|
plugin_id: str,
|
|
lookup: PluginLookupResult,
|
|
entity_type: str,
|
|
entity_id: str,
|
|
loop: asyncio.AbstractEventLoop,
|
|
) -> dict[str, Any]:
|
|
"""Validate plugin/entity compatibility, run the plugin, and trigger auto-queue follow-ups.
|
|
|
|
Args:
|
|
plugin_id: The plugin ID string (used in error reporting).
|
|
lookup: Discriminated tuple from plugins.get_plugin(); (None, None) if not found.
|
|
entity_type: Entity type string (e.g. 'cabinets', 'shelves', 'books').
|
|
entity_id: ID of the entity to operate on.
|
|
loop: Running event loop for executor dispatch.
|
|
|
|
Returns:
|
|
dataclasses.asdict() of the updated entity row.
|
|
|
|
Raises:
|
|
PluginNotFoundError: If lookup is (None, None).
|
|
InvalidPluginEntityError: If the entity_type is not compatible with the plugin category.
|
|
PluginTargetMismatchError: If a boundary_detector plugin's target mismatches the entity.
|
|
"""
|
|
match lookup:
|
|
case (None, None):
|
|
raise PluginNotFoundError(plugin_id)
|
|
|
|
case ("boundary_detector", plugin):
|
|
if entity_type not in ("cabinets", "shelves"):
|
|
raise InvalidPluginEntityError("boundary_detector", entity_type)
|
|
if entity_type == "cabinets" and plugin.target != "shelves":
|
|
raise PluginTargetMismatchError(plugin.plugin_id, "shelves", plugin.target)
|
|
if entity_type == "shelves" and plugin.target != "books":
|
|
raise PluginTargetMismatchError(plugin.plugin_id, "books", plugin.target)
|
|
result = await loop.run_in_executor(None, run_boundary_detector, plugin, entity_type, entity_id)
|
|
return dataclasses.asdict(result)
|
|
|
|
case ("text_recognizer", plugin):
|
|
if entity_type != "books":
|
|
raise InvalidPluginEntityError("text_recognizer", entity_type)
|
|
result = await loop.run_in_executor(None, run_text_recognizer, plugin, entity_id)
|
|
for ap in plugin_registry.get_auto_queue("archive_searchers"):
|
|
loop.run_in_executor(archive_executor, run_archive_searcher_bg, ap, entity_id)
|
|
return dataclasses.asdict(result)
|
|
|
|
case ("book_identifier", plugin):
|
|
if entity_type != "books":
|
|
raise InvalidPluginEntityError("book_identifier", entity_type)
|
|
result = await loop.run_in_executor(None, run_book_identifier, plugin, entity_id)
|
|
return dataclasses.asdict(result)
|
|
|
|
case ("archive_searcher", plugin):
|
|
if entity_type != "books":
|
|
raise InvalidPluginEntityError("archive_searcher", entity_type)
|
|
result = await loop.run_in_executor(archive_executor, run_archive_searcher, plugin, entity_id)
|
|
return dataclasses.asdict(result)
|