Initial commit
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.
This commit is contained in:
108
src/logic/__init__.py
Normal file
108
src/logic/__init__.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user