- Introduced dataclasses for phrase candidates, judgments, and matches in `models.py`. - Implemented database operations for candidate and protected phrases in `store.py`, including loading, saving, and deleting phrases. - Enhanced text normalization functions in `text_normalization.py` with detailed docstrings. - Refactored search functionality to utilize new models and methods for detecting protected phrases.
170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
"""Admin routes for the EPUB search web UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from python.ebook_search.api.bm25_tasks import schedule_bm25_refresh
|
|
from python.ebook_search.api.dependencies import (
|
|
AppConfig, # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
|
)
|
|
from python.ebook_search.api.web import templates
|
|
from python.ebook_search.embeddings import embed_missing_chunks, embedding_model_stats
|
|
from python.ebook_search.ingest import ingest_configured_paths
|
|
from python.ebook_search.protected_phrases.generate_ngrams import generate_candidate_phrases_for_books
|
|
from python.ebook_search.protected_phrases.judge_ngrams import judge_candidate_phrases_for_books
|
|
from python.fastapi_tools import DbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/admin")
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
def admin(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Render the admin page."""
|
|
stats = embedding_model_stats(session)
|
|
logger.info("ebook_admin_page_loaded models=%s", len(stats))
|
|
return templates.TemplateResponse(request, "admin.html", {"config": config, "stats": stats})
|
|
|
|
|
|
@router.post("/scan", response_class=HTMLResponse)
|
|
def scan_library(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Scan configured library paths for EPUB changes."""
|
|
try:
|
|
count = ingest_configured_paths(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
logger.exception("ebook_admin_scan_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info("ebook_admin_scan_complete changed_files=%s", count)
|
|
if count > 0:
|
|
schedule_bm25_refresh(request.app)
|
|
return templates.TemplateResponse(request, "partials/admin_status.html", {"message": f"Indexed {count} EPUBs"})
|
|
|
|
|
|
@router.post("/generate-ngrams", response_class=HTMLResponse)
|
|
def generate_ngrams(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Generate candidate n-grams for indexed books without LLM judging."""
|
|
try:
|
|
result = generate_candidate_phrases_for_books(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
session.rollback()
|
|
logger.exception("ebook_admin_generate_ngrams_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info(
|
|
"ebook_admin_generate_ngrams_complete books_seen=%s books_built=%s candidates=%s",
|
|
result.books_seen,
|
|
result.books_built,
|
|
result.candidate_phrases,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{
|
|
"message": (
|
|
f"Generated n-grams for {result.books_built} of {result.books_seen} books; "
|
|
f"{result.candidate_phrases} candidates stored"
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/judge-ngrams", response_class=HTMLResponse)
|
|
def judge_ngrams(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Judge stored candidate n-grams and promote accepted protected phrases."""
|
|
try:
|
|
result = judge_candidate_phrases_for_books(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
session.rollback()
|
|
logger.exception("ebook_admin_judge_ngrams_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info(
|
|
"ebook_admin_judge_ngrams_complete books_seen=%s books_judged=%s books_failed=%s candidates_judged=%s "
|
|
"protected=%s mentions=%s",
|
|
result.books_seen,
|
|
result.books_judged,
|
|
result.books_failed,
|
|
result.candidates_judged,
|
|
result.protected_phrases,
|
|
result.phrase_mentions,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{
|
|
"message": (
|
|
f"Judged {result.candidates_judged} candidates across {result.books_judged} of "
|
|
f"{result.books_seen} books; {result.protected_phrases} protected phrases, "
|
|
f"{result.phrase_mentions} mentions"
|
|
+ (f"; {result.books_failed} books failed" if result.books_failed else "")
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/embed-missing", response_class=HTMLResponse)
|
|
def embed_missing(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Embed chunks missing vectors for the configured model."""
|
|
try:
|
|
count = embed_missing_chunks(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
logger.exception("ebook_admin_embed_missing_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info("ebook_admin_embed_missing_complete chunks=%s", count)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{"message": f"Embedded {count} chunks"},
|
|
)
|
|
|
|
|
|
@router.post("/embed-all", response_class=HTMLResponse)
|
|
def embed_all(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Embed all chunks missing vectors in fixed-size batches."""
|
|
total = 0
|
|
batches = 0
|
|
try:
|
|
while True:
|
|
count = embed_missing_chunks(session, config)
|
|
if count == 0:
|
|
break
|
|
session.commit()
|
|
total += count
|
|
batches += 1
|
|
logger.info(
|
|
"ebook_admin_embed_all_batch_complete batch=%s chunks=%s total_chunks=%s",
|
|
batches,
|
|
count,
|
|
total,
|
|
)
|
|
except Exception as error:
|
|
logger.exception(
|
|
"ebook_admin_embed_all_failed batches=%s chunks=%s",
|
|
batches,
|
|
total,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/error.html",
|
|
{"message": f"Embed all failed after {total} chunks in {batches} batches: {error}"},
|
|
status_code=500,
|
|
)
|
|
|
|
logger.info("ebook_admin_embed_all_complete batches=%s chunks=%s", batches, total)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{"message": f"Embedded {total} chunks in {batches} batches of {config.embedding_batch_size}"},
|
|
)
|