feat: refactor pHash threshold to percentage and separate auto-link vs suggest
Switched from Hamming distance (0-64) to similarity percentage (0-100%) for better user understanding. Also introduced separate thresholds for: 1. Auto-linking: Strict threshold (92%) for high-confidence automatic linking. 2. Suggestion: Permissive threshold (82%) for manual linking candidates.
This commit is contained in:
@@ -22,17 +22,44 @@ def find_closest_parent(
|
||||
before_timestamp: float | None = None,
|
||||
sort_strategy: str = "phash_only",
|
||||
) -> int | None:
|
||||
"""
|
||||
Find the most likely parent image based on pHash distance.
|
||||
|
||||
DOCUMENTATION FOR AI DEVELOPERS:
|
||||
There are two distinct types of thresholds used in this system:
|
||||
|
||||
1. Auto-linking Threshold (gallery.auto_link_phash_threshold):
|
||||
- Used during new image registration and full library scans.
|
||||
- PURPOSE: To automatically establish a high-confidence lineage without manual intervention.
|
||||
- POLICY: Only link if there is a VERY high probability of relatedness. If not met,
|
||||
it's better to leave the image without a parent and let the user link it manually.
|
||||
- DEFAULT: 92% (pHash distance <= 5)
|
||||
|
||||
2. Suggestion Threshold (gallery.suggest_phash_threshold):
|
||||
- Used for manual "Add source image" dialogs.
|
||||
- PURPOSE: To show a list of potential candidates that the user might want to link.
|
||||
- POLICY: Be more permissive to help users find relevant ancestors even if significant
|
||||
changes were made (e.g., changing many prompts or models).
|
||||
- DEFAULT: 82% (pHash distance <= 11)
|
||||
|
||||
CRITICAL RULE: NEVER use the permissive Suggestion Threshold for automatic linking tasks.
|
||||
Doing so creates incorrect lineage data that is difficult for users to clean up.
|
||||
"""
|
||||
if not phash:
|
||||
return None
|
||||
|
||||
if threshold is None:
|
||||
# Fetch from settings if not provided
|
||||
# FALLBACK: Use Suggestion Threshold if no threshold is provided.
|
||||
# However, callers SHOULD provide the correct threshold based on the task.
|
||||
cursor.execute("SELECT value FROM settings WHERE key = ?", ("gallery.suggest_phash_threshold",))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
try:
|
||||
val = json.loads(row[0])
|
||||
threshold = val if isinstance(val, int) else 8
|
||||
if isinstance(val, (int, float)):
|
||||
threshold = round(64 * (1 - val / 100))
|
||||
else:
|
||||
threshold = 8
|
||||
except Exception:
|
||||
threshold = 8
|
||||
else:
|
||||
|
||||
@@ -73,6 +73,16 @@ def get_parent_suggestions(
|
||||
strategy: str = "phash_created",
|
||||
threshold: int = 12,
|
||||
) -> list[ParentSuggestion]:
|
||||
"""
|
||||
Get a list of candidate parent images for manual selection.
|
||||
|
||||
DOCUMENTATION FOR AI DEVELOPERS:
|
||||
This function is used for MANUAL linking via the UI dialog.
|
||||
It uses a more permissive threshold (gallery.suggest_phash_threshold)
|
||||
to help users find potential ancestors even if they have diverged significantly.
|
||||
|
||||
DO NOT use this permissive threshold for automatic linking tasks.
|
||||
"""
|
||||
# 1. Find source filename matches (if strategy includes it)
|
||||
source_matches = []
|
||||
if strategy == "filename_phash":
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
@@ -121,12 +122,40 @@ def infer_parent_id(
|
||||
prompt_json: str | dict | None = None,
|
||||
threshold: int | None = None,
|
||||
) -> int | None:
|
||||
"""
|
||||
Infer the parent ID for an image.
|
||||
This function is used for AUTOMATIC linking during registration or scan.
|
||||
|
||||
DOCUMENTATION FOR AI DEVELOPERS:
|
||||
This function MUST use a strict threshold (gallery.auto_link_phash_threshold)
|
||||
by default. Automatic linking should only happen when there is high confidence
|
||||
that the images are related.
|
||||
|
||||
- Auto-linking is for high-probability relationships.
|
||||
- If the relationship is ambiguous, DO NOT link automatically.
|
||||
- Users can always use the 'Add Source' dialog (which uses a more permissive
|
||||
threshold) to link images manually.
|
||||
"""
|
||||
from .image_service import extract_source_filenames
|
||||
|
||||
parent_id = None
|
||||
if created_at is None:
|
||||
created_at = time.time()
|
||||
|
||||
# Determine threshold for pHash matching
|
||||
if threshold is None:
|
||||
# Fetch auto-linking threshold from settings
|
||||
cursor.execute("SELECT value FROM settings WHERE key = ?", ("gallery.auto_link_phash_threshold",))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
try:
|
||||
val = json.loads(row[0])
|
||||
threshold = round(64 * (1 - val / 100))
|
||||
except Exception:
|
||||
threshold = 5 # Default distance for 92%
|
||||
else:
|
||||
threshold = 5
|
||||
|
||||
# 1. Try to get source filenames (for filename_phash strategy)
|
||||
source_filenames = []
|
||||
if strategy == "filename_phash":
|
||||
@@ -421,8 +450,9 @@ def _scan_thread(
|
||||
|
||||
parent_id = infer_parent_id(cursor, fname, subf, itype, iphash, icreated, strategy=matching_strategy)
|
||||
|
||||
if parent_id and parent_id != img_id:
|
||||
cursor.execute("UPDATE images SET parent_id = ? WHERE id = ?", (parent_id, img_id))
|
||||
if link_strategy == "all" or parent_id:
|
||||
if parent_id != img_id:
|
||||
cursor.execute("UPDATE images SET parent_id = ? WHERE id = ?", (parent_id, img_id))
|
||||
|
||||
processed_linking += 1
|
||||
if processed_linking % 5 == 0 or processed_linking == total_linking:
|
||||
|
||||
@@ -37,14 +37,14 @@ export const SettingsModal: React.FC = () => {
|
||||
useState<string>(state.settings["viewer.thumbnail_window_size"].toString());
|
||||
const [trashRetentionDaysInput, setTrashRetentionDaysInput] =
|
||||
useState<string>(state.settings["gallery.trash_retention_days"].toString());
|
||||
const [autoLinkPhashThresholdInput, setAutoLinkPhashThresholdInput] =
|
||||
useState<string>(
|
||||
state.settings["gallery.auto_link_phash_threshold"].toString(),
|
||||
);
|
||||
const [suggestPhashThresholdInput, setSuggestPhashThresholdInput] =
|
||||
useState<string>(
|
||||
state.settings["gallery.suggest_phash_threshold"].toString(),
|
||||
);
|
||||
const [relatedPhashThresholdInput, setRelatedPhashThresholdInput] =
|
||||
useState<string>(
|
||||
state.settings["gallery.related_phash_threshold"].toString(),
|
||||
);
|
||||
const [maxPositivePromptLinesInput, setMaxPositivePromptLinesInput] =
|
||||
useState<string>(
|
||||
state.settings["viewer.details.max_positive_prompt_lines"].toString(),
|
||||
@@ -148,24 +148,24 @@ export const SettingsModal: React.FC = () => {
|
||||
max: 365,
|
||||
},
|
||||
{
|
||||
key: "gallery.suggest_phash_threshold",
|
||||
label: "Parent Suggestion Threshold",
|
||||
key: "gallery.auto_link_phash_threshold",
|
||||
label: "Auto Linking Threshold (%)",
|
||||
description:
|
||||
"Maximum pHash distance for automatic parent linking (0-64). Default: 12. Recommended: 8-16. Smaller is stricter.",
|
||||
"Minimum similarity percentage for automatic parent linking during creation or scan (0-100). Default: 92%. Should be strict to avoid false links.",
|
||||
type: "number",
|
||||
category: "General",
|
||||
min: 0,
|
||||
max: 64,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
key: "gallery.related_phash_threshold",
|
||||
label: "Related Images Threshold",
|
||||
key: "gallery.suggest_phash_threshold",
|
||||
label: "Parent Suggestion Threshold (%)",
|
||||
description:
|
||||
"Maximum pHash distance for 'Similar Images' view (0-64). Default: 8. Recommended: 4-12. Smaller is stricter.",
|
||||
"Minimum similarity percentage for showing candidates in manual 'Add Source' dialog (0-100). Default: 82%. Permissive enough to find ancestors.",
|
||||
type: "number",
|
||||
category: "General",
|
||||
min: 0,
|
||||
max: 64,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
key: "sidebar.show_filename",
|
||||
@@ -667,10 +667,10 @@ export const SettingsModal: React.FC = () => {
|
||||
setThumbnailWindowSizeInput(value);
|
||||
} else if (key === "gallery.trash_retention_days") {
|
||||
setTrashRetentionDaysInput(value);
|
||||
} else if (key === "gallery.auto_link_phash_threshold") {
|
||||
setAutoLinkPhashThresholdInput(value);
|
||||
} else if (key === "gallery.suggest_phash_threshold") {
|
||||
setSuggestPhashThresholdInput(value);
|
||||
} else if (key === "gallery.related_phash_threshold") {
|
||||
setRelatedPhashThresholdInput(value);
|
||||
} else if (key === "viewer.details.max_positive_prompt_lines") {
|
||||
setMaxPositivePromptLinesInput(value);
|
||||
} else if (key === "viewer.details.max_negative_prompt_lines") {
|
||||
@@ -868,11 +868,11 @@ export const SettingsModal: React.FC = () => {
|
||||
"gallery.trash_retention_days"
|
||||
? trashRetentionDaysInput
|
||||
: config.key ===
|
||||
"gallery.suggest_phash_threshold"
|
||||
? suggestPhashThresholdInput
|
||||
"gallery.auto_link_phash_threshold"
|
||||
? autoLinkPhashThresholdInput
|
||||
: config.key ===
|
||||
"gallery.related_phash_threshold"
|
||||
? relatedPhashThresholdInput
|
||||
"gallery.suggest_phash_threshold"
|
||||
? suggestPhashThresholdInput
|
||||
: (localSettings[
|
||||
config.key
|
||||
] as unknown as number)
|
||||
@@ -924,6 +924,14 @@ export const SettingsModal: React.FC = () => {
|
||||
"gallery.trash_retention_days"
|
||||
].toString(),
|
||||
);
|
||||
} else if (
|
||||
config.key === "gallery.auto_link_phash_threshold"
|
||||
) {
|
||||
setAutoLinkPhashThresholdInput(
|
||||
localSettings[
|
||||
"gallery.auto_link_phash_threshold"
|
||||
].toString(),
|
||||
);
|
||||
} else if (
|
||||
config.key === "gallery.suggest_phash_threshold"
|
||||
) {
|
||||
@@ -932,14 +940,6 @@ export const SettingsModal: React.FC = () => {
|
||||
"gallery.suggest_phash_threshold"
|
||||
].toString(),
|
||||
);
|
||||
} else if (
|
||||
config.key === "gallery.related_phash_threshold"
|
||||
) {
|
||||
setRelatedPhashThresholdInput(
|
||||
localSettings[
|
||||
"gallery.related_phash_threshold"
|
||||
].toString(),
|
||||
);
|
||||
} else if (
|
||||
config.key ===
|
||||
"viewer.details.max_positive_prompt_lines"
|
||||
|
||||
@@ -75,8 +75,8 @@ export const initialState: GalleryState = {
|
||||
"gallery.trash_retention_days": 30,
|
||||
"gallery.trash.show_missing": false,
|
||||
"gallery.view_mode": "grid_details",
|
||||
"gallery.related_phash_threshold": 8,
|
||||
"gallery.suggest_phash_threshold": 12,
|
||||
"gallery.suggest_phash_threshold": 82,
|
||||
"gallery.auto_link_phash_threshold": 92,
|
||||
"viewer.shortcut.1": "tag:keep next",
|
||||
"viewer.shortcut.2": "tag:best next",
|
||||
"viewer.shortcut.3": "tag:fix-needed next",
|
||||
|
||||
+1
-1
@@ -99,8 +99,8 @@ export interface Settings {
|
||||
"gallery.trash_retention_days": number;
|
||||
"gallery.trash.show_missing": boolean;
|
||||
"gallery.view_mode": "grid_details" | "grid_only";
|
||||
"gallery.related_phash_threshold": number;
|
||||
"gallery.suggest_phash_threshold": number;
|
||||
"gallery.auto_link_phash_threshold": number;
|
||||
"viewer.shortcut.1": string;
|
||||
"viewer.shortcut.2": string;
|
||||
"viewer.shortcut.3": string;
|
||||
|
||||
Reference in New Issue
Block a user