CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 16s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 31s
CI and images / integration (push) Successful in 2m37s
CI and images / sign-extension (push) Successful in 2s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m36s
CI and images / smoke-web (push) Successful in 56s
CI and images / promote (push) Successful in 2s
Five of Todding's Discord doodles (pencil lines on white, 3000px) were skipped on import as "single color", so their posts showed text and no image. The predicate sampled a 64px BILINEAR thumbnail, which blends thin strokes into the paper, and 0.95 "one color" is below how white a doodle is. - Sample 256x256 by NEAREST, so each sample is a real pixel. - Default threshold 0.995: blank means essentially blank. Migration 0116 moves a stored 0.95 (the old default) with it; the settings slider now spans 0.9-1 in 0.005 steps so the value is reachable. - The Cleanup audit shares the predicate, so it stops flagging sketches too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
12 KiB
Python
251 lines
12 KiB
Python
"""ImportSettings — single-row table holding the importer's tunable knobs.
|
|
|
|
Enforced as a single row via a CHECK (id = 1) constraint. The application
|
|
always SELECTs id=1 and never inserts/deletes after the initial migration.
|
|
"""
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
CheckConstraint,
|
|
Float,
|
|
Integer,
|
|
Text,
|
|
select,
|
|
text,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from .base import Base
|
|
|
|
|
|
class ImportSettings(Base):
|
|
__tablename__ = "import_settings"
|
|
# Bare constraint name — Base.metadata's naming convention applies the
|
|
# ck_<table>_<name> prefix, producing the final ck_import_settings_singleton.
|
|
# Bare name — Base.metadata's naming convention prepends ck_<table>_,
|
|
# producing ck_import_settings_singleton. The chain shipped the DOUBLED
|
|
# ck_import_settings_ck_import_settings_singleton, because the migration
|
|
# pre-prefixed the name and the convention prefixed it again; alembic
|
|
# 0088 renames it to what this line has always produced (#3275).
|
|
__table_args__ = (CheckConstraint("id = 1", name="singleton"),)
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
import_scan_path: Mapped[str] = mapped_column(Text, nullable=False, default="/import", server_default="/import")
|
|
|
|
min_width: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
min_height: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
|
|
skip_transparent: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
|
transparency_threshold: Mapped[float] = mapped_column(Float, nullable=False, default=0.9, server_default="0.9")
|
|
|
|
skip_single_color: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
|
single_color_threshold: Mapped[float] = mapped_column(Float, nullable=False, default=0.995, server_default="0.995")
|
|
single_color_tolerance: Mapped[int] = mapped_column(Integer, nullable=False, default=30, server_default="30")
|
|
|
|
# Hamming distance over a 256-bit pHash (utils.phash, hash_size=16). The
|
|
# unit CHANGED in migration 0098 — it used to be bits out of 64 — so the
|
|
# old default of 10 is not this scale's 10, and 0098 resets every row.
|
|
# This is now the cheap PRE-FILTER: the aspect + pixel gates decide, which
|
|
# is what lets it be generous enough to catch a re-encoded rescale.
|
|
phash_threshold: Mapped[int] = mapped_column(Integer, nullable=False, default=24, server_default="24")
|
|
|
|
# FC-3c downloader knobs
|
|
download_rate_limit_seconds: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=3.0,
|
|
server_default="3",
|
|
)
|
|
download_validate_files: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True,
|
|
server_default="true",
|
|
)
|
|
|
|
# FC-3d scheduling knobs
|
|
download_schedule_default_seconds: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=28800,
|
|
server_default="28800",
|
|
)
|
|
download_event_retention_days: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=90,
|
|
server_default="90",
|
|
)
|
|
# How far back a routine tick keeps looking after it has run out of new
|
|
# posts, so a creator who EDITS an older post to attach a hotfix build is
|
|
# still reached (ingest_core.DEFAULT_REVISIT_DAYS carries the reasoning).
|
|
# A knob rather than a constant because how long a creator keeps editing is
|
|
# a property of the creator, not of FabledCurator: 0 turns the revisit off
|
|
# and restores the pure count early-out.
|
|
download_revisit_days: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=30,
|
|
server_default="30",
|
|
)
|
|
download_failure_warning_threshold: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=5,
|
|
server_default="5",
|
|
)
|
|
|
|
# FC-3h backup knobs.
|
|
backup_db_nightly_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False,
|
|
server_default="false",
|
|
)
|
|
backup_db_nightly_hour_utc: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=3,
|
|
server_default="3",
|
|
)
|
|
backup_db_keep_last_n: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=14,
|
|
server_default="14",
|
|
)
|
|
backup_images_keep_last_n: Mapped[int] = mapped_column(
|
|
Integer, nullable=False, default=3,
|
|
server_default="3",
|
|
)
|
|
|
|
# FC-6.3 series continuation matcher. enabled gates the rescan; threshold is
|
|
# the weighted-score cut-off (0..1) above which a pending suggestion is made.
|
|
series_suggest_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True,
|
|
server_default="true",
|
|
)
|
|
series_suggest_threshold: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=0.5,
|
|
server_default="0.5",
|
|
)
|
|
|
|
# Milestone 388 E5 — the announcement matcher: "this Patreon post announced
|
|
# that Discord drop". Lives here rather than in MLSettings, with the series
|
|
# matcher it is modelled on, because it runs no inference: the signals are
|
|
# time proximity and whether the post says so.
|
|
discord_link_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True,
|
|
server_default="true",
|
|
)
|
|
# The weighted-score cut-off. 0.60 is not arbitrary: it is deliberately set
|
|
# ABOVE the largest single signal weight, which is what makes "time
|
|
# proximity alone must never be sufficient" an ARITHMETIC property rather
|
|
# than a hope. On a busy day an artist posts several times; if proximity
|
|
# could carry a pair by itself, every one of those days would produce false
|
|
# pairs and the review queue would be abandoned. See
|
|
# post_association_service.WEIGHTS — a guard test pins the relationship.
|
|
discord_link_threshold: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=0.60,
|
|
server_default="0.60",
|
|
)
|
|
# How far apart the announcement and the drop may be. The Patreon post
|
|
# exists IN ORDER TO announce the drop, so they are minutes-to-hours apart;
|
|
# a day is generous and still excludes "same week".
|
|
discord_link_window_hours: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=24.0,
|
|
server_default="24",
|
|
)
|
|
|
|
# Whether FC links a CONCLUSIVE pair without asking.
|
|
#
|
|
# Operator, 2026-09-24: *"I don't want this to be manual that defeats the
|
|
# convenience that I'm going for."* Confirm-only was the right default
|
|
# while the only signals were circumstantial — proximity and a body that
|
|
# mentions Discord can never be more than suggestive, and asking was the
|
|
# honest response to that. A shared working name is different in kind: when
|
|
# the name appears in exactly these two posts and nowhere else in the
|
|
# artist's library, there is nothing for the operator to adjudicate.
|
|
#
|
|
# Only the conclusive band is affected (post_association_service.
|
|
# AUTO_LINK_FLOOR). Everything weaker still queues, and an accepted link is
|
|
# a row the operator can dismiss, so this is reversible in the UI rather
|
|
# than only in the database.
|
|
discord_link_auto: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True,
|
|
server_default="true",
|
|
)
|
|
|
|
# The unified card (#4402). A linked Discord drop is NOT absorbed into its
|
|
# teaser — it keeps its own post, date and provenance, and the teaser's card
|
|
# shows it by REFERENCE. Operator, 2026-09-24: *"discord 'posts' land as
|
|
# normal and only hidden from the post view they're posted the same day."*
|
|
#
|
|
# So the drop's own card leaves the feed only when it sits within this many
|
|
# hours of the teaser that references it — the adjacency that reads as the
|
|
# same thing twice. Hours rather than a calendar day: a teaser at 23:00 and
|
|
# its drop at 01:00 are one release, and "the same day" has no timezone
|
|
# the server can know.
|
|
discord_link_fold_hours: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=24.0,
|
|
server_default="24",
|
|
)
|
|
# How far from the teaser the card reaches for the rest of a piece's
|
|
# variants — the wips, alts and censor passes a creator trickles out under
|
|
# one working name (#4401). Measured on artist 8: named families spread a
|
|
# median 5 days and up to 44, while every name collision found spreads
|
|
# over 500. A reference, not a regrouping, so a generous value costs one
|
|
# extra thumbnail at worst — never a post moved or hidden.
|
|
discord_family_window_days: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=60.0,
|
|
server_default="60",
|
|
)
|
|
|
|
# #830 off-platform file-host downloads — per-host enable lever (default on,
|
|
# rule #26). Column names are extdl_<host>_enabled so the worker reads them
|
|
# via getattr(settings, f"extdl_{host}_enabled", True).
|
|
extdl_mega_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
extdl_gdrive_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
extdl_mediafire_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
extdl_dropbox_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
extdl_pixeldrain_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
|
|
# -- Post-text translation via the Interpreter LAN service (milestone 143).
|
|
# Off by default with NO default host — it needs a reachable Interpreter
|
|
# service (the operator's, behind a reverse proxy), which not every install
|
|
# has; the operator sets the URL and flips it on. Empty base_url OR disabled
|
|
# → the translate sweep no-ops.
|
|
translation_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False, server_default="false",
|
|
)
|
|
interpreter_base_url: Mapped[str] = mapped_column(
|
|
Text, nullable=False, default="", server_default="",
|
|
)
|
|
translation_target_lang: Mapped[str] = mapped_column(
|
|
Text, nullable=False, default="en", server_default="en",
|
|
)
|
|
# The latin-script acceptance floor for the translation gate: a translation
|
|
# whose Interpreter-reported confidence is below this is kept as the original
|
|
# (operator-tunable, milestone 155). Default 0.9 — stricter than the old
|
|
# hardcoded 0.8, because Interpreter confidently mis-detects short ASCII
|
|
# English (e.g. "… WIP Part 1") as a European language at ~0.86. CJK stays
|
|
# trusted regardless (script-detected). Per-post overrides handle the misses.
|
|
translation_min_confidence: Mapped[float] = mapped_column(
|
|
# text() because alembic 0084 used sa.text(); see ml_settings for why
|
|
# the form matters and why it is per-column (#3275).
|
|
Float, nullable=False, default=0.9, server_default=text("0.9"),
|
|
)
|
|
|
|
# Title-based WIP auto-tagging (task #1458). When a freshly-imported post's
|
|
# TITLE explicitly declares work-in-progress ("WIP" / "work in progress"),
|
|
# the importer applies the `wip` system tag to its images — the artist's own
|
|
# label, used to keep unfinished pieces out of the Explore/gallery browse. ON
|
|
# by default (rule 26 — the feature works out of the box). Gates only the
|
|
# LIVE import hook; the existing catalogue is caught by the operator-triggered
|
|
# "Scan existing posts" backfill (which runs regardless of this flag).
|
|
wip_title_tagging_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default="true",
|
|
)
|
|
|
|
@classmethod
|
|
async def load(cls, session) -> ImportSettings:
|
|
"""The singleton settings row (id=1), via an async session."""
|
|
return (await session.execute(select(cls).where(cls.id == 1))).scalar_one()
|
|
|
|
@classmethod
|
|
def load_sync(cls, session) -> ImportSettings:
|
|
"""The singleton settings row (id=1), via a sync session."""
|
|
return session.execute(select(cls).where(cls.id == 1)).scalar_one()
|