diff --git a/alembic/versions/0078_ml_settings_detectors.py b/alembic/versions/0078_ml_settings_detectors.py new file mode 100644 index 0000000..6d04601 --- /dev/null +++ b/alembic/versions/0078_ml_settings_detectors.py @@ -0,0 +1,83 @@ +"""ml_settings crop-proposer / detector config (#134) + +Move the WHERE-to-crop detector config (per-proposer enable + weights + conf, +plus caps + dedupe IoU) into the DB so it's UI-tunable and announced to the GPU +agent in the lease (like the embedder model) — no restart, agent env is now +bootstrap-only. All server_defaults are the working values so existing rows + +fresh installs crop out-of-the-box with all three proposers ON. + +Revision ID: 0078 +Revises: 0077 +Create Date: 2026-07-05 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0078" +down_revision: Union[str, None] = "0077" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +_ANATOMY_DEFAULT = ( + "https://github.com/aperveyev/booru_yolo/raw/main/models/yolov11m_aa22.pt" +) +_PANEL_DEFAULT = "mosesb/best-comic-panel-detection::best.pt" + + +def upgrade() -> None: + op.add_column("ml_settings", sa.Column( + "detector_person_enabled", sa.Boolean(), nullable=False, + server_default=sa.true())) + op.add_column("ml_settings", sa.Column( + "detector_person_weights", sa.String(512), nullable=False, + server_default="yolo11n.pt")) + op.add_column("ml_settings", sa.Column( + "detector_person_conf", sa.Float(), nullable=False, + server_default=sa.text("0.35"))) + op.add_column("ml_settings", sa.Column( + "detector_anatomy_enabled", sa.Boolean(), nullable=False, + server_default=sa.true())) + op.add_column("ml_settings", sa.Column( + "detector_anatomy_weights", sa.String(512), nullable=False, + server_default=_ANATOMY_DEFAULT)) + op.add_column("ml_settings", sa.Column( + "detector_anatomy_conf", sa.Float(), nullable=False, + server_default=sa.text("0.30"))) + op.add_column("ml_settings", sa.Column( + "detector_panel_enabled", sa.Boolean(), nullable=False, + server_default=sa.true())) + op.add_column("ml_settings", sa.Column( + "detector_panel_weights", sa.String(512), nullable=False, + server_default=_PANEL_DEFAULT)) + op.add_column("ml_settings", sa.Column( + "detector_panel_conf", sa.Float(), nullable=False, + server_default=sa.text("0.30"))) + op.add_column("ml_settings", sa.Column( + "detector_max_figures", sa.Integer(), nullable=False, + server_default=sa.text("8"))) + op.add_column("ml_settings", sa.Column( + "detector_max_components", sa.Integer(), nullable=False, + server_default=sa.text("8"))) + op.add_column("ml_settings", sa.Column( + "detector_max_panels", sa.Integer(), nullable=False, + server_default=sa.text("8"))) + op.add_column("ml_settings", sa.Column( + "detector_max_regions", sa.Integer(), nullable=False, + server_default=sa.text("128"))) + op.add_column("ml_settings", sa.Column( + "detector_dedupe_iou", sa.Float(), nullable=False, + server_default=sa.text("0.85"))) + + +def downgrade() -> None: + for col in ( + "detector_person_enabled", "detector_person_weights", "detector_person_conf", + "detector_anatomy_enabled", "detector_anatomy_weights", "detector_anatomy_conf", + "detector_panel_enabled", "detector_panel_weights", "detector_panel_conf", + "detector_max_figures", "detector_max_components", "detector_max_panels", + "detector_max_regions", "detector_dedupe_iou", + ): + op.drop_column("ml_settings", col) diff --git a/backend/app/models/ml_settings.py b/backend/app/models/ml_settings.py index a45cc3d..dfb97d5 100644 --- a/backend/app/models/ml_settings.py +++ b/backend/app/models/ml_settings.py @@ -91,6 +91,70 @@ class MLSettings(Base): embedder_model_name: Mapped[str] = mapped_column( String(128), nullable=False, default="google/siglip2-so400m-patch16-512" ) + # -- Crop proposers / detectors (#1202, #134) -------------------------- + # WHERE-to-crop YOLO detectors feeding the crop→SigLIP bag + CCIP. Config + # lives HERE (DB) and is announced to the GPU agent in the lease — same as + # the embedder model — so it is UI-tunable with NO restart, and the agent's + # env is bootstrap-only. Each weights spec is an ultralytics builtin name, + # an http(s) URL, or "hf_repo::file" (agent's _resolve). enabled off (or an + # empty weights) skips that proposer. All ON by default (operator 2026-07-05) + # so a fresh install crops out-of-the-box. + # person: general COCO figure detector for Western/realistic art the anime + # person-detector misses → NMS-merged with imgutils → CCIP + concept. + detector_person_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True + ) + detector_person_weights: Mapped[str] = mapped_column( + String(512), nullable=False, default="yolo11n.pt" + ) + detector_person_conf: Mapped[float] = mapped_column( + Float, nullable=False, default=0.35 + ) + # anatomy: booru_yolo anime/furry/NSFW torso components → concept crops. + # Default = yolov11m_aa22 (26 classes, best mAP50-95 0.96), committed in the + # upstream repo so the URL resolves. License UNSTATED — fine for a private + # homelab (operator accepted #1202). + detector_anatomy_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True + ) + detector_anatomy_weights: Mapped[str] = mapped_column( + String(512), nullable=False, + default=( + "https://github.com/aperveyev/booru_yolo/raw/main/models/" + "yolov11m_aa22.pt" + ), + ) + detector_anatomy_conf: Mapped[float] = mapped_column( + Float, nullable=False, default=0.30 + ) + # panel: comic page → panel regions → concept crops (Apache-2.0, YOLOv12x). + detector_panel_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True + ) + detector_panel_weights: Mapped[str] = mapped_column( + String(512), nullable=False, + default="mosesb/best-comic-panel-detection::best.pt", + ) + detector_panel_conf: Mapped[float] = mapped_column( + Float, nullable=False, default=0.30 + ) + # Per-frame caps bound the crop→embed explosion; max_regions is the hard + # per-job backstop; dedupe_iou drops near-duplicate crops before the embed. + detector_max_figures: Mapped[int] = mapped_column( + Integer, nullable=False, default=8 + ) + detector_max_components: Mapped[int] = mapped_column( + Integer, nullable=False, default=8 + ) + detector_max_panels: Mapped[int] = mapped_column( + Integer, nullable=False, default=8 + ) + detector_max_regions: Mapped[int] = mapped_column( + Integer, nullable=False, default=128 + ) + detector_dedupe_iou: Mapped[float] = mapped_column( + Float, nullable=False, default=0.85 + ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )