From 834ef1fef35aa06c32fb5afaeb8a0a600fe95cb8 Mon Sep 17 00:00:00 2001 From: MajoorWaldi Date: Fri, 6 Mar 2026 12:43:46 +0100 Subject: [PATCH] feat(docs): comprehensive AI features guide and new documentation - Added new AI_FEATURES.md guide covering all AI-powered capabilities - Updated DOCUMENTATION_INDEX.md to include new AI features guide - Added AI Features section to README.md with overview and models used - Improved backfill warning information in README.md - Updated reset index / delete DB procedures in DB_MAINTENANCE.md - Added configuration options for AI features in README.md --- README.md | 148 +++ docs/AI_FEATURES.md | 1081 +++++++++++++++++ docs/DB_MAINTENANCE.md | 4 +- docs/DOCUMENTATION_INDEX.md | 20 +- js/api/client.js | 13 +- js/app/dialogs.js | 279 ++++- js/app/i18n.js | 144 ++- js/app/settings/settingsAdvanced.js | 20 +- js/app/settings/settingsCore.js | 5 +- js/app/versionCheck.js | 4 +- .../contextmenu/addToCollectionMenu.js | 35 +- .../controllers/collectionsController.js | 29 +- js/features/contextmenu/GridContextMenu.js | 8 +- js/features/panel/AssetsManagerPanel.js | 73 +- .../panel/controllers/contextController.js | 9 + .../panel/controllers/gridController.js | 38 +- .../panel/controllers/scopeController.js | 56 +- .../panel/controllers/sidebarController.js | 6 +- js/features/panel/messages/messageCenter.js | 324 +++++ .../messages/messagePopoverController.js | 250 ++++ js/features/panel/state/panelState.js | 12 +- js/features/panel/views/contextPillsView.js | 22 +- js/features/panel/views/headerView.js | 10 + js/features/panel/views/iconButton.js | 2 + js/features/panel/views/messagePopoverView.js | 73 ++ js/features/panel/views/popoverManager.js | 17 + js/features/panel/views/summaryBarView.js | 33 +- js/features/panel/views/tabsView.js | 5 +- js/features/status/StatusDot.js | 84 +- js/features/viewer/FloatingViewer.js | 514 ++++++-- js/features/viewer/floatingViewerManager.js | 40 +- js/tests/floating_viewer.vitest.mjs | 169 +++ js/tests/floating_viewer_manager.vitest.mjs | 324 +++++ .../grid_controller_semantic_mode.vitest.mjs | 113 ++ js/tests/message_center.vitest.mjs | 115 ++ js/tests/scope_controller.vitest.mjs | 80 ++ js/theme/theme-comfy.css | 457 ++++++- js/utils/deleteGuard.js | 10 +- list.json | 1 + mjr_am_backend/adapters/db/sqlite_facade.py | 10 + mjr_am_backend/config.py | 29 +- .../features/index/index_prepare_ops.py | 23 +- mjr_am_backend/features/index/scanner.py | 87 +- .../features/index/vector_indexer.py | 293 ++++- .../features/index/vector_searcher.py | 19 + .../features/index/vector_service.py | 225 ++-- mjr_am_backend/routes/core/services.py | 38 +- mjr_am_backend/routes/handlers/assets_impl.py | 102 ++ .../routes/handlers/db_maintenance.py | 2 +- .../routes/handlers/hybrid_search.py | 19 +- mjr_am_backend/routes/handlers/scan.py | 53 +- .../routes/handlers/vector_search.py | 115 +- mjr_am_backend/routes/handlers/viewer.py | 56 + requirements.txt | 9 + tests/assets/z-image_00024_.png | Bin 0 -> 365085 bytes tests/core/test_paths_services.py | 66 + tests/features/test_assets_impl_routes.py | 1 - tests/features/test_hybrid_search_routes.py | 52 + tests/features/test_index_prepare_ops.py | 21 + .../features/test_index_prepare_ops_extra.py | 38 + .../test_index_scanner_vector_auto.py | 27 + .../test_prompt_alignment_multisignal.py | 205 ++++ tests/features/test_scan_routes_more.py | 256 ++++ tests/features/test_vector_search_routes.py | 97 ++ .../test_vector_service_video_xclip.py | 18 +- user_guide.html | 795 ++++++++++++ 66 files changed, 6924 insertions(+), 359 deletions(-) create mode 100644 docs/AI_FEATURES.md create mode 100644 js/features/panel/messages/messageCenter.js create mode 100644 js/features/panel/messages/messagePopoverController.js create mode 100644 js/features/panel/views/messagePopoverView.js create mode 100644 js/tests/floating_viewer.vitest.mjs create mode 100644 js/tests/floating_viewer_manager.vitest.mjs create mode 100644 js/tests/grid_controller_semantic_mode.vitest.mjs create mode 100644 js/tests/message_center.vitest.mjs create mode 100644 js/tests/scope_controller.vitest.mjs create mode 100644 list.json create mode 100644 tests/assets/z-image_00024_.png create mode 100644 tests/features/test_prompt_alignment_multisignal.py create mode 100644 user_guide.html diff --git a/README.md b/README.md index c52c00d1..2a4f78d8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Useful links: - ๐Ÿ“ Changelog: [`CHANGELOG.md`](CHANGELOG.md) - ๐Ÿ“š Documentation index: [`docs/DOCUMENTATION_INDEX.md`](docs/DOCUMENTATION_INDEX.md) - ๐Ÿ”ง API reference: [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) +- ๐Ÿค– **AI Features Guide**: [`docs/AI_FEATURES.md`](docs/AI_FEATURES.md) โ€” NEW! --- @@ -45,6 +46,9 @@ Useful links: - [Majoor Floating Viewer (MFV)](#majoor-floating-viewer-mfv) - [Hotkeys & Shortcuts](#hotkeys--shortcuts) - [Advanced Features](#advanced-features) +- [AI Features (How to Use)](#ai-features-how-to-use) +- [Debug: Reset Index / Delete DB](#debug-reset-index--delete-db) +- [Backfill Warning (Important)](#backfill-warning-important) - [Configuration](#configuration) - [Troubleshooting](#troubleshooting) - [Development & Testing](#development--testing) @@ -83,6 +87,17 @@ Useful links: ## What's New in v2.4.0 +### ๐Ÿค– AI Features โ€” Comprehensive Guide โ€” NEW! +Complete AI-powered asset discovery and organization: +- **Semantic Search**: Natural language queries ("sunset over mountains") +- **Find Similar**: Visual similarity matching +- **AI Auto-Tags**: Automatic tagging (portrait, landscape, cyberpunk, etc.) +- **Enhanced Captions**: Florence-2 generated descriptions +- **Prompt Alignment**: Score image-prompt matching quality +- **Smart Collections**: Auto-group by visual themes +- **Models**: SigLIP2 (image/text), X-CLIP (video), Florence-2 (caption) +- See full guide: [`docs/AI_FEATURES.md`](docs/AI_FEATURES.md) + ### ๐ŸŽ‰ Majoor Floating Viewer (MFV) โ€” NEW! ๐ŸŽฏ A lightweight floating viewer panel for real-time generation comparison: - **Live Stream Mode**: Automatically follows new generations from Save/Load nodes @@ -322,6 +337,139 @@ See [`docs/DB_MAINTENANCE.md`](docs/DB_MAINTENANCE.md) for database recovery pro --- +## AI Features (How to Use) + +๐Ÿ“š **Complete Guide**: See [`docs/AI_FEATURES.md`](docs/AI_FEATURES.md) for comprehensive documentation. + +### What AI features are available + +- **๐Ÿ”ฎ AI Semantic Search**: Natural-language search across your library (e.g., "sunset over mountains") +- **๐Ÿ” Find Similar**: Pick one asset and retrieve visually similar assets +- **๐Ÿท๏ธ AI Auto-Tags**: Automatic tag suggestions (portrait, landscape, cyberpunk, anime, etc.) +- **๐Ÿ“ Enhanced Captions**: Florence-2 generated detailed image descriptions +- **๐Ÿ“Š Prompt Alignment Score**: Measure how well an image matches its generation prompt +- **๐Ÿ“ Smart Collections**: Auto-create themed collections from AI suggestions +- **๐Ÿ”ฌ Discover Groups**: Cluster your library by visual similarity + +### How to use AI features + +1. **Enable AI**: Open **Settings** โ†’ Keep **Enable AI semantic search** enabled (default: ON) +2. **Build Vectors**: After indexing, click **Backfill vectors** in **Index Status** +3. **Use Features**: + - **Search**: Click sparkles icon (๐Ÿ”ฎ) โ†’ Type natural language queries + - **Find Similar**: Select asset โ†’ Right-click โ†’ Find Similar + - **Collections**: Open Collections โ†’ Smart Suggestions / Discover Groups + - **Caption**: Open asset โ†’ Sidebar โ†’ Image Description โ†’ Generate + - **Alignment**: Open asset โ†’ Sidebar โ†’ Prompt Alignment score + +### Per-Asset Quick Actions + +You can trigger scan and backfill vectors for **individual assets** directly from the grid: + +| Action | How To | +|--------|--------| +| **Index Asset** | Click status dot on card โ†’ "Index Asset" | +| **Generate Vector** | Click status dot โ†’ "Generate Vector" | +| **Generate Caption** | Hover card โ†’ Click sparkles (๐Ÿ”ฎ) โ†’ Generate Caption | +| **Compute Alignment** | Hover card โ†’ Click sparkles โ†’ Compute Alignment | +| **Suggest Tags** | Hover card โ†’ Click sparkles โ†’ Suggest Tags | +| **Find Similar** | Hover card โ†’ Click sparkles โ†’ Find Similar | + +#### Visual Status Indicators + +| Indicator | Meaning | +|-----------|---------| +| ๐ŸŸข Green dot | Asset fully indexed with vectors | +| ๐ŸŸก Yellow dot | Asset indexed, vectors pending | +| ๐Ÿ”ด Red dot | Indexing failed or vectors unavailable | +| ๐Ÿ”ฎ Sparkles visible | AI features available | +| โณ Spinning icon | Processing in progress | + +### AI Models + +| Model | Purpose | Size | +|-------|---------|------| +| **SigLIP2 SO400M** | Image & text embeddings | ~1.2 GB | +| **X-CLIP Base** | Video embeddings | ~600 MB | +| **Florence-2 Base** | Image captioning | ~800 MB | + +Models are downloaded automatically on first use and cached locally. + +### Quick Tips + +- โœ… Non-AI features work independently if AI is disabled +- โœ… AI quality depends on metadata quality and vector coverage +- โœ… First backfill can take time (5-30 min for 1000 assets) +- โœ… GPU acceleration recommended for large libraries + +### Configuration + +```bash +# Enable/disable AI features (default: ON) +export MJR_AM_ENABLE_VECTOR_SEARCH=1 + +# Adjust auto-tag sensitivity (0.0-1.0, default: 0.06) +export MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.06 + +# Verbose AI logging for debugging +export MJR_AM_AI_VERBOSE_LOGS=1 +``` + +### Performance + +| Library Size | Backfill Time (CPU) | Backfill Time (GPU) | +|--------------|---------------------|---------------------| +| 100 assets | 2-5 min | 30-60 sec | +| 500 assets | 10-25 min | 2-5 min | +| 1000 assets | 20-50 min | 5-10 min | +| 5000 assets | 2-4 hours | 25-50 min | + +*GPU: NVIDIA RTX 3060 or equivalent* + +--- + +## Debug: Reset Index / Delete DB + +Use this when indexing behavior is inconsistent, after major updates, or when the DB is corrupted. + +### Reset Index +- Rebuilds index data and rescans files. +- Best first step when DB is still readable. + +### Delete DB +- Emergency recovery mode: force-delete DB files and rebuild from scratch. +- Use this when DB is corrupted or reset fails. + +### Dialog behavior +When you click **Reset Index** or **Delete DB**, the UI first asks whether existing AI vectors should be kept, then shows a simple confirmation dialog. +- If you keep vectors, the reset/rebuild preserves the existing AI vector data. +- If you do not keep vectors, the reset is full and vectors will be recalculated later if needed. + +--- + +## Backfill Warning (Important) + +Backfilling vectors can take a long time on large libraries, especially over 1000 assets. + +### How backfill works +- It scans indexed assets and computes missing AI embeddings (vectors). +- It runs as a background process, in batches. +- You can continue using **Assets Manager** and **ComfyUI** while it runs. + +### Why it can be slow +- Number of assets and media type mix (images/videos) +- CPU/GPU availability and model load time +- Disk speed and overall system load +- Available system RAM (more RAM helps keep processing smoother) + +### What you get after backfill +- Better semantic search coverage +- Better **Find Similar** results +- Better clustering/smart collection quality +- Better prompt-alignment and AI-assisted metadata workflows + +--- + ## Configuration ### Browser Settings (localStorage) diff --git a/docs/AI_FEATURES.md b/docs/AI_FEATURES.md new file mode 100644 index 00000000..06df7a80 --- /dev/null +++ b/docs/AI_FEATURES.md @@ -0,0 +1,1081 @@ +# Majoor Assets Manager โ€” AI Features Guide + +**Comprehensive guide to AI-powered features in Majoor Assets Manager** + +*Version: 2.4.0* +*Last Updated: March 5, 2026* + +--- + +## Table of Contents + +- [Overview](#overview) +- [AI Models & Technologies](#ai-models--technologies) +- [Enabling AI Features](#enabling-ai-features) +- [Core AI Features](#core-ai-features) + - [Semantic Search](#1-semantic-search) + - [Find Similar](#2-find-similar) + - [AI Auto-Tags](#3-ai-auto-tags) + - [Enhanced Captions (Florence-2)](#4-enhanced-captions-florence-2) + - [Prompt Alignment Score](#5-prompt-alignment-score) + - [Smart Collections](#6-smart-collections) + - [Discover Groups](#7-discover-groups) +- [How It Works](#how-it-works) +- [Configuration & Tuning](#configuration--tuning) +- [Performance Considerations](#performance-considerations) +- [Troubleshooting](#troubleshooting) +- [API Reference](#api-reference) +- [Privacy & Security](#privacy--security) + +--- + +## Overview + +Majoor Assets Manager includes a suite of **AI-powered features** that leverage multimodal embedding models to provide semantic search, visual similarity matching, and automated metadata generation. These features help you discover and organize your generated assets more intelligently. + +### What AI Features Provide + +| Feature | Description | Use Case | +|---------|-------------|----------| +| **Semantic Search** | Search using natural language instead of keywords | "sunset over mountains" finds matching images | +| **Find Similar** | Discover visually similar assets | Find variations of a successful generation | +| **AI Auto-Tags** | Automatic tag suggestions based on image content | Auto-tag "portrait", "landscape", "cyberpunk" | +| **Enhanced Captions** | AI-generated detailed image descriptions | Generate searchable captions for images | +| **Prompt Alignment** | Score how well image matches its prompt | Verify generation quality | +| **Smart Collections** | Auto-group assets by visual similarity | Create themed collections automatically | +| **Discover Groups** | Cluster library by visual themes | Explore your library's content patterns | + +### Key Benefits + +- **Natural Language Search**: No need to remember exact filenames or tags +- **Visual Discovery**: Find assets by appearance, not just metadata +- **Automated Organization**: Reduce manual tagging workload +- **Quality Insights**: Understand prompt-image alignment +- **Themed Grouping**: Automatically discover patterns in your library + +--- + +## AI Models & Technologies + +### Primary Models + +| Model | Purpose | Dimensions | Source | +|-------|---------|------------|--------| +| **SigLIP2 SO400M** | Image & text embeddings | 1152 | Google | +| **X-CLIP Base** | Video embeddings | 768 | Microsoft | +| **Florence-2 Base** | Image captioning | N/A | Microsoft | + +### Technology Stack + +- **Sentence Transformers**: Model loading and inference +- **Faiss**: Vector similarity search (Facebook AI Similarity Search) +- **Transformers**: HuggingFace transformers for model inference +- **SQLite with vector extension**: Embedding storage + +### Model Download & Caching + +Models are downloaded automatically on first use and cached locally: + +``` +Cache Location: ~/.cache/huggingface/hub/ +Models: + - google/siglip-so400m-patch14-384 (SigLIP2) + - microsoft/xclip-base-patch32 (X-CLIP) + - microsoft/Florence-2-base (Florence-2) +``` + +**Initial Download Sizes:** +- SigLIP2: ~1.2 GB +- X-CLIP: ~600 MB +- Florence-2: ~800 MB + +--- + +## Enabling AI Features + +### Method 1: Via Settings UI (Recommended) + +1. Open **Assets Manager** panel in ComfyUI +2. Click **Settings** (gear icon) +3. Find **AI Features** section +4. Toggle **Enable AI semantic search** to ON +5. Wait for initial model download (progress shown in console) +6. Click **Save Settings** + +### Method 2: Via Environment Variable + +Add to your environment before starting ComfyUI: + +```bash +# Windows (PowerShell) +$env:MJR_AM_ENABLE_VECTOR_SEARCH="1" + +# Windows (Command Prompt) +set MJR_AM_ENABLE_VECTOR_SEARCH=1 + +# Linux/macOS +export MJR_AM_ENABLE_VECTOR_SEARCH=1 +``` + +### Verification + +After enabling: + +1. Check console for model loading messages: + ``` + INFO: Loading multimodal embedding model 'google/siglip-so400m-patch14-384' โ€ฆ + INFO: SigLIP2 model loaded and ready: 'google/siglip-so400m-patch14-384' (dim=1152) + ``` + +2. In Assets Manager, look for: + - Sparkles icon (๐Ÿ”ฎ) in search bar for semantic search toggle + - "Find Similar" button in asset context menu + - AI-related options in Collections panel + +### Initial Setup Workflow + +``` +1. Enable AI features in Settings + โ†“ +2. Trigger initial index scan (Ctrl+S) + โ†“ +3. Wait for metadata extraction to complete + โ†“ +4. Click "Backfill vectors" in Index Status + โ†“ +5. Wait for vector embeddings to be computed + โ†“ +6. AI features are now fully functional +``` + +### Per-Asset Vector Operations + +You can trigger scan and backfill vectors for **individual assets** directly from the grid view: + +#### Via Card Status Dot + +1. **Locate the status indicator** on an asset card (small dot in corner) +2. **Click the status dot** to open the asset actions menu +3. Select one of: + - **"Index Asset"** โ€” Trigger metadata scan for this asset only + - **"Generate Vector"** โ€” Compute AI embedding for this asset + - **"Re-index"** โ€” Force re-scan and re-compute vectors + +#### Via Sparkles Icon (Prime Icon) + +1. **Hover over an asset card** to reveal action buttons +2. **Click the sparkles icon** (๐Ÿ”ฎ) if visible on the card +3. Choose from AI-specific actions: + - **Generate Caption** โ€” Run Florence-2 captioning + - **Compute Alignment** โ€” Calculate prompt alignment score + - **Suggest Tags** โ€” Generate AI auto-tags + - **Find Similar** โ€” Show visually similar assets + +#### When to Use Per-Asset Operations + +| Scenario | Recommended Action | +|----------|-------------------| +| New asset added after backfill | Click status dot โ†’ Generate Vector | +| Caption missing/outdated | Click sparkles โ†’ Generate Caption | +| Poor alignment score | Click sparkles โ†’ Re-compute Alignment | +| Asset not appearing in semantic search | Status dot โ†’ Index Asset, then Generate Vector | +| Testing AI features on single image | Use sparkles icon actions | + +#### Visual Indicators + +| Indicator | Meaning | +|-----------|---------| +| ๐ŸŸข Green dot | Asset fully indexed with vectors | +| ๐ŸŸก Yellow dot | Asset indexed, vectors pending | +| ๐Ÿ”ด Red dot | Indexing failed or vectors unavailable | +| ๐Ÿ”ฎ Sparkles visible | AI features available for this asset | +| โณ Spinning icon | Processing in progress | + +
+๐Ÿ’ก Tip: For best results, run a full backfill first, then use per-asset operations to update individual items as needed. +
+ +--- + +## Core AI Features + +### 1. Semantic Search + +Search your asset library using natural language queries instead of keywords. + +#### How to Use + +1. Open **Assets Manager** panel +2. Click the **sparkles icon** (๐Ÿ”ฎ) in the search bar to enable semantic mode +3. Type a natural language query: + - "sunset over mountains with orange sky" + - "cyberpunk city at night" + - "portrait of a woman with blue hair" +4. Press Enter to search + +#### Query Examples + +| Query Type | Example | +|------------|---------| +| **Visual description** | "red sports car on mountain road" | +| **Style/Mood** | "dark gothic castle in mist" | +| **Color-based** | "images with dominant blue tones" | +| **Subject-based** | "anime character with long white hair" | +| **French queries** | "chien" โ†’ finds dog images (auto-translated) | + +#### How It Works + +1. Your query is converted to a text embedding using SigLIP2 +2. The embedding is compared against all indexed image embeddings +3. Results are ranked by cosine similarity (closest matches first) +4. Results are hydrated with full asset metadata for display + +#### Tips + +- Be descriptive: "sunset beach with palm trees" works better than "beach" +- Use color words: "green forest", "blue ocean", "red sunset" +- Include style keywords: "anime", "photorealistic", "cyberpunk" +- Semantic search works across languages (FR โ†’ EN auto-translation) + +--- + +### 2. Find Similar + +Discover assets visually similar to a selected asset. + +#### How to Use + +1. **Select an asset** in the grid view (click once) +2. **Right-click** โ†’ **Find Similar** (or click the Clone icon) +3. View visually similar assets ranked by similarity score +4. Adjust `top_k` parameter to show more/fewer results (default: 20) + +#### Use Cases + +- **Variation Discovery**: Find successful variations of a generation +- **Style Exploration**: Discover assets with similar aesthetic +- **Quality Comparison**: Compare different attempts at same prompt +- **Theme Grouping**: Find all assets matching a visual theme + +#### Similarity Score + +Results include a similarity score (0.0 to 1.0): +- **0.85+**: Very similar (near-duplicates or variations) +- **0.70-0.85**: Highly similar (same theme/style) +- **0.55-0.70**: Moderately similar (related concepts) +- **<0.55**: Loosely related (shared elements only) + +#### Technical Details + +- Reference asset is excluded from results +- Uses cosine similarity in embedding space +- Searches across all scopes (Outputs, Inputs, Custom) +- Respects current scope filter if applied + +--- + +### 3. AI Auto-Tags + +Automatically suggested tags based on image content analysis. + +#### How It Works + +1. Image embedding is compared against predefined tag vocabulary +2. Tags with similarity above threshold are suggested +3. Auto-tags are stored separately from user tags +4. Tags can be viewed and applied to assets + +#### Available Auto-Tags + +The system includes 20+ canonical tags: + +| Category | Tags | +|----------|------| +| **Subject** | portrait, landscape, character, food, vehicle, nature, architecture | +| **Style** | cyberpunk, anime, photorealistic, abstract, fantasy, sci-fi, horror | +| **Medium** | watercolor, pixel-art, 3d-render, sketch, black-and-white | +| **Content** | nsfw (adult content detection) | + +#### How to View Auto-Tags + +1. Open an asset in the **Viewer** +2. Look at the **Sidebar** โ†’ **AI Tags** section +3. See suggested tags with confidence scores + +#### How to Apply Auto-Tags + +1. In Viewer sidebar, click **Apply All** to add all AI tags +2. Or click individual tags to add selectively +3. Tags are merged with your existing tags + +#### Configuration + +Adjust sensitivity via environment variable: + +```bash +# Lower threshold = more tags (less precise) +# Higher threshold = fewer tags (more precise) +MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.06 # Default +``` + +--- + +### 4. Enhanced Captions (Florence-2) + +Generate detailed AI-powered image descriptions. + +#### How to Generate + +1. Open an asset in the **Viewer** +2. In the sidebar, find **Image Description** section +3. Click **Generate** button +4. Wait for caption generation (5-30 seconds) +5. Generated caption appears in sidebar + +#### Caption Quality + +Florence-2 generates detailed, descriptive captions: + +**Example Input**: Anime-style image of a girl with blue hair + +**Generated Caption**: +> "An anime-style illustration of a young woman with long flowing blue hair and large expressive eyes. She is wearing a school uniform with a white shirt and blue skirt. The background shows a soft gradient of pink and purple, suggesting a sunset or dawn setting. The art style is typical of modern anime with clean lines and vibrant colors." + +#### Use Cases + +- **Accessibility**: Describe images for visually impaired users +- **Search Enhancement**: Captions are indexed for semantic search +- **Documentation**: Auto-generate descriptions for asset catalogs +- **Prompt Analysis**: Compare generated caption with original prompt + +#### Batch Generation + +For multiple assets, use the API: + +```bash +POST /mjr/am/vector/caption/{asset_id} +``` + +Or use the "Backfill vectors" feature which generates captions for all images. + +#### Storage + +- Captions stored in `asset_metadata.enhanced_caption` field +- Persisted in SQLite database +- Survives index resets (stored separately from vectors) + +--- + +### 5. Prompt Alignment Score + +Measure how well an image matches its generation prompt. + +#### What It Measures + +The alignment score quantifies the semantic similarity between: +- The original generation prompt (from metadata) +- The visual content of the image (from embedding) + +#### Score Interpretation + +| Score Range | Interpretation | +|-------------|----------------| +| **0.80-1.00** | Excellent alignment (image matches prompt very well) | +| **0.65-0.80** | Good alignment (minor deviations) | +| **0.50-0.65** | Moderate alignment (noticeable differences) | +| **0.30-0.50** | Poor alignment (significant prompt drift) | +| **<0.30** | Very poor alignment (image doesn't match prompt) | + +#### How It's Calculated + +The score uses **multi-signal fusion**: + +1. **Multi-segment imageโ†”text score** (60% weight) + - Prompt split into segments + - Each segment scored against image + - Length-weighted average with best-segment bonus + +2. **Captionโ†”prompt text similarity** (20% weight) + - Florence-2 caption compared to prompt + - Text-to-text coherence measure + +3. **Semantic dimension score** (20% weight) + - Subject, style, medium, mood, color dimensions + - Each dimension scored separately + +**Negative Prompt Penalty**: If negative prompt exists, its similarity to image reduces the score (penalizes unwanted content leakage). + +#### How to View + +1. Open asset in **Viewer** +2. Look for **Prompt Alignment** section in sidebar +3. Score displayed as percentage (0-100%) + +#### Use Cases + +- **Quality Control**: Identify generations that didn't match intent +- **Prompt Engineering**: Refine prompts based on alignment feedback +- **Curation**: Filter library by alignment quality +- **Model Comparison**: Compare different models' prompt adherence + +--- + +### 6. Smart Collections + +Automatically create collections based on AI suggestions. + +#### How to Use + +1. Open **Collections** panel +2. Click **Smart Suggestions** button +3. Review AI-suggested collections +4. Click **Create** to accept a suggestion + +#### Suggestion Types + +The AI analyzes your library and suggests: + +- **Theme-based**: "Cyberpunk Collection", "Nature Scenes" +- **Style-based**: "Anime Collection", "Photorealistic" +- **Color-based**: "Blue Tones", "Warm Colors" +- **Subject-based**: "Portraits", "Landscapes", "Vehicles" + +#### How Suggestions Work + +1. Asset embeddings are clustered using k-means +2. Each cluster is analyzed for common themes +3. Representative tags and descriptions generated +4. Suggestions presented for approval + +#### Configuration + +Adjust number of suggestions: + +```json +{ + "k": 8 // Number of clusters (2-20) +} +``` + +More clusters = more specific collections +Fewer clusters = broader collections + +--- + +### 7. Discover Groups + +Cluster your entire library by visual similarity. + +#### How to Use + +1. Open **Collections** panel +2. Click **Discover Groups** button +3. Wait for clustering to complete +4. Browse discovered groups +5. Convert groups to collections as desired + +#### Group Characteristics + +Each group includes: +- **Preview**: Sample images from the group +- **Size**: Number of assets in group +- **Tags**: Common AI-suggested tags +- **Representative Query**: Natural language description + +#### Use Cases + +- **Library Exploration**: Discover patterns in your assets +- **Cleanup**: Identify duplicate or near-duplicate generations +- **Curation**: Find themed subsets for projects +- **Analysis**: Understand your generation habits + +#### Performance + +Clustering time depends on library size: + +| Assets | Time (approx.) | +|--------|----------------| +| 100-500 | 5-15 seconds | +| 500-2000 | 15-60 seconds | +| 2000-10000 | 1-5 minutes | +| 10000+ | 5-15 minutes | + +--- + +## How It Works + +### Architecture Overview + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Assets Manager UI โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Semantic โ”‚ โ”‚ Find โ”‚ โ”‚ Auto โ”‚ โ”‚ Smart โ”‚ โ”‚ +โ”‚ โ”‚ Search โ”‚ โ”‚ Similar โ”‚ โ”‚ Tags โ”‚ โ”‚Collectionsโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + HTTP API + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Majoor Backend (Python) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Vector Service Layer โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ SigLIP2 โ”‚ โ”‚ X-CLIP โ”‚ โ”‚Florence-2โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (Image) โ”‚ โ”‚ (Video) โ”‚ โ”‚ (Caption)โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Faiss Index (In-Memory) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ SQLite (asset_embeddings table) โ”‚ โ”‚ +โ”‚ โ”‚ - asset_id โ”‚ โ”‚ +โ”‚ โ”‚ - vector (BLOB) โ”‚ โ”‚ +โ”‚ โ”‚ - aesthetic_score โ”‚ โ”‚ +โ”‚ โ”‚ - auto_tags (JSON) โ”‚ โ”‚ +โ”‚ โ”‚ - enhanced_caption โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Embedding Pipeline + +``` +1. Asset Discovery + โ†“ +2. Metadata Extraction (ExifTool, FFprobe) + โ†“ +3. Vector Embedding Computation + โ”œโ”€ Image โ†’ SigLIP2 image encoder + โ”œโ”€ Video โ†’ X-CLIP video encoder (keyframes) + โ””โ”€ Text โ†’ SigLIP2 text encoder + โ†“ +4. Embedding Storage + โ””โ”€ SQLite: asset_embeddings table + โ†“ +5. Index Building + โ””โ”€ Faiss IndexFlatIP (cosine similarity) + โ†“ +6. Query Processing + โ”œโ”€ Text query โ†’ text embedding + โ”œโ”€ Image query โ†’ image embedding + โ””โ”€ Similarity search โ†’ ranked results +``` + +### Vector Storage Schema + +```sql +CREATE TABLE asset_embeddings ( + asset_id INTEGER PRIMARY KEY, + vector BLOB NOT NULL, -- Packed float32 array + model_name TEXT NOT NULL, -- e.g., "google/siglip-so400m-patch14-384" + aesthetic_score REAL, -- Prompt alignment score (0-1) + auto_tags TEXT, -- JSON array of tag strings + enhanced_caption TEXT, -- Florence-2 generated caption + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## Configuration & Tuning + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `MJR_AM_ENABLE_VECTOR_SEARCH` | `1` | Enable/disable AI features | +| `MJR_AM_VECTOR_MODEL` | `google/siglip-so400m-patch14-384` | Image/text model | +| `MJR_AM_VECTOR_VIDEO_MODEL` | `microsoft/xclip-base-patch32` | Video model | +| `MJR_AM_PROMPT_MODEL` | `microsoft/Florence-2-base` | Caption model | +| `MJR_AM_VECTOR_DIM` | `1152` | Embedding dimension | +| `MJR_AM_VECTOR_AUTOTAG_THRESHOLD` | `0.06` | Auto-tag sensitivity | +| `MJR_AM_VECTOR_SIMILAR_TOPK` | `20` | Default similar results | +| `MJR_AM_VECTOR_KEYFRAME_INTERVAL` | `5.0` | Video keyframe interval (sec) | +| `MJR_AM_VECTOR_BATCH_SIZE` | `32` | Embedding batch size | +| `MJR_AM_AI_VERBOSE_LOGS` | `0` | Verbose AI logging | + +### Model Selection + +#### Image/Text Model Options + +| Model | Dimensions | Speed | Quality | Use Case | +|-------|------------|-------|---------|----------| +| `google/siglip-so400m-patch14-384` | 1152 | Medium | High | Default, balanced | +| `google/siglip-base-patch16-224` | 768 | Fast | Medium | Lower RAM systems | +| `google/siglip-large-patch16-384` | 1024 | Slow | Very High | Quality-focused | + +#### Video Model Options + +| Model | Dimensions | Speed | Quality | +|-------|------------|-------|---------| +| `microsoft/xclip-base-patch32` | 768 | Medium | Good | +| `microsoft/xclip-large-patch14` | 1024 | Slow | Better | + +#### Caption Model Options + +| Model | Size | Speed | Quality | +|-------|------|-------|---------| +| `microsoft/Florence-2-base` | ~230M | Fast | Good | +| `microsoft/Florence-2-large` | ~580M | Medium | Better | + +### Tuning Examples + +#### Faster Embedding (Lower Quality) + +```bash +export MJR_AM_VECTOR_MODEL="google/siglip-base-patch16-224" +export MJR_AM_VECTOR_BATCH_SIZE=64 +export MJR_AM_VECTOR_DIM=768 +``` + +#### Higher Quality (Slower) + +```bash +export MJR_AM_VECTOR_MODEL="google/siglip-large-patch16-384" +export MJR_AM_PROMPT_MODEL="microsoft/Florence-2-large" +export MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.30 +``` + +#### Low-RAM System + +```bash +export MJR_AM_VECTOR_BATCH_SIZE=8 +export MJR_AM_VECTOR_MODEL="google/siglip-base-patch16-224" +``` + +--- + +## Performance Considerations + +### Resource Requirements + +| Component | Minimum | Recommended | Optimal | +|-----------|---------|-------------|---------| +| **RAM** | 8 GB | 16 GB | 32 GB | +| **VRAM** | 0 GB (CPU) | 4 GB | 8+ GB | +| **Storage** | 5 GB free | 10 GB free | 20+ GB free | +| **CPU** | 4 cores | 8 cores | 12+ cores | + +### Model Loading Times + +| Model | Cold Load | Cached | +|-------|-----------|--------| +| SigLIP2 SO400M | 30-60s | 5-10s | +| X-CLIP Base | 20-40s | 3-8s | +| Florence-2 Base | 15-30s | 2-5s | + +### Embedding Speed + +| Asset Type | CPU Only | GPU (RTX 3060) | +|------------|----------|----------------| +| Image (1080p) | 2-5 sec | 0.5-1 sec | +| Video (1 min) | 10-30 sec | 3-10 sec | +| Text caption | 0.5-2 sec | 0.1-0.5 sec | + +### Backfill Performance + +**Backfill vectors** computes embeddings for all assets without vectors: + +| Library Size | Time (CPU) | Time (GPU) | +|--------------|------------|------------| +| 100 assets | 2-5 min | 30-60 sec | +| 500 assets | 10-25 min | 2-5 min | +| 1000 assets | 20-50 min | 5-10 min | +| 5000 assets | 2-4 hours | 25-50 min | + +### Optimization Tips + +1. **Enable GPU acceleration** (CUDA): + ```bash + pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 + ``` + +2. **Adjust batch size** for your hardware: + - Low RAM: `MJR_AM_VECTOR_BATCH_SIZE=8` + - High RAM: `MJR_AM_VECTOR_BATCH_SIZE=64` + +3. **Use CPU-only mode** if GPU memory is limited: + ```bash + export CUDA_VISIBLE_DEVICES="" + ``` + +4. **Limit index size** for large libraries: + - Vector searcher caps at 100,000 assets + - Most recent assets prioritized + +5. **Schedule backfill** during off-hours: + - Run overnight for large libraries + - System remains usable during backfill + +--- + +## Troubleshooting + +### Common Issues + +#### 1. AI Features Not Appearing + +**Symptoms**: No sparkles icon, no "Find Similar" button + +**Solutions**: +- Check if AI features are enabled in Settings +- Verify `MJR_AM_ENABLE_VECTOR_SEARCH=1` in environment +- Restart ComfyUI after enabling +- Check console for model loading errors + +#### 2. Model Download Fails + +**Symptoms**: Error downloading models, timeout + +**Solutions**: +```bash +# Check internet connectivity +ping huggingface.co + +# Manual model download +huggingface-cli download google/siglip-so400m-patch14-384 + +# Set HF mirror (for China/regions with restrictions) +export HF_ENDPOINT=https://hf-mirror.com +``` + +#### 3. Out of Memory (OOM) + +**Symptoms**: System freezes, crashes during embedding + +**Solutions**: +- Reduce batch size: `MJR_AM_VECTOR_BATCH_SIZE=8` +- Close other applications +- Use CPU-only mode to free GPU RAM +- Restart ComfyUI to clear memory + +#### 4. Slow Performance + +**Symptoms**: Searches take >5 seconds, UI lag + +**Solutions**: +- Reduce library size (split into multiple scopes) +- Use GPU acceleration if available +- Increase system RAM +- Disable verbose logging: `MJR_AM_AI_VERBOSE_LOGS=0` + +#### 5. Poor Search Results + +**Symptoms**: Irrelevant results for queries + +**Solutions**: +- Ensure vectors are backfilled: Index Status โ†’ Backfill vectors +- Use more descriptive queries +- Check model loaded correctly in console +- Verify embedding dimension matches model + +#### 6. Caption Generation Fails + +**Symptoms**: "Generate" button shows error, no caption + +**Solutions**: +- Check Florence-2 model loaded (console logs) +- Verify asset is an image (not video) +- Increase timeout: `MJR_AM_DB_TIMEOUT=120` +- Check disk space for model cache + +### Diagnostic Commands + +```bash +# Check model cache +ls -la ~/.cache/huggingface/hub/ + +# Verify Python packages +pip list | grep -E "sentence-transformers|transformers|faiss" + +# Test model loading +python -c "from sentence_transformers import SentenceTransformer; m = SentenceTransformer('google/siglip-so400m-patch14-384'); print('OK')" + +# Check vector index status +curl http://localhost:8188/mjr/am/vector/stats +``` + +### Log Analysis + +Enable verbose logging for debugging: + +```bash +export MJR_AM_AI_VERBOSE_LOGS=1 +``` + +Look for these log patterns: + +``` +# Successful model load +INFO: Loading multimodal embedding model 'google/siglip-so400m-patch14-384' โ€ฆ +INFO: SigLIP2 model loaded and ready: 'google/siglip-so400m-patch14-384' (dim=1152) + +# Successful embedding +DEBUG: Vector embedding computed for asset 12345 + +# Search query +INFO: Semantic search: 'sunset mountains' โ†’ 20 results +``` + +--- + +## API Reference + +### Semantic Search + +```http +GET /mjr/am/vector/search?q={query}&top_k={count}&scope={scope} +``` + +**Parameters**: +- `q` (required): Natural language query +- `top_k` (optional): Max results (default: 20, max: 200) +- `scope` (optional): `output`, `input`, `custom`, `all` + +**Example**: +```bash +curl "http://localhost:8188/mjr/am/vector/search?q=sunset%20beach&top_k=10" +``` + +**Response**: +```json +{ + "ok": true, + "data": [ + { + "id": 12345, + "asset_id": 12345, + "filepath": "/path/to/image.png", + "filename": "image.png", + "kind": "image", + "_vectorScore": 0.8923 + } + ] +} +``` + +### Find Similar + +```http +GET /mjr/am/vector/similar/{asset_id}?top_k={count} +``` + +**Parameters**: +- `asset_id` (required): Reference asset ID +- `top_k` (optional): Max results (default: 20) + +**Example**: +```bash +curl "http://localhost:8188/mjr/am/vector/similar/12345?top_k=10" +``` + +### Prompt Alignment + +```http +GET /mjr/am/vector/alignment/{asset_id} +``` + +**Response**: +```json +{ + "ok": true, + "data": 0.7523 +} +``` + +### Generate Caption + +```http +POST /mjr/am/vector/caption/{asset_id} +``` + +**Response**: +```json +{ + "ok": true, + "data": "An anime-style illustration of..." +} +``` + +### Auto-Tags + +```http +GET /mjr/am/vector/auto-tags/{asset_id} +``` + +**Response**: +```json +{ + "ok": true, + "data": ["anime", "portrait", "fantasy"] +} +``` + +### Suggest Collections + +```http +POST /mjr/am/vector/suggest-collections +Content-Type: application/json + +{"k": 8} +``` + +**Response**: +```json +{ + "ok": true, + "data": { + "suggestions": [ + { + "name": "Cyberpunk Collection", + "query": "cyberpunk neon city night", + "estimated_size": 45, + "tags": ["cyberpunk", "sci-fi", "night"] + } + ] + } +} +``` + +### Vector Stats + +```http +GET /mjr/am/vector/stats +``` + +**Response**: +```json +{ + "ok": true, + "data": { + "total": 1234, + "avg_score": 0.6523, + "dim": 1152, + "enabled": true + } +} +``` + +### Re-index Asset + +```http +POST /mjr/am/vector/index/{asset_id} +``` + +Force re-computation of embedding for a single asset. + +--- + +## Privacy & Security + +### Data Privacy + +- **Local Processing**: All AI inference runs locally on your machine +- **No Cloud Upload**: Images and prompts never leave your computer +- **Local Cache**: Models cached in `~/.cache/huggingface/hub/` +- **No Telemetry**: No usage data sent to developers + +### Network Access + +Initial model download requires internet access: + +- Downloads from HuggingFace CDN +- One-time download per model +- Subsequent uses work offline + +### Security Considerations + +1. **Model Integrity**: Models downloaded from official HuggingFace repos +2. **Code Execution**: No arbitrary code execution from models +3. **Sandboxing**: Models run in same process as ComfyUI +4. **Resource Limits**: Built-in rate limiting prevents abuse + +### Environment Isolation + +For enhanced security, run in isolated environment: + +```bash +# Create virtual environment +python -m venv venv +source venv/bin/activate # Linux/macOS +venv\Scripts\activate # Windows + +# Install dependencies +pip install -r requirements.txt +``` + +--- + +## Appendix: Model Cards + +### SigLIP2 SO400M + +- **Publisher**: Google +- **Architecture**: Vision-Language Transformer +- **Training**: Contrastive image-text pairs +- **License**: Apache 2.0 +- **HuggingFace**: https://huggingface.co/google/siglip-so400m-patch14-384 + +### X-CLIP Base + +- **Publisher**: Microsoft +- **Architecture**: Video-Language Transformer +- **Training**: Video-text pairs +- **License**: MIT +- **HuggingFace**: https://huggingface.co/microsoft/xclip-base-patch32 + +### Florence-2 Base + +- **Publisher**: Microsoft +- **Architecture**: Vision-Language Model +- **Training**: Image-caption pairs +- **License**: MIT +- **HuggingFace**: https://huggingface.co/microsoft/Florence-2-base + +--- + +## Changelog + +### v2.4.0 (March 2026) +- Added Florence-2 caption generation +- Improved prompt alignment scoring with multi-signal fusion +- Added French-to-English query translation +- Enhanced auto-tag vocabulary (20+ tags) +- Smart Collections with k-means clustering + +### v2.3.0 (February 2026) +- Initial AI features release +- Semantic search with SigLIP2 +- Find Similar functionality +- Basic auto-tagging + +--- + +## Support & Resources + +### Documentation +- [Main Documentation Index](DOCUMENTATION_INDEX.md) +- [API Reference](API_REFERENCE.md) +- [Configuration Guide](SETTINGS_CONFIGURATION.md) + +### Community +- [GitHub Issues](https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/issues) +- [GitHub Discussions](https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/discussions) + +### Model Documentation +- [Sentence Transformers Docs](https://sbert.net/) +- [HuggingFace Transformers](https://huggingface.co/docs/transformers) +- [Faiss Documentation](https://faiss.ai/) + +--- + +*This documentation is part of the Majoor Assets Manager project.* +*Copyright ยฉ 2026 Ewald ALOEBOETOE (MajoorWaldi)* diff --git a/docs/DB_MAINTENANCE.md b/docs/DB_MAINTENANCE.md index ae29d8e6..9fd70056 100644 --- a/docs/DB_MAINTENANCE.md +++ b/docs/DB_MAINTENANCE.md @@ -42,9 +42,7 @@ The **Delete DB** button calls `POST /mjr/am/db/force-delete`. It is designed to ### Confirmation dialog -Before proceeding, a native browser `confirm()` dialog warns: - -> This will permanently delete the index database and rebuild it from scratch. All ratings, tags, and cached metadata will be lost. +Before proceeding, the UI first asks whether existing AI vectors should be kept, then shows a simple confirmation dialog in the ComfyUI Manager style for the selected action. ### What is lost diff --git a/docs/DOCUMENTATION_INDEX.md b/docs/DOCUMENTATION_INDEX.md index 78bb2e4b..070d2675 100644 --- a/docs/DOCUMENTATION_INDEX.md +++ b/docs/DOCUMENTATION_INDEX.md @@ -2,8 +2,8 @@ Welcome to the comprehensive documentation for the **Majoor Assets Manager** for ComfyUI. This collection of guides covers all features, from basic usage to advanced configuration and development. -**Current Version**: 2.3.3 -**Last Updated**: February 28, 2026 +**Current Version**: 2.4.0 +**Last Updated**: March 5, 2026 --- @@ -15,7 +15,8 @@ Welcome to the comprehensive documentation for the **Majoor Assets Manager** for 3. **[Hotkeys & Shortcuts](HOTKEYS_SHORTCUTS.md)** โ€” Essential keyboard shortcuts 4. **[Basic Search](SEARCH_FILTERING.md)** โ€” Find your assets quickly -### Returning Users (v2.3.3) +### Returning Users (v2.4.0) +- **[AI Features Guide](AI_FEATURES.md)** โ€” NEW comprehensive AI capabilities - **[Majoor Floating Viewer](VIEWER_FEATURE_TUTORIAL.md#majoor-floating-viewer-mfv)** โ€” NEW real-time preview panel - **[Changelog](../CHANGELOG.md)** โ€” What's new in this version - **[Linux Support](INSTALLATION.md#linux-installation)** โ€” Now fully supported on Linux @@ -39,6 +40,7 @@ Welcome to the comprehensive documentation for the **Majoor Assets Manager** for | **[VIEWER_FEATURE_TUTORIAL.md](VIEWER_FEATURE_TUTORIAL.md)** | Advanced viewer, MFV, and analysis tools | | **[RATINGS_TAGS_COLLECTIONS.md](RATINGS_TAGS_COLLECTIONS.md)** | Organization with ratings, tags, and collections | | **[DRAG_DROP.md](DRAG_DROP.md)** | Drag & drop to canvas and OS | +| **[AI_FEATURES.md](AI_FEATURES.md)** | ๐Ÿ†• AI-powered semantic search, auto-tags, and more | ### Configuration & Settings | Document | Description | @@ -103,6 +105,18 @@ Welcome to the comprehensive documentation for the **Majoor Assets Manager** for - Node compatibility and staging - File transfer mechanisms +### AI Features โ€” NEW! ๐ŸŽ‰ +**[AI_FEATURES.md](AI_FEATURES.md)** โ€” Complete guide to AI-powered capabilities: +- **Semantic Search**: Natural language search across your library +- **Find Similar**: Discover visually similar assets +- **AI Auto-Tags**: Automatic tag suggestions (portrait, landscape, cyberpunk, etc.) +- **Enhanced Captions**: Florence-2 generated image descriptions +- **Prompt Alignment Score**: Measure image-prompt matching quality +- **Smart Collections**: Auto-group assets by visual similarity +- **Discover Groups**: Cluster library by visual themes +- Models: SigLIP2 (image/text), X-CLIP (video), Florence-2 (caption) +- Configuration, tuning, and troubleshooting + --- ## ๐Ÿ”ง Configuration Guides diff --git a/js/api/client.js b/js/api/client.js index 3e53de1a..2bbce067 100644 --- a/js/api/client.js +++ b/js/api/client.js @@ -882,6 +882,13 @@ export async function resetIndex(options = {}) { clear_metadata_cache: _bool(options.clearMetadataCache ?? options.clear_metadata_cache, true), clear_asset_metadata: _bool(options.clearAssetMetadata ?? options.clear_asset_metadata, true), clear_assets: _bool(options.clearAssets ?? options.clear_assets, true), + preserve_vectors: _bool( + options.preserveVectors ?? + options.preserve_vectors ?? + options.keepVectors ?? + options.keep_vectors, + false + ), rebuild_fts: _bool(options.rebuildFts ?? options.rebuild_fts, true), incremental: _bool(options.incremental, false), fast: _bool(options.fast, true), @@ -1099,7 +1106,8 @@ export async function vectorSearch(query, topKOrOptions = 20) { let url = `${ENDPOINTS.VECTOR_SEARCH}?q=${encodeURIComponent(q)}&top_k=${topK}`; if (scope) url += `&scope=${encodeURIComponent(scope)}`; if (customRootId) url += `&custom_root_id=${encodeURIComponent(customRootId)}`; - return get(url); + // Model cold-start (SigLIP download/load) can take 60-120s on first use + return get(url, { timeoutMs: 120_000 }); } /** @@ -1310,7 +1318,8 @@ export async function hybridSearch(query, { topK = 50, scope = "output", customR if (!q) return { ok: false, error: "Empty query" }; let url = `${ENDPOINTS.HYBRID_SEARCH}?q=${encodeURIComponent(q)}&top_k=${Math.max(1, Math.min(200, topK))}&scope=${encodeURIComponent(scope)}`; if (customRootId) url += `&custom_root_id=${encodeURIComponent(customRootId)}`; - return get(url); + // Model cold-start (SigLIP download/load) can take 60-120s on first use + return get(url, { timeoutMs: 120_000 }); } /** diff --git a/js/app/dialogs.js b/js/app/dialogs.js index 60a29a23..e898fd0e 100644 --- a/js/app/dialogs.js +++ b/js/app/dialogs.js @@ -5,6 +5,9 @@ * alert/confirm/prompt popups. Falls back to window.* when unavailable. */ +import { getComfyApp } from "./comfyApiBridge.js"; +import { t } from "./i18n.js"; + const getComfyUi = () => { try { const app = getComfyApp(); @@ -232,7 +235,7 @@ export const comfyAlert = async (message, title = "Majoor", options = {}) => { style: { display: "flex", justifyContent: "flex-end", gap: "10px" }, }, [ $el("button", { - textContent: "Confirm", + textContent: t("dialog.confirm", "Confirm"), onclick: close, style: { padding: "10px 16px", @@ -264,7 +267,7 @@ export const comfyConfirm = async (message, title = "Majoor") => { if (extensionDialog) { try { const result = await extensionDialog.confirm({ - title: String(title || "Confirm"), + title: String(title || t("dialog.confirm", "Confirm")), message: String(message || ""), }); return !!result; @@ -329,7 +332,7 @@ export const comfyConfirm = async (message, title = "Majoor") => { style: { display: "flex", justifyContent: "flex-end", gap: "10px" }, }, [ $el("button", { - textContent: "Cancel", + textContent: t("dialog.cancel", "Cancel"), onclick: () => finish(false), style: { padding: "10px 16px", @@ -342,7 +345,7 @@ export const comfyConfirm = async (message, title = "Majoor") => { }, }), $el("button", { - textContent: "Confirm", + textContent: t("dialog.confirm", "Confirm"), onclick: () => finish(true), style: { padding: "10px 16px", @@ -370,12 +373,273 @@ export const comfyConfirm = async (message, title = "Majoor") => { }); }; +export const comfyYesNoCancel = async (message, title = "Majoor", labels = {}) => { + const yesLabel = String(labels?.yes || t("dialog.yes", "Yes")); + const noLabel = String(labels?.no || t("dialog.no", "No")); + const cancelLabel = String(labels?.cancel || t("dialog.cancel", "Cancel")); + + const Dialog = getComfyDialogCtor(); + if (!Dialog) { + try { + const answer = window.prompt( + `${String(title || "Majoor")}\n\n${String(message || "")}\n\n${yesLabel}=y, ${noLabel}=n, ${cancelLabel}=c`, + "c" + ); + const normalized = String(answer || "").trim().toLowerCase(); + if (!normalized) return "cancel"; + if (["y", "yes", "1"].includes(normalized)) return "yes"; + if (["n", "no", "2"].includes(normalized)) return "no"; + return "cancel"; + } catch { + return "cancel"; + } + } + + return new Promise((resolve) => { + const dialog = new Dialog(); + styleDialog(dialog); + + const finish = (value) => { + try { + dialog.close(); + } catch (e) { console.debug?.(e); } + resolve(value || "cancel"); + }; + + const content = $el("div", { + style: { + display: "flex", + flexDirection: "column", + gap: "18px", + padding: "18px 20px 18px 20px", + }, + onkeydown: (e) => { + if (e.key === "Escape") { + e.preventDefault(); + finish("cancel"); + } + }, + }, [ + $el("div", { + style: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + }, + }, [ + $el("div", { + textContent: title, + style: { + fontWeight: "700", + fontSize: "30px", + color: "rgba(255,255,255,0.96)", + lineHeight: "1.2", + }, + }), + ]), + $el("div", { + textContent: String(message || ""), + style: { + fontSize: "22px", + color: "rgba(255,255,255,0.86)", + whiteSpace: "pre-wrap", + lineHeight: "1.45", + }, + }), + $el("div", { + style: { display: "flex", justifyContent: "flex-end", gap: "10px", flexWrap: "wrap" }, + }, [ + $el("button", { + textContent: yesLabel, + onclick: () => finish("yes"), + style: { + padding: "10px 16px", + borderRadius: "10px", + border: "1px solid rgba(17,132,255,0.75)", + background: "#1184ff", + color: "rgba(255,255,255,0.98)", + fontWeight: "600", + cursor: "pointer", + }, + }), + $el("button", { + textContent: noLabel, + onclick: () => finish("no"), + style: { + padding: "10px 16px", + borderRadius: "10px", + border: "1px solid rgba(244,67,54,0.70)", + background: "rgba(244,67,54,0.18)", + color: "rgba(255,220,220,0.98)", + fontWeight: "600", + cursor: "pointer", + }, + }), + $el("button", { + textContent: cancelLabel, + onclick: () => finish("cancel"), + style: { + padding: "10px 16px", + borderRadius: "10px", + border: "1px solid rgba(255,255,255,0.18)", + background: "rgba(255,255,255,0.06)", + color: "rgba(255,255,255,0.85)", + fontWeight: "600", + cursor: "pointer", + }, + }), + ]), + ]); + + try { + dialog.show(content); + setTimeout(() => hideDialogNativeClose(dialog), 0); + } catch { + resolve("cancel"); + } + }); +}; + +export const comfyChoice = async (message, title = "Majoor", choices = []) => { + const safeChoices = Array.isArray(choices) + ? choices + .filter((c) => c && typeof c === "object") + .map((c, idx) => ({ + id: String(c.id || `choice_${idx}`), + label: String(c.label || c.id || `Choice ${idx + 1}`), + variant: String(c.variant || "").toLowerCase(), + })) + : []; + + if (!safeChoices.length) return null; + + const Dialog = getComfyDialogCtor(); + if (!Dialog) { + try { + const numbered = safeChoices + .map((c, idx) => `${idx + 1}. ${c.label}`) + .join("\n"); + const answer = window.prompt( + `${String(title || "Majoor")}\n\n${String(message || "")}\n\n${numbered}\n\n${t("dialog.choiceTypeNumber", "Type a number:")}`, + String(safeChoices.length) + ); + if (answer == null) return null; + const index = Number.parseInt(String(answer).trim(), 10) - 1; + if (Number.isInteger(index) && index >= 0 && index < safeChoices.length) { + return safeChoices[index].id; + } + return null; + } catch { + return null; + } + } + + return new Promise((resolve) => { + const dialog = new Dialog(); + styleDialog(dialog); + + const finish = (value) => { + try { + dialog.close(); + } catch (e) { console.debug?.(e); } + resolve(value ?? null); + }; + + const buttonForChoice = (choice) => { + const isDanger = choice.variant === "danger"; + const isPrimary = choice.variant === "primary"; + return $el("button", { + textContent: choice.label, + onclick: () => finish(choice.id), + style: { + padding: "10px 14px", + borderRadius: "10px", + border: isDanger + ? "1px solid rgba(244,67,54,0.70)" + : isPrimary + ? "1px solid rgba(17,132,255,0.75)" + : "1px solid rgba(255,255,255,0.18)", + background: isDanger + ? "rgba(244,67,54,0.18)" + : isPrimary + ? "#1184ff" + : "rgba(255,255,255,0.06)", + color: isDanger + ? "rgba(255,220,220,0.98)" + : isPrimary + ? "rgba(255,255,255,0.98)" + : "rgba(255,255,255,0.85)", + fontWeight: "600", + cursor: "pointer", + }, + }); + }; + + const content = $el("div", { + style: { + display: "flex", + flexDirection: "column", + gap: "18px", + padding: "18px 20px 18px 20px", + }, + onkeydown: (e) => { + if (e.key === "Escape") { + e.preventDefault(); + finish(null); + } + }, + }, [ + $el("div", { + style: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + }, + }, [ + $el("div", { + textContent: title, + style: { + fontWeight: "700", + fontSize: "30px", + color: "rgba(255,255,255,0.96)", + lineHeight: "1.2", + }, + }), + ]), + $el("div", { + textContent: String(message || ""), + style: { + fontSize: "22px", + color: "rgba(255,255,255,0.86)", + whiteSpace: "pre-wrap", + lineHeight: "1.45", + }, + }), + $el("div", { + style: { + display: "flex", + justifyContent: "flex-end", + gap: "10px", + flexWrap: "wrap", + }, + }, safeChoices.map(buttonForChoice)), + ]); + + try { + dialog.show(content); + setTimeout(() => hideDialogNativeClose(dialog), 0); + } catch { + resolve(null); + } + }); +}; + export const comfyPrompt = async (message, defaultValue = "", title = "Majoor") => { const extensionDialog = getExtensionManagerDialog(); if (extensionDialog) { try { const result = await extensionDialog.prompt({ - title: String(title || "Prompt"), + title: String(title || t("dialog.prompt", "Prompt")), message: String(message || ""), defaultValue: String(defaultValue ?? ""), }); @@ -454,7 +718,7 @@ export const comfyPrompt = async (message, defaultValue = "", title = "Majoor") style: { display: "flex", justifyContent: "flex-end", gap: "10px" }, }, [ $el("button", { - textContent: "Cancel", + textContent: t("dialog.cancel", "Cancel"), onclick: () => finish(null), style: { padding: "8px 12px", @@ -466,7 +730,7 @@ export const comfyPrompt = async (message, defaultValue = "", title = "Majoor") }, }), $el("button", { - textContent: "OK", + textContent: t("dialog.ok", "OK"), onclick: () => finish(input.value), style: { padding: "8px 12px", @@ -498,4 +762,3 @@ export const comfyPrompt = async (message, defaultValue = "", title = "Majoor") } }); }; -import { getComfyApp } from "./comfyApiBridge.js"; diff --git a/js/app/i18n.js b/js/app/i18n.js index 440b949f..28147e32 100644 --- a/js/app/i18n.js +++ b/js/app/i18n.js @@ -198,6 +198,7 @@ const DICTIONARY = { "tab.input": "Input", "tab.all": "All", "tab.custom": "Custom", + "tab.similar": "Similar", "manager.title": "Assets Manager", // --- Panel: Buttons --- @@ -226,6 +227,7 @@ const DICTIONARY = { "btn.up": "Up", "btn.saving": "Saving...", "btn.restoring": "Restoring...", + "btn.markAllRead": "Mark all read", // --- Panel: Labels --- "label.folder": "Folder", @@ -252,14 +254,27 @@ const DICTIONARY = { "label.heightPx": "Height (px)", "label.day": "Day", "label.collections": "Collections", + "label.collection": "collection", + "label.messages": "Messages", + "label.readMe": "Read Me", + "label.userGuide": "User Guide", + "label.info": "Info", + "btn.giveStar": "Give a star", "label.filters": "Filters", "label.selectFolder": "Select folder?", "label.thisFolder": "this folder", + "label.thisFile": "this file", "label.computer": "Computer", "search.placeholder": "Search assets...", "search.title": "Search by filename, tags, or attributes (e.g. rating:5, ext:png)", "search.semanticToggle": "Toggle AI semantic search (CLIP-based)", "search.aiSearch": "AI Search", + "search.findSimilar": "Find Similar", + "search.findingSimilar": "Finding similar assets...", + "search.selectAssetForSimilar": "Select an asset first to find similar images/videos.", + "search.findSimilarFailed": "Failed to find similar assets", + "search.similarResults": "Similar to asset #{id} ({n} results)", + "search.similarReference": "Reference #{id}", "action.copyToClipboard": "Copy to clipboard", "action.clickToCopy": "Click to copy", "tooltip.copyFieldValue": "Copy value", @@ -274,6 +289,7 @@ const DICTIONARY = { "tooltip.tab.input": "Browse input folder assets", "tooltip.tab.output": "Browse generated outputs", "tooltip.tab.custom": "Browse browser folders", + "tooltip.tab.similar": "Browse current similar findings", "tooltip.browserFolders": "Browser folders", "tooltip.pinnedFolders": "Pinned folders", "tooltip.clearFilter": "Clear {label}", @@ -281,9 +297,12 @@ const DICTIONARY = { "tooltip.closeSidebar": "Close sidebar", "tooltip.closeSidebarEsc": "Close sidebar (Esc)", "tooltip.supportKofi": "Buy Me a White Monster Drink", + "tooltip.starGithub": "Open GitHub and give a star", "tooltip.sidebarTab": "Assets Manager - Browse and search your outputs", "tooltip.openMFV": "Open Floating Viewer", "tooltip.closeMFV": "Close Floating Viewer", + "tooltip.openMessages": "Messages and updates", + "tooltip.openMessagesUnread": "Messages ({count} unread)", "tooltip.deleteDb": "Force-delete database and rebuild from scratch", "tooltip.workflowMultiOutput": "Multiple outputs with different prompts", "tooltip.generationInputs": "Input files used in generation", @@ -424,6 +443,7 @@ const DICTIONARY = { "scope.output": "Outputs", "scope.custom": "Custom", "scope.customBrowser": "Browser", + "scope.similar": "Similar", // --- Tools --- "tool.exiftool": "ExifTool metadata", @@ -440,8 +460,24 @@ const DICTIONARY = { "msg.errorLoadingFolders": "Error loading folders", "msg.noGenerationData": "No generation data found for this file.", "msg.rawMetadata": "Raw metadata", + "msg.noMessages": "No messages for now.", "msg.noPinnedFolders": "No pinned folders", "msg.noTagsYet": "No tags yet...", + "msg.category.information": "Information", + "msg.whatsNew.title.pinReference": "What's New", + "msg.whatsNew.body.pinReference": "Floating Viewer: new Pin Reference feature. You can now pin A or B, then compare quickly with selected assets in the grid while keeping the fixed reference.", + "msg.whatsNew.title.vectorResetKeepVectors": "Important", + "msg.whatsNew.body.vectorResetKeepVectors": "Reset index and Delete DB now first ask whether to keep AI vectors. If you already have older indexed assets, keeping the vectors is recommended: a full reset without them can trigger a long Vector Backfill for old assets and temporarily increase RAM usage.", + "msg.whatsNew.title.localUserGuide": "Need help?", + "msg.whatsNew.body.localUserGuide": "Open the local User Guide directly from your Assets Manager custom_nodes folder.", + "msg.collectionAdd.added": "Added {added} item(s) to \"{name}\".", + "msg.collectionAdd.skippedExisting": "Skipped {count} item(s): already present in the collection.", + "msg.collectionAdd.skippedDuplicate": "Ignored {count} duplicate(s) in selection.", + "msg.collectionAdd.noneAddedExisting": "No new items added to \"{name}\" (all exist).", + "msg.aiFeaturesNotice": "Warning:\n\nAssets Manager now includes AI features like auto captions, AI search, smart collections, etc.\nThese features are still WIP.\n\nIf you already had Assets Manager installed, delete the DB first.\nWhen enrich is finished, run Vector Backfill for your existing assets.\nAfter clicking Backfill, you can go sleep: it may take a very, very long time if you have many assets.", + "msg.dbResetNoticeDetail": "Majoor Update Notice:\n\nTo avoid database errors with this new version, please delete your existing index. Click the 'Delete DB' button in the Index Status panel to reset it.", + "msg.nightlyUpdateTitle": "Majoor Assets Manager", + "msg.nightlyUpdateDetail": "A newer nightly build is available: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/releases/tag/nightly", "msg.newVersionTitle": "Majoor Assets Manager", "msg.newVersionDetail": "A new version is available: {latest} (Current: {current})", "msg.dbResetNotice": "Database reset required", @@ -530,11 +566,17 @@ const DICTIONARY = { "ctx.resetRating": "Reset rating", "ctx.showMetadataPanel": "Show metadata panel", "ctx.unpinFolder": "Unpin folder", + "ctx.openFolder": "Open folder", "ctx.pinAsBrowserRoot": "Pin as Browser Root", // --- Dialogs --- "dialog.confirm": "Confirm", "dialog.cancel": "Cancel", + "dialog.yes": "Yes", + "dialog.no": "No", + "dialog.ok": "OK", + "dialog.prompt": "Prompt", + "dialog.choiceTypeNumber": "Type a number:", "dialog.delete.title": "Delete file?", "dialog.delete.msg": "Are you sure you want to delete this file? This cannot be undone.", "dialog.rename.title": "Rename file", @@ -563,6 +605,20 @@ const DICTIONARY = { "dialog.deleteExactDuplicates": "Delete exact duplicates?", "dialog.startDuplicateAnalysis": "Start duplicate analysis?", "dialog.dbDelete.confirm": "This will permanently delete the index database and rebuild it from scratch. All ratings, tags, and cached metadata will be lost.\n\nContinue?", + "dialog.settingsSaveFailed": "Majoor: Failed to save settings (browser storage full or blocked).", + "dialog.confirmDeleteTitle": "Majoor: Confirm delete", + "dialog.deleteSelectedFiles": "Delete {count} selected files?", + "dialog.deleteSingleFile": "Delete \"{label}\"?", + "dialog.vectorsReset.title": "AI vectors", + "dialog.vectorsReset.choice": "Also reset AI vectors?\n\nConfirm = yes, reset everything (vectors will be recalculated)\nCancel = no, keep existing vectors", + "dialog.vectorsReset.keepQuestion": "Keep existing AI vectors?\n\nConfirm = keep vectors\nCancel = continue without vectors", + "dialog.vectorsReset.wipeConfirm": "Reset AI vectors too?\n\nConfirm = yes, reset everything\nCancel = abort", + "dialog.vectorsReset.singleQuestion": "Choose reset mode for {action}:\n\nYes = keep existing AI vectors\nNo = full reset (vectors will be recalculated)\nCancel = abort", + "dialog.vectorsReset.optionKeep": "Yes - keep vectors", + "dialog.vectorsReset.optionFull": "No - full reset", + "dialog.vectorsReset.optionCancel": "Cancel", + "dialog.resetIndex.confirmKeepVectors": "This will reset index data and rescan files while keeping existing AI vectors.\n\nContinue?", + "dialog.dbDelete.keepVectorsConfirm": "This will reset index data and keep existing AI vectors. Database files will not be force-deleted.\n\nContinue?", // --- Toasts --- "toast.scanStarted": "Scan started", @@ -600,6 +656,13 @@ const DICTIONARY = { "toast.noValidAssetsSelected": "No valid assets selected.", "toast.failedCreateCollectionDot": "Failed to create collection.", "toast.failedAddAssetsToCollection": "Failed to add assets to collection.", + "toast.failedCreateSmartCollection": "Failed to create smart collection", + "toast.failedAddAssetsToSmartCollection": "Failed to add assets to smart collection", + "toast.noGroupsFoundIndexFirst": "No groups found. Index more assets first.", + "toast.failedLoadClusterAssets": "Failed to load cluster assets", + "toast.collectionCreatedWithAssets": "Collection \"{name}\" created with {count} assets!", + "toast.collectionCreatedNamed": "Collection \"{name}\" created.", + "toast.clusterAnalysisFailed": "Cluster analysis failed", "toast.removeFromCollectionFailed": "Failed to remove from collection.", "toast.removeFromCollectionError": "Error removing from collection: {error}", "toast.copyClipboardFailed": "Failed to copy to clipboard", @@ -644,6 +707,14 @@ const DICTIONARY = { "toast.unableResolveFolderPath": "Unable to resolve folder path", "toast.pathCopyFailed": "Failed to copy path", "toast.noFilePath": "No file path available for this asset.", + "toast.vectorBackfillStarting": "Starting vector backfill... This may take a while.", + "toast.vectorBackfillRunning": "Vector backfill still running in background{job}.", + "toast.vectorBackfillComplete": "Vector backfill complete! Processed: {processed}, Indexed: {indexed}, Skipped: {skipped}", + "toast.vectorBackfillFailedGeneric": "Backfill failed", + "toast.vectorBackfillFailedDetail": "Vector backfill failed: {error}", + "toast.rescanUpdatingAiIndex": "Rescanning file + updating AI index...", + "toast.metadataVectorUpdated": "Metadata + AI vector index updated for this asset.", + "toast.metadataUpdatedVectorFailed": "Metadata updated. AI vector index could not be updated.", "toast.downloadingFile": "Downloading {filename}...", "toast.playbackRate": "Playback {rate}x", "toast.metadataRefreshed": "Metadata refreshed{suffix}", @@ -691,6 +762,7 @@ const DICTIONARY = { "tab.input": "Entree", "tab.all": "Tout", "tab.custom": "Navigateur", + "tab.similar": "Similaire", "manager.title": "Gestionnaire d'assets", "scope.all": "Entrees + Sorties", @@ -699,11 +771,21 @@ const DICTIONARY = { "scope.output": "Sorties", "scope.custom": "Navigateur", "scope.customBrowser": "Navigateur", + "scope.similar": "Similaire", "search.placeholder": "Rechercher des assets...", "search.title": "Rechercher par nom de fichier, tags ou attributs (ex. rating:5, ext:png)", "search.semanticToggle": "Activer/desactiver la recherche semantique IA (CLIP)", "search.aiSearch": "Recherche IA", + "search.findSimilar": "Trouver similaires", + "search.findingSimilar": "Recherche d'assets similaires...", + "search.selectAssetForSimilar": "Selectionnez d'abord un asset pour trouver des images/videos similaires.", + "search.findSimilarFailed": "Echec de la recherche similaire", + "search.similarResults": "Similaires a l'asset #{id} ({n} resultats)", + "search.similarReference": "Reference #{id}", + "tooltip.openMessages": "Messages et nouveautes", + "tooltip.openMessagesUnread": "Messages ({count} non lus)", + "tooltip.tab.similar": "Parcourir les trouvailles similaires courantes", "setting.ai.vector.enabled.name": "Activer la recherche semantique IA", "setting.ai.vector.enabled.desc": "Active la recherche semantique basee sur CLIP. Desactivez pour une recherche par mots-cles uniquement.", @@ -718,6 +800,7 @@ const DICTIONARY = { "btn.up": "Monter", "btn.saving": "Sauvegarde...", "btn.restoring": "Restauration...", + "btn.markAllRead": "Tout marquer lu", "ctx.pinAsBrowserRoot": "Epingler comme racine Browser", "ctx.createFolderHere": "Creer dossier ici...", @@ -725,12 +808,32 @@ const DICTIONARY = { "ctx.moveFolder": "Deplacer dossier...", "ctx.deleteFolder": "Supprimer dossier...", "ctx.refreshMetadata": "Rafraichir metadonnees", + "ctx.openFolder": "Ouvrir dossier", "dialog.browserRootLabelOptional": "Label pour nouvelle racine browser (optionnel)", "dialog.newFolderName": "Nom du nouveau dossier", "dialog.renameFolder": "Renommer dossier", "dialog.destinationDirectoryPath": "Chemin dossier destination", "dialog.deleteFolderRecursive": "Supprimer le dossier \"{name}\" et tout son contenu ?", + "dialog.settingsSaveFailed": "Majoor : echec sauvegarde des parametres (stockage navigateur plein ou bloque).", + "dialog.yes": "Oui", + "dialog.no": "Non", + "dialog.ok": "OK", + "dialog.prompt": "Saisie", + "dialog.choiceTypeNumber": "Entrez un numero :", + "dialog.confirmDeleteTitle": "Majoor : confirmer suppression", + "dialog.deleteSelectedFiles": "Supprimer {count} fichiers selectionnes ?", + "dialog.deleteSingleFile": "Supprimer \"{label}\" ?", + "dialog.vectorsReset.title": "Vecteurs IA", + "dialog.vectorsReset.choice": "Reinitialiser aussi les vecteurs IA ?\n\nConfirmer = oui, tout reinitialiser (les vecteurs seront recalcules)\nAnnuler = non, conserver les vecteurs existants", + "dialog.vectorsReset.keepQuestion": "Conserver les vecteurs IA existants ?\n\nConfirmer = conserver les vecteurs\nAnnuler = continuer sans vecteurs", + "dialog.vectorsReset.wipeConfirm": "Reinitialiser aussi les vecteurs IA ?\n\nConfirmer = oui, tout reinitialiser\nAnnuler = abandonner", + "dialog.vectorsReset.singleQuestion": "Choisissez le mode de reinitialisation pour {action} :\n\nOui = conserver les vecteurs IA existants\nNon = reinitialisation complete (les vecteurs seront recalcules)\nAnnuler = abandonner", + "dialog.vectorsReset.optionKeep": "Oui - conserver vecteurs", + "dialog.vectorsReset.optionFull": "Non - reinit complete", + "dialog.vectorsReset.optionCancel": "Annuler", + "dialog.resetIndex.confirmKeepVectors": "Cette action reinitialise l'index et relance le scan en conservant les vecteurs IA existants.\n\nContinuer ?", + "dialog.dbDelete.keepVectorsConfirm": "Cette action reinitialise l'index et conserve les vecteurs IA existants. Les fichiers DB ne seront pas supprimes de force.\n\nContinuer ?", "toast.createFolderFailed": "Echec creation dossier", "toast.renameFolderFailed": "Echec renommage dossier", @@ -742,13 +845,52 @@ const DICTIONARY = { "toast.folderDeleted": "Dossier supprime", "toast.pinFolderFailed": "Echec epinglage dossier", "toast.folderPinnedAsBrowserRoot": "Dossier epingle comme racine browser", + "toast.failedCreateSmartCollection": "Echec creation collection intelligente", + "toast.failedAddAssetsToSmartCollection": "Echec ajout des assets a la collection intelligente", + "toast.noGroupsFoundIndexFirst": "Aucun groupe trouve. Indexez plus d'assets d'abord.", + "toast.failedLoadClusterAssets": "Echec chargement assets du cluster", + "toast.collectionCreatedWithAssets": "Collection \"{name}\" creee avec {count} assets !", + "toast.collectionCreatedNamed": "Collection \"{name}\" creee.", + "toast.clusterAnalysisFailed": "Echec analyse des clusters", + "toast.vectorBackfillStarting": "Demarrage du vector backfill... Cela peut prendre du temps.", + "toast.vectorBackfillRunning": "Le vector backfill continue en arriere-plan{job}.", + "toast.vectorBackfillComplete": "Vector backfill termine ! Traites : {processed}, Indexes : {indexed}, Ignores : {skipped}", + "toast.vectorBackfillFailedGeneric": "Echec du backfill", + "toast.vectorBackfillFailedDetail": "Echec du vector backfill : {error}", + "toast.rescanUpdatingAiIndex": "Rescan du fichier + mise a jour index IA...", + "toast.metadataVectorUpdated": "Metadonnees + index vectoriel IA mis a jour pour cet asset.", + "toast.metadataUpdatedVectorFailed": "Metadonnees mises a jour. L'index vectoriel IA n'a pas pu etre mis a jour.", "label.computer": "Ordinateur", + "label.collection": "collection", + "label.thisFile": "ce fichier", + "label.messages": "Messages", + "label.readMe": "Read Me", + "label.userGuide": "Guide utilisateur", + "label.info": "Info", + "btn.giveStar": "Mettre une etoile", "label.resolutionMinWxH": "Min LxH (px)", "label.resolutionMaxWxH": "Max LxH (px)", + "msg.noMessages": "Aucun message pour le moment.", "msg.noPinnedFolders": "Aucun dossier epingle", "sidebar.placeholderSelectAsset": "Selectionnez un asset pour voir les details", - "msg.noTagsYet": "Aucun tag pour le moment..." + "msg.noTagsYet": "Aucun tag pour le moment...", + "msg.category.information": "Information", + "msg.whatsNew.title.pinReference": "Quoi de neuf", + "msg.whatsNew.body.pinReference": "Floating Viewer : nouvelle fonction Pin Reference. Vous pouvez maintenant epingler A ou B, puis comparer rapidement avec les assets selectionnes dans la grille tout en gardant la reference fixe.", + "msg.whatsNew.title.vectorResetKeepVectors": "Quoi de neuf", + "msg.whatsNew.body.vectorResetKeepVectors": "Reset index et Delete DB demandent d'abord s'il faut conserver les vecteurs IA. Si vous avez deja des anciens assets indexes, garder les vecteurs est recommande : un reset complet sans eux peut declencher un long Vector Backfill sur les anciens assets et augmenter temporairement la consommation RAM.", + "msg.whatsNew.title.localUserGuide": "Quoi de neuf", + "msg.whatsNew.body.localUserGuide": "Ouvrez le Guide utilisateur local directement depuis le dossier custom_nodes d'Assets Manager.", + "msg.collectionAdd.added": "{added} element(s) ajoute(s) a \"{name}\".", + "msg.collectionAdd.skippedExisting": "{count} element(s) ignores : deja presents dans la collection.", + "msg.collectionAdd.skippedDuplicate": "{count} doublon(s) ignores dans la selection.", + "msg.collectionAdd.noneAddedExisting": "Aucun nouvel element ajoute a \"{name}\" (tous deja presents).", + "msg.aiFeaturesNotice": "Avertissement :\n\nAssets Manager inclut maintenant des fonctions IA (auto captions, recherche IA, smart collections, etc.).\nCes fonctions sont encore en cours de developpement.\n\nSi Assets Manager etait deja installe, supprimez d'abord la DB.\nQuand l'enrichissement est termine, lancez Vector Backfill pour vos assets existants.\nApres avoir clique sur Backfill, cela peut prendre tres longtemps si vous avez beaucoup d'assets.", + "msg.dbResetNoticeDetail": "Note de mise a jour Majoor :\n\nPour eviter les erreurs de base de donnees avec cette version, supprimez votre index existant. Cliquez sur le bouton \"Delete DB\" dans le panneau Index Status pour le reinitialiser.", + "msg.nightlyUpdateTitle": "Majoor Assets Manager", + "msg.nightlyUpdateDetail": "Une build nightly plus recente est disponible : https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/releases/tag/nightly", + "tooltip.starGithub": "Ouvrir GitHub et mettre une etoile", }, }; diff --git a/js/app/settings/settingsAdvanced.js b/js/app/settings/settingsAdvanced.js index f5f00c74..9ddcf460 100644 --- a/js/app/settings/settingsAdvanced.js +++ b/js/app/settings/settingsAdvanced.js @@ -543,7 +543,7 @@ export function registerAdvancedSettings(safeAddSetting, settings, notifyApplied if (String(value || "") !== "Run backfill now") return; try { - comfyToast("Starting vector backfill... This may take a while.", "info"); + comfyToast(t("toast.vectorBackfillStarting", "Starting vector backfill... This may take a while."), "info"); const result = await vectorBackfill(64); if (result?.ok) { @@ -557,11 +557,19 @@ export function registerAdvancedSettings(safeAddSetting, settings, notifyApplied if (pending) { const jobId = String(data?.job_id || "").trim(); comfyToast( - `Vector backfill still running in background${jobId ? ` (job ${jobId.slice(0, 8)})` : ""}.`, + t( + "toast.vectorBackfillRunning", + "Vector backfill still running in background{job}.", + { job: jobId ? ` (job ${jobId.slice(0, 8)})` : "" } + ), "info", ); } else { - const msg = `Vector backfill complete! Processed: ${processed}, Indexed: ${indexed}, Skipped: ${skipped}`; + const msg = t( + "toast.vectorBackfillComplete", + "Vector backfill complete! Processed: {processed}, Indexed: {indexed}, Skipped: {skipped}", + { processed, indexed, skipped } + ); comfyToast(msg, "success"); } try { @@ -573,11 +581,11 @@ export function registerAdvancedSettings(safeAddSetting, settings, notifyApplied console.warn("[Majoor] Failed to refresh vector stats:", statsErr); } } else { - throw new Error(result?.error || "Backfill failed"); + throw new Error(result?.error || t("toast.vectorBackfillFailedGeneric", "Backfill failed")); } } catch (error) { - const errMsg = error?.message || String(error || "Unknown error"); - comfyToast(`Vector backfill failed: ${errMsg}`, "error"); + const errMsg = error?.message || String(error || t("status.unknown", "unknown")); + comfyToast(t("toast.vectorBackfillFailedDetail", "Vector backfill failed: {error}", { error: errMsg }), "error"); console.error("[Majoor] Vector backfill error:", error); } }, diff --git a/js/app/settings/settingsCore.js b/js/app/settings/settingsCore.js index 65670264..21e0dbb5 100644 --- a/js/app/settings/settingsCore.js +++ b/js/app/settings/settingsCore.js @@ -9,6 +9,7 @@ import { safeDispatchCustomEvent } from "../../utils/events.js"; import { SettingsStore } from "./SettingsStore.js"; import { SETTINGS_KEY, SETTINGS_SCHEMA_VERSION } from "../settingsStore.js"; import { _safeBool, _safeNum, deepMerge, resolveGridMinSize } from "./settingsUtils.js"; +import { t } from "../i18n.js"; // โ”€โ”€โ”€ Default settings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -236,7 +237,9 @@ export const saveMajoorSettings = (settings) => { if (now - last > 30_000) { window._mjrSettingsSaveFailAt = now; import("./dialogs.js") - .then(({ comfyAlert }) => comfyAlert("Majoor: Failed to save settings (browser storage full or blocked).")) + .then(({ comfyAlert }) => comfyAlert( + t("dialog.settingsSaveFailed", "Majoor: Failed to save settings (browser storage full or blocked).") + )) .catch(() => {}); } } catch (e) { console.debug?.(e); } diff --git a/js/app/versionCheck.js b/js/app/versionCheck.js index ecb08cc2..336417be 100644 --- a/js/app/versionCheck.js +++ b/js/app/versionCheck.js @@ -160,7 +160,7 @@ function showAiFeaturesNoticeOnce(localVersion, branch) { comfyAlert( t( "msg.aiFeaturesNotice", - "Avertissement / Warning:\n\nAssets Manager now includes AI features like auto captions, AI search, smart collections, etc.\nThese features are still WIP.\n\nIf you already had Assets Manager installed, delete the DB first.\nWhen enrich is finished, run Vector Backfill for your existing assets.\nAfter clicking Backfill, you can go sleep: it may take a very, very long time if you have many assets." + "Warning:\n\nAssets Manager now includes AI features like auto captions, AI search, smart collections, etc.\nThese features are still WIP.\n\nIf you already had Assets Manager installed, delete the DB first.\nWhen enrich is finished, run Vector Backfill for your existing assets.\nAfter clicking Backfill, you can go sleep: it may take a very, very long time if you have many assets." ), "Majoor", { native: true } @@ -313,7 +313,7 @@ export async function checkMajoorVersion({ force = false } = {}) { setTimeout(() => { comfyAlert( t( - "msg.dbResetNotice", + "msg.dbResetNoticeDetail", "Majoor Update Notice:\n\nTo avoid database errors with this new version, please delete your existing index. Click the 'Delete DB' button in the Index Status panel to reset it." ), "Majoor", diff --git a/js/features/collections/contextmenu/addToCollectionMenu.js b/js/features/collections/contextmenu/addToCollectionMenu.js index 8c7bf0fd..7a828cda 100644 --- a/js/features/collections/contextmenu/addToCollectionMenu.js +++ b/js/features/collections/contextmenu/addToCollectionMenu.js @@ -1,6 +1,7 @@ import { comfyPrompt } from "../../../app/dialogs.js"; import { comfyToast } from "../../../app/toast.js"; import { listCollections, createCollection, addAssetsToCollection } from "../../../api/client.js"; +import { t } from "../../../app/i18n.js"; import { getOrCreateMenu as getOrCreateMenuCore, createMenuItem, @@ -51,17 +52,17 @@ function formatAddResultMessage({ collectionName, selectedCount, addRes }) { const skippedExisting = Number(addRes?.data?.skipped_existing ?? 0) || 0; const skippedDuplicate = Number(addRes?.data?.skipped_duplicate ?? 0) || 0; - const name = String(collectionName || "").trim() || "collection"; - let msg = `Added ${added} item(s) to "${name}".`; + const name = String(collectionName || "").trim() || t("label.collection", "collection"); + let msg = t("msg.collectionAdd.added", "Added {added} item(s) to \"{name}\".", { added, name }); if (skippedExisting > 0) { - msg += `\n\nSkipped ${skippedExisting} item(s): already present in the collection.`; + msg += `\n\n${t("msg.collectionAdd.skippedExisting", "Skipped {count} item(s): already present in the collection.", { count: skippedExisting })}`; } if (skippedDuplicate > 0) { - msg += `\n\nIgnored ${skippedDuplicate} duplicate(s) in selection.`; + msg += `\n\n${t("msg.collectionAdd.skippedDuplicate", "Ignored {count} duplicate(s) in selection.", { count: skippedDuplicate })}`; } if (added === 0 && skippedExisting > 0 && selectedCount > 0) { - msg = `No new items added to "${name}" (all exist).`; + msg = t("msg.collectionAdd.noneAddedExisting", "No new items added to \"{name}\" (all exist).", { name }); } return msg; } @@ -74,7 +75,7 @@ export async function showAddToCollectionMenu({ x, y, assets }) { const selected = Array.isArray(assets) ? assets.map(simplifyAsset).filter(Boolean) : []; if (!selected.length) { - comfyToast("No valid assets selected.", "warning"); + comfyToast(t("toast.noValidAssetsSelected", "No valid assets selected."), "warning"); return; } @@ -83,17 +84,20 @@ export async function showAddToCollectionMenu({ x, y, assets }) { // If none exist, directly prompt creation. if (!collections.length) { - const name = await comfyPrompt("Create collection", "My collection"); + const name = await comfyPrompt( + t("dialog.createCollection", "Create collection"), + t("dialog.collectionPlaceholder", "My collection") + ); if (!name) return; const created = await createCollection(name); if (!created?.ok) { - comfyToast(created?.error || "Failed to create collection.", "error"); + comfyToast(created?.error || t("toast.failedCreateCollectionDot", "Failed to create collection."), "error"); return; } const cid = created.data?.id; const addRes = await addAssetsToCollection(cid, selected); if (!addRes?.ok) { - comfyToast(addRes?.error || "Failed to add assets to collection.", "error"); + comfyToast(addRes?.error || t("toast.failedAddAssetsToCollection", "Failed to add assets to collection."), "error"); return; } comfyToast(formatAddResultMessage({ collectionName: created.data?.name || name, selectedCount: selected.length, addRes }), "success", 4000); @@ -103,19 +107,22 @@ export async function showAddToCollectionMenu({ x, y, assets }) { const menu = getOrCreateCollectionsMenu(); clearMenu(menu); - const createIt = createItem("Create collection...", "pi pi-plus", async () => { + const createIt = createItem(`${t("dialog.createCollection", "Create collection")}...`, "pi pi-plus", async () => { hideMenu(menu); - const name = await comfyPrompt("Create collection", "My collection"); + const name = await comfyPrompt( + t("dialog.createCollection", "Create collection"), + t("dialog.collectionPlaceholder", "My collection") + ); if (!name) return; const created = await createCollection(name); if (!created?.ok) { - comfyToast(created?.error || "Failed to create collection.", "error"); + comfyToast(created?.error || t("toast.failedCreateCollectionDot", "Failed to create collection."), "error"); return; } const cid = created.data?.id; const addRes = await addAssetsToCollection(cid, selected); if (!addRes?.ok) { - comfyToast(addRes?.error || "Failed to add assets to collection.", "error"); + comfyToast(addRes?.error || t("toast.failedAddAssetsToCollection", "Failed to add assets to collection."), "error"); return; } comfyToast(formatAddResultMessage({ collectionName: created.data?.name || name, selectedCount: selected.length, addRes }), "success", 4000); @@ -132,7 +139,7 @@ export async function showAddToCollectionMenu({ x, y, assets }) { hideMenu(menu); const addRes = await addAssetsToCollection(cid, selected); if (!addRes?.ok) { - comfyToast(addRes?.error || "Failed to add assets to collection.", "error"); + comfyToast(addRes?.error || t("toast.failedAddAssetsToCollection", "Failed to add assets to collection."), "error"); return; } comfyToast(formatAddResultMessage({ collectionName: name, selectedCount: selected.length, addRes }), "success", 4000); diff --git a/js/features/collections/controllers/collectionsController.js b/js/features/collections/controllers/collectionsController.js index 3ff8b9c7..8a828667 100644 --- a/js/features/collections/controllers/collectionsController.js +++ b/js/features/collections/controllers/collectionsController.js @@ -306,7 +306,7 @@ async function _appendSmartSuggestions(menu, state, popovers, collectionsPopover // 1. Create the collection const res = await createCollection(idea.label); if (!res?.ok) { - comfyToast(res?.error || "Failed to create smart collection", "error"); + comfyToast(res?.error || t("toast.failedCreateSmartCollection", "Failed to create smart collection"), "error"); return; } const colId = String(res.data?.id || ""); @@ -334,7 +334,7 @@ async function _appendSmartSuggestions(menu, state, popovers, collectionsPopover } const addRes = await addAssetsToCollection(colId, assets); if (!addRes?.ok) { - comfyToast(addRes?.error || "Failed to add assets to smart collection", "error"); + comfyToast(addRes?.error || t("toast.failedAddAssetsToSmartCollection", "Failed to add assets to smart collection"), "error"); return; } const addedCount = Number(addRes?.data?.added || assets.length || 0); @@ -359,7 +359,7 @@ async function _appendSmartSuggestions(menu, state, popovers, collectionsPopover await reloadGrid(); } catch (err) { console.error("[Majoor] Smart collection creation failed:", err); - comfyToast("Failed to create smart collection", "error"); + comfyToast(t("toast.failedCreateSmartCollection", "Failed to create smart collection"), "error"); } }); @@ -444,7 +444,7 @@ async function _appendDiscoverGroups(menu, state, popovers, collectionsPopover, try { const res = await vectorSuggestCollections(8); if (!res?.ok || !Array.isArray(res.data) || !res.data.length) { - comfyToast("No groups found. Index more assets first.", "info", 3000); + comfyToast(t("toast.noGroupsFoundIndexFirst", "No groups found. Index more assets first."), "info", 3000); discoverBtn.textContent = "๐Ÿ” Analyze Library"; discoverBtn.disabled = false; return; @@ -522,7 +522,7 @@ async function _appendDiscoverGroups(menu, state, popovers, collectionsPopover, createBtn.textContent = "โ€ฆ"; const colRes = await createCollection(label); if (!colRes?.ok) { - comfyToast(colRes?.error || "Failed to create collection", "error"); + comfyToast(colRes?.error || t("toast.failedCreateCollection", "Failed to create collection"), "error"); createBtn.disabled = false; createBtn.textContent = "+ Create"; return; @@ -531,22 +531,29 @@ async function _appendDiscoverGroups(menu, state, popovers, collectionsPopover, if (colId && allIds.length) { const hydrated = await _resolveCollectionAssetsByIds(allIds); if (!hydrated.ok) { - comfyToast(hydrated.error || "Failed to load cluster assets", "error"); + comfyToast(hydrated.error || t("toast.failedLoadClusterAssets", "Failed to load cluster assets"), "error"); createBtn.disabled = false; createBtn.textContent = "+ Create"; return; } const addRes = await addAssetsToCollection(colId, hydrated.data || []); if (!addRes?.ok) { - comfyToast(addRes?.error || "Failed to add assets to collection", "error"); + comfyToast(addRes?.error || t("toast.failedAddAssetsToCollection", "Failed to add assets to collection"), "error"); createBtn.disabled = false; createBtn.textContent = "+ Create"; return; } const addedCount = Number(addRes?.data?.added || (hydrated.data || []).length || 0); - comfyToast(`Collection "${label}" created with ${addedCount} assets!`, "success", 3000); + comfyToast( + t("toast.collectionCreatedWithAssets", "Collection \"{name}\" created with {count} assets!", { + name: label, + count: addedCount, + }), + "success", + 3000 + ); } else { - comfyToast(`Collection "${label}" created.`, "success", 2200); + comfyToast(t("toast.collectionCreatedNamed", "Collection \"{name}\" created.", { name: label }), "success", 2200); } state.collectionId = colId; state.collectionName = label; @@ -555,7 +562,7 @@ async function _appendDiscoverGroups(menu, state, popovers, collectionsPopover, await reloadGrid(); } catch (err) { console.error("[Majoor] Cluster collection creation failed:", err); - comfyToast("Failed to create collection", "error"); + comfyToast(t("toast.failedCreateCollection", "Failed to create collection"), "error"); createBtn.disabled = false; createBtn.textContent = "+ Create"; } @@ -568,7 +575,7 @@ async function _appendDiscoverGroups(menu, state, popovers, collectionsPopover, } } catch (err) { console.error("[Majoor] Discover groups failed:", err); - comfyToast("Cluster analysis failed", "error"); + comfyToast(t("toast.clusterAnalysisFailed", "Cluster analysis failed"), "error"); discoverBtn.textContent = "๐Ÿ” Analyze Library"; discoverBtn.disabled = false; } diff --git a/js/features/contextmenu/GridContextMenu.js b/js/features/contextmenu/GridContextMenu.js index e67349ad..c6e16b94 100644 --- a/js/features/contextmenu/GridContextMenu.js +++ b/js/features/contextmenu/GridContextMenu.js @@ -403,9 +403,9 @@ export function bindGridContextMenu({ const currentPath = String(gridContainer?.dataset?.mjrSubfolder || "").trim(); if (!currentPath) return; menu.appendChild( - createItem("Create folder here...", "pi pi-plus", null, async () => { + createItem(t("ctx.createFolderHere", "Create folder here..."), "pi pi-plus", null, async () => { try { - const name = await comfyPrompt("New folder name", ""); + const name = await comfyPrompt(t("dialog.newFolderName", "New folder name"), ""); const next = String(name || "").trim(); if (!next) return; const res = await browserFolderOp({ op: "create", path: currentPath, name: next }); @@ -480,7 +480,7 @@ export function bindGridContextMenu({ const isBrowserScope = String(panelState?.scope || "").toLowerCase() === "custom"; const folderPath = String(asset?.filepath || "").trim(); menu.appendChild( - createItem("Open folder", "pi pi-folder-open", null, () => { + createItem(t("ctx.openFolder", "Open folder"), "pi pi-folder-open", null, () => { try { gridContainer.dispatchEvent( new CustomEvent("mjr:open-folder-asset", { @@ -581,7 +581,7 @@ export function bindGridContextMenu({ menu.appendChild( createItem(t("ctx.deleteFolder"), "pi pi-trash", null, async () => { try { - const folderName = String(asset?.filename || "this folder"); + const folderName = String(asset?.filename || t("label.thisFolder", "this folder")); const ok = await comfyConfirm( t("dialog.deleteFolderRecursive", { name: folderName }) ); diff --git a/js/features/panel/AssetsManagerPanel.js b/js/features/panel/AssetsManagerPanel.js index 4f5377f9..ec8533d6 100644 --- a/js/features/panel/AssetsManagerPanel.js +++ b/js/features/panel/AssetsManagerPanel.js @@ -52,8 +52,10 @@ import { createFilterPopoverView } from "./views/filterPopoverView.js"; import { createSortPopoverView } from "./views/sortPopoverView.js"; import { createSearchView } from "./views/searchView.js"; import { createPinnedFoldersPopoverView } from "./views/pinnedFoldersPopoverView.js"; +import { createMessagePopoverView } from "./views/messagePopoverView.js"; import { createSummaryBarView } from "./views/summaryBarView.js"; import { createAgendaCalendar } from "../filters/calendar/AgendaCalendar.js"; +import { bindMessagePopoverController } from "./messages/messagePopoverController.js"; import { normalizeQuery } from "./controllers/query.js"; import { createGridController } from "./controllers/gridController.js"; @@ -133,9 +135,13 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } try { container._mjrPanelState = state; } catch (e) { console.debug?.(e); } + state.viewScope = String(state.viewScope || "").trim().toLowerCase() === "similar" ? "similar" : ""; + state.similarResults = Array.isArray(state.similarResults) ? state.similarResults : []; + state.similarTitle = String(state.similarTitle || ""); + state.similarSourceAssetId = String(state.similarSourceAssetId || ""); const headerView = createHeaderView(); - const { header, headerActions, tabButtons, customMenuBtn, filterBtn, sortBtn, collectionsBtn, pinnedFoldersBtn } = headerView; + const { header, headerActions, tabButtons, customMenuBtn, filterBtn, sortBtn, collectionsBtn, pinnedFoldersBtn, messageBtn } = headerView; const { customPopover, customSelect, customAddBtn, customRemoveBtn } = createCustomPopoverView(); const { @@ -158,8 +164,10 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } const { sortPopover, sortMenu } = createSortPopoverView(); const { collectionsPopover, collectionsMenu } = createCollectionsPopoverView(); const { pinnedFoldersPopover, pinnedFoldersMenu } = createPinnedFoldersPopoverView(); + const { messagePopover, messageList, markReadBtn } = createMessagePopoverView(); headerActions.appendChild(customPopover); + headerActions.appendChild(messagePopover); const { searchSection, searchInputEl, similarBtn, setSemanticEnabled } = createSearchView({ filterBtn, @@ -213,6 +221,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } `; const gridWrapper = document.createElement("div"); + gridWrapper.classList.add("mjr-am-grid-scroll"); gridWrapper.style.cssText = ` flex: 1; overflow: auto; @@ -479,6 +488,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } getCollectionAssets, disposeGrid, getQuery, + searchInputEl, state }); @@ -683,6 +693,13 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } } }); + const exitSimilarViewIfActive = async ({ reload = false } = {}) => { + try { + if (String(state.viewScope || "").trim().toLowerCase() !== "similar") return; + await scopeController?.clearSimilarScope?.({ reload }); + } catch (e) { console.debug?.(e); } + }; + if (!header._mjrTabListenersBound) { Object.values(tabButtons).forEach((btn) => { btn.addEventListener("click", () => scopeController.setScope(btn.dataset.scope), { signal: panelLifecycleAC?.signal }); @@ -690,17 +707,35 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } header._mjrTabListenersBound = true; } + bindMessagePopoverController({ + messageBtn, + messagePopover, + messageList, + markReadBtn, + popovers, + signal: panelLifecycleAC?.signal, + onBeforeToggle: () => { + popovers.close(customPopover); + popovers.close(filterPopover); + popovers.close(sortPopover); + popovers.close(collectionsPopover); + popovers.close(pinnedFoldersPopover); + }, + }); + popovers.setDismissWhitelist([ customPopover, filterPopover, sortPopover, collectionsPopover, pinnedFoldersPopover, + messagePopover, customMenuBtn, filterBtn, sortBtn, collectionsBtn, - pinnedFoldersBtn + pinnedFoldersBtn, + messageBtn ]); customMenuBtn.addEventListener("click", (e) => { @@ -708,6 +743,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(filterPopover); popovers.close(sortPopover); popovers.close(collectionsPopover); + popovers.close(messagePopover); popovers.toggle(customPopover, customMenuBtn); }, { signal: panelLifecycleAC?.signal }); @@ -716,6 +752,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(customPopover); popovers.close(sortPopover); popovers.close(collectionsPopover); + popovers.close(messagePopover); popovers.toggle(filterPopover, filterBtn); }, { signal: panelLifecycleAC?.signal }); @@ -730,6 +767,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(filterPopover); popovers.close(sortPopover); popovers.close(collectionsPopover); + popovers.close(messagePopover); } catch (err) { console.debug?.(err); } const selectedIdRaw = String( @@ -757,13 +795,13 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } return; } const list = Array.isArray(res?.data) ? res.data : []; - await loadAssetsFromList(gridContainer, list, { - title: t("search.similarResults", "Similar to asset #{id} ({n} results)", { - id: selectedId, - n: list.length, - }), - reset: true, + state.similarResults = list; + state.similarSourceAssetId = String(selectedId); + state.similarTitle = t("search.similarResults", "Similar to asset #{id} ({n} results)", { + id: selectedId, + n: list.length, }); + await scopeController?.setScope?.("similar"); try { state.lastGridCount = Number(list.length || 0) || 0; state.lastGridTotal = Number(list.length || 0) || 0; @@ -794,6 +832,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(customPopover); popovers.close(filterPopover); popovers.close(collectionsPopover); + popovers.close(messagePopover); } }); @@ -814,6 +853,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(customPopover); popovers.close(filterPopover); popovers.close(sortPopover); + popovers.close(messagePopover); } }); @@ -961,6 +1001,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } popovers.close(filterPopover); popovers.close(sortPopover); popovers.close(collectionsPopover); + popovers.close(messagePopover); popovers.toggle(pinnedFoldersPopover, pinnedFoldersBtn); }, { signal: panelLifecycleAC?.signal }); @@ -1013,6 +1054,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } reloadGrid: gridController.reloadGrid, lifecycleSignal: panelLifecycleAC?.signal || null, onFiltersChanged: () => { + void exitSimilarViewIfActive({ reload: false }); try { agendaCalendar?.refresh?.(); } catch (e) { console.debug?.(e); } @@ -1048,6 +1090,19 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } reloadGrid: () => gridController.reloadGrid(), getExtraContext: () => ({ duplicatesAlert: _duplicatesAlert }), extraActions: { + clearSimilarScope: async () => { + try { + await scopeController?.clearSimilarScope?.(); + } catch (e) { console.debug?.(e); } + }, + clearTransientContext: async () => { + try { + state.viewScope = ""; + state.similarResults = []; + state.similarTitle = ""; + state.similarSourceAssetId = ""; + } catch (e) { console.debug?.(e); } + }, resetBrowserHistory: () => { try { browserNav?.resetHistory?.(); @@ -1423,6 +1478,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } const value = e?.target?.value ?? ""; if (value === lastSearchValue) return; lastSearchValue = value; + void exitSimilarViewIfActive({ reload: false }); notifyContextChanged(); if (searchTimeout) clearTimeout(searchTimeout); @@ -1443,6 +1499,7 @@ export async function renderAssetsManager(container, { useComfyThemeUI = true } }, { signal: panelLifecycleAC?.signal }); searchInputEl.addEventListener("keypress", (e) => { if (e.key === "Enter") { + void exitSimilarViewIfActive({ reload: false }); startTimer('searchQuery'); gridController.reloadGrid(); notifyContextChanged(); diff --git a/js/features/panel/controllers/contextController.js b/js/features/panel/controllers/contextController.js index ac57a381..91b6f1bb 100644 --- a/js/features/panel/controllers/contextController.js +++ b/js/features/panel/controllers/contextController.js @@ -84,6 +84,12 @@ export function createContextController({ } catch (e) { console.debug?.(e); } }, clearScope: async () => { + try { + if (String(state?.viewScope || "").trim().toLowerCase() === "similar" && typeof extraActions?.clearSimilarScope === "function") { + await extraActions.clearSimilarScope(); + return; + } + } catch (e) { console.debug?.(e); } let didSetScope = false; try { state.customRootId = ""; @@ -244,6 +250,9 @@ export function createContextController({ state.currentFolderRelativePath = ""; delete gridContainer?.dataset?.mjrSubfolder; resetBrowserHistory(); + try { + await extraActions?.clearTransientContext?.(); + } catch (e) { console.debug?.(e); } if (typeof scopeController?.setScope === "function") { await scopeController.setScope("output"); didSetScope = true; diff --git a/js/features/panel/controllers/gridController.js b/js/features/panel/controllers/gridController.js index 87e19ccc..1fd4ae8e 100644 --- a/js/features/panel/controllers/gridController.js +++ b/js/features/panel/controllers/gridController.js @@ -3,12 +3,22 @@ import { comfyToast } from "../../../app/toast.js"; import { t } from "../../../app/i18n.js"; import { loadMajoorSettings } from "../../../app/settings.js"; -export function createGridController({ gridContainer, loadAssets, loadAssetsFromList, getCollectionAssets, disposeGrid, getQuery, state }) { +export function createGridController({ + gridContainer, + loadAssets, + loadAssetsFromList, + getCollectionAssets, + disposeGrid, + getQuery, + searchInputEl, + state, +}) { let _isReloading = false; let _pendingReload = false; let _lastReloadErrorAt = 0; let _lastAiHintAt = 0; const RELOAD_WATCHDOG_MS = 30000; + const AI_RELOAD_WATCHDOG_MS = 150000; const AI_HINT_COOLDOWN_MS = 15_000; const runWithWatchdog = async (promiseFactory, timeoutMs = RELOAD_WATCHDOG_MS) => { @@ -32,8 +42,7 @@ export function createGridController({ gridContainer, loadAssets, loadAssetsFrom */ const _isSemanticMode = () => { try { - const input = gridContainer?.closest?.(".mjr-am-panel")?.querySelector?.("#mjr-search-input"); - return input?.dataset?.mjrSemanticMode === "1"; + return searchInputEl?.dataset?.mjrSemanticMode === "1"; } catch { return false; } }; @@ -305,6 +314,25 @@ export function createGridController({ gridContainer, loadAssets, loadAssetsFrom state.collectionName = ""; } + if (String(state.viewScope || "").toLowerCase() === "similar") { + const list = Array.isArray(state.similarResults) ? state.similarResults : []; + const sourceId = String(state.similarSourceAssetId || "").trim(); + const title = String( + state.similarTitle + || t("search.similarResults", "Similar to asset #{id} ({n} results)", { + id: sourceId || "?", + n: list.length, + }) + ).trim(); + const result = await loadAssetsFromList(gridContainer, list, { title: title || "Similar", reset: true }); + try { + state.lastGridCount = Number(result?.count || 0) || 0; + state.lastGridTotal = Number(result?.total || 0) || 0; + gridContainer.dispatchEvent?.(new CustomEvent("mjr:grid-stats", { detail: result || {} })); + } catch (e) { console.debug?.(e); } + return; + } + const result = await _loadWithSemanticFallback(getQuery()); // Track search query timing if timer was started @@ -335,7 +363,8 @@ export function createGridController({ gridContainer, loadAssets, loadAssetsFrom while (_pendingReload) { _pendingReload = false; try { - await runWithWatchdog(() => runReloadOnce()); + const useAiTimeout = _isAiEnabled() && (_isSemanticMode() || String(getQuery?.() || "").length >= 12); + await runWithWatchdog(() => runReloadOnce(), useAiTimeout ? AI_RELOAD_WATCHDOG_MS : RELOAD_WATCHDOG_MS); } catch (err) { // Keep UI responsive even if one reload got stuck, but don't fail silently. try { @@ -367,4 +396,3 @@ export function createGridController({ gridContainer, loadAssets, loadAssetsFrom return { reloadGrid }; } - diff --git a/js/features/panel/controllers/scopeController.js b/js/features/panel/controllers/scopeController.js index 0d49c208..8e270a70 100644 --- a/js/features/panel/controllers/scopeController.js +++ b/js/features/panel/controllers/scopeController.js @@ -10,29 +10,79 @@ export function createScopeController({ onBeforeReload = null }) { let scopeSwitchSeq = 0; + const hasSimilarResults = () => { + try { + return !!(String(state?.viewScope || "").trim().toLowerCase() === "similar" + || (Array.isArray(state?.similarResults) && state.similarResults.length > 0) + || String(state?.similarTitle || "").trim()); + } catch { + return false; + } + }; + + const resetSimilarScope = () => { + state.viewScope = ""; + state.similarResults = []; + state.similarTitle = ""; + state.similarSourceAssetId = ""; + }; const setActiveTabStyles = () => { const buttons = Object.values(tabButtons || {}); + const activeScope = String(state?.viewScope || state?.scope || "output").toLowerCase(); buttons.forEach((b) => { const scope = b?.dataset?.scope; - const active = scope === state.scope; + const active = scope === activeScope; b?.classList?.toggle?.("is-active", active); }); + try { + const similarBtn = tabButtons?.tabSimilar; + if (similarBtn) { + similarBtn.style.display = hasSimilarResults() ? "" : "none"; + } + } catch (e) { console.debug?.(e); } customMenuBtn.style.display = "none"; customPopover.style.display = "none"; popovers.close(customPopover); }; + const clearSimilarScope = async ({ reload = true } = {}) => { + const hadSimilar = hasSimilarResults(); + resetSimilarScope(); + setActiveTabStyles(); + try { + onChanged?.(); + } catch (e) { console.debug?.(e); } + if (!hadSimilar || !reload) return; + await reloadGrid(); + }; + const setScope = async (scope) => { const requestedSeq = ++scopeSwitchSeq; - const allowed = new Set(["output", "outputs", "input", "inputs", "all", "custom"]); + const allowed = new Set(["output", "outputs", "input", "inputs", "all", "custom", "similar"]); const normalized = String(scope || "").toLowerCase(); if (!allowed.has(normalized)) return; + if (normalized === "similar") { + if (!hasSimilarResults()) return; + state.viewScope = "similar"; + setActiveTabStyles(); + try { + onChanged?.(); + } catch (e) { console.debug?.(e); } + try { + await onBeforeReload?.(state); + } catch (e) { console.debug?.(e); } + if (requestedSeq !== scopeSwitchSeq) return; + await reloadGrid(); + return; + } + // Switching scope exits any active collection view. state.collectionId = ""; state.collectionName = ""; + resetSimilarScope(); state.scope = normalized === "outputs" ? "output" : normalized === "inputs" ? "input" : normalized; // Scope switch should reset folder context to avoid stale filtering behavior. @@ -56,6 +106,6 @@ export function createScopeController({ await reloadGrid(); }; - return { setScope, setActiveTabStyles }; + return { setScope, setActiveTabStyles, clearSimilarScope }; } diff --git a/js/features/panel/controllers/sidebarController.js b/js/features/panel/controllers/sidebarController.js index 583dad43..e715e2be 100644 --- a/js/features/panel/controllers/sidebarController.js +++ b/js/features/panel/controllers/sidebarController.js @@ -31,7 +31,7 @@ async function rescanSingleAsset({ card, asset, sidebar, onAssetUpdated }) { } } catch (e) { console.debug?.(e); } - try { comfyToast("Rescanning file + updating AI index...", "info", 2200); } catch (e) { console.debug?.(e); } + try { comfyToast(t("toast.rescanUpdatingAiIndex", "Rescanning file + updating AI index..."), "info", 2200); } catch (e) { console.debug?.(e); } const fileEntry = { filename: asset.filename, @@ -106,9 +106,9 @@ async function rescanSingleAsset({ card, asset, sidebar, onAssetUpdated }) { try { if (indexOk && vectorIndexOk) { - comfyToast("Metadata + AI vector index updated for this asset.", "success", 2200); + comfyToast(t("toast.metadataVectorUpdated", "Metadata + AI vector index updated for this asset."), "success", 2200); } else if (indexOk && vectorAttempted && !vectorIndexOk) { - comfyToast("Metadata updated. AI vector index could not be updated.", "warning", 2600); + comfyToast(t("toast.metadataUpdatedVectorFailed", "Metadata updated. AI vector index could not be updated."), "warning", 2600); } } catch (e) { console.debug?.(e); } diff --git a/js/features/panel/messages/messageCenter.js b/js/features/panel/messages/messageCenter.js new file mode 100644 index 00000000..9915ff2c --- /dev/null +++ b/js/features/panel/messages/messageCenter.js @@ -0,0 +1,324 @@ +const STORAGE_MESSAGES_KEY = "mjr_panel_messages_v1"; +const STORAGE_LAST_READ_KEY = "mjr_panel_messages_last_read_v1"; +const STORAGE_DISMISSED_BUILTIN_IDS_KEY = "mjr_panel_messages_dismissed_builtin_ids_v1"; +const MAX_STORED_MESSAGES = 120; +const LOCAL_USER_GUIDE_URL = "/mjr/am/user-guide"; +const ALLOWED_MESSAGE_LEVELS = new Set(["info", "success", "warning", "error"]); + +export const PANEL_MESSAGES_EVENT = "mjr:panel-messages-changed"; + +let _loaded = false; +let _messages = []; +const BUILTIN_PANEL_MESSAGES = Object.freeze([ + { + id: "whats-new-2026-03-05-pin-reference", + title: "What's New", + titleKey: "msg.whatsNew.title.pinReference", + category: "Information", + categoryKey: "msg.category.information", + level: "info", + createdAt: Date.parse("2026-03-05T10:00:00Z"), + body: "Floating Viewer: new Pin Reference feature. You can now pin A or B, then compare quickly with selected assets in the grid while keeping the fixed reference.", + bodyKey: "msg.whatsNew.body.pinReference", + }, + { + id: "whats-new-2026-03-05-vector-reset-keep-vectors", + title: "What's New", + titleKey: "msg.whatsNew.title.vectorResetKeepVectors", + category: "Information", + categoryKey: "msg.category.information", + level: "info", + createdAt: Date.parse("2026-03-05T12:00:00Z"), + body: "Reset index and Delete DB now first ask whether to keep AI vectors. If you already have older indexed assets, keeping the vectors is recommended: a full reset without them can trigger a long Vector Backfill for old assets and temporarily increase RAM usage.", + bodyKey: "msg.whatsNew.body.vectorResetKeepVectors", + }, + { + id: "whats-new-2026-03-05-local-user-guide", + title: "What's New", + titleKey: "msg.whatsNew.title.localUserGuide", + category: "Information", + categoryKey: "msg.category.information", + level: "info", + createdAt: Date.parse("2026-03-05T13:00:00Z"), + body: "Open the local User Guide directly from your Assets Manager custom_nodes folder.", + bodyKey: "msg.whatsNew.body.localUserGuide", + actionLabel: "User Guide", + actionLabelKey: "label.userGuide", + actionUrl: LOCAL_USER_GUIDE_URL, + }, +]); +const BUILTIN_MESSAGE_IDS = new Set(BUILTIN_PANEL_MESSAGES + .map((msg) => String(msg?.id || "").trim()) + .filter(Boolean)); + +function _toNumber(value, fallback = 0) { + const n = Number(value); + return Number.isFinite(n) ? n : fallback; +} + +function _safeNow() { + return Date.now(); +} + +function _makeId() { + return `msg-${_safeNow()}-${Math.random().toString(36).slice(2, 8)}`; +} + +function _normalizeMessageLevel(value) { + const raw = String(value || "").trim().toLowerCase(); + if (raw === "warn") return "warning"; + if (raw === "danger") return "error"; + return ALLOWED_MESSAGE_LEVELS.has(raw) ? raw : "info"; +} + +function _isBuiltinMessageId(id) { + return BUILTIN_MESSAGE_IDS.has(String(id || "").trim()); +} + +function _normalizeMessage(input = {}) { + const id = String(input?.id || "").trim() || _makeId(); + const title = String(input?.title || "").trim() || "Info"; + const titleKey = String(input?.titleKey || "").trim(); + const emoji = String(input?.emoji || "").trim(); + const body = String(input?.body || "").trim(); + const bodyKey = String(input?.bodyKey || "").trim(); + const category = String(input?.category || "").trim() || "Info"; + const categoryKey = String(input?.categoryKey || "").trim(); + const createdAt = _toNumber(input?.createdAt, _safeNow()); + const level = _normalizeMessageLevel(input?.level); + const actionLabel = String(input?.actionLabel || "").trim(); + const actionLabelKey = String(input?.actionLabelKey || "").trim(); + const actionUrl = String(input?.actionUrl || "").trim(); + return { + id, + title, + titleKey, + emoji, + body, + bodyKey, + category, + categoryKey, + createdAt, + level, + actionLabel, + actionLabelKey, + actionUrl, + }; +} + +function _readStorageJson(key, fallback) { + try { + const raw = localStorage.getItem(key); + if (!raw) return fallback; + return JSON.parse(raw); + } catch { + return fallback; + } +} + +function _writeStorageJson(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // ignore storage quota/privacy errors + } +} + +function _readLastReadAt() { + try { + return _toNumber(localStorage.getItem(STORAGE_LAST_READ_KEY), 0); + } catch { + return 0; + } +} + +function _writeLastReadAt(ts) { + try { + localStorage.setItem(STORAGE_LAST_READ_KEY, String(_toNumber(ts, _safeNow()))); + } catch { + // ignore storage quota/privacy errors + } +} + +function _readDismissedBuiltinIds() { + const raw = _readStorageJson(STORAGE_DISMISSED_BUILTIN_IDS_KEY, []); + const ids = Array.isArray(raw) ? raw : []; + return new Set( + ids + .map((id) => String(id || "").trim()) + .filter((id) => _isBuiltinMessageId(id)) + ); +} + +function _writeDismissedBuiltinIds(ids) { + const safeIds = Array.from(ids || []) + .map((id) => String(id || "").trim()) + .filter((id) => _isBuiltinMessageId(id)); + _writeStorageJson(STORAGE_DISMISSED_BUILTIN_IDS_KEY, safeIds); +} + +function _dismissBuiltinMessage(id) { + const key = String(id || "").trim(); + if (!_isBuiltinMessageId(key)) return false; + const dismissedIds = _readDismissedBuiltinIds(); + if (dismissedIds.has(key)) return false; + dismissedIds.add(key); + _writeDismissedBuiltinIds(dismissedIds); + return true; +} + +function _restoreBuiltinMessage(id) { + const key = String(id || "").trim(); + if (!_isBuiltinMessageId(key)) return false; + const dismissedIds = _readDismissedBuiltinIds(); + if (!dismissedIds.delete(key)) return false; + _writeDismissedBuiltinIds(dismissedIds); + return true; +} + +function _sortMessages() { + _messages.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0)); + if (_messages.length > MAX_STORED_MESSAGES) { + _messages = _messages.slice(0, MAX_STORED_MESSAGES); + } +} + +function _persistMessages() { + _writeStorageJson(STORAGE_MESSAGES_KEY, _messages); +} + +function _seedBuiltinMessages() { + const dismissedBuiltinIds = _readDismissedBuiltinIds(); + let changed = false; + for (const raw of BUILTIN_PANEL_MESSAGES) { + const normalized = _normalizeMessage(raw); + if (dismissedBuiltinIds.has(normalized.id)) { + continue; + } + const idx = _messages.findIndex((m) => m.id === normalized.id); + if (idx < 0) { + _messages.push(normalized); + changed = true; + continue; + } + const prev = _messages[idx]; + const merged = { + ...prev, + ...normalized, + // Keep the original publish timestamp if already present. + createdAt: _toNumber(prev?.createdAt, normalized.createdAt), + }; + if (JSON.stringify(prev) !== JSON.stringify(merged)) { + _messages[idx] = merged; + changed = true; + } + } + if (!changed) return false; + _sortMessages(); + _persistMessages(); + return true; +} + +function _emitChanged() { + try { + window.dispatchEvent(new CustomEvent(PANEL_MESSAGES_EVENT, { + detail: { + total: _messages.length, + unread: getPanelUnreadCount(), + }, + })); + } catch (e) { console.debug?.(e); } +} + +function _ensureLoaded() { + if (_loaded) return; + _loaded = true; + const parsed = _readStorageJson(STORAGE_MESSAGES_KEY, []); + const safeArray = Array.isArray(parsed) ? parsed : []; + _messages = safeArray.map(_normalizeMessage); + _sortMessages(); +} + +export function ensurePanelMessagesReady() { + _ensureLoaded(); + if (_seedBuiltinMessages()) _emitChanged(); +} + +export function listPanelMessages() { + _ensureLoaded(); + return _messages.map((message) => ({ ...message })); +} + +export function getPanelUnreadCount() { + _ensureLoaded(); + const lastReadAt = _readLastReadAt(); + return _messages.reduce((count, msg) => { + return count + (Number(msg.createdAt || 0) > lastReadAt ? 1 : 0); + }, 0); +} + +export function getPanelLastReadAt() { + _ensureLoaded(); + return _readLastReadAt(); +} + +export function markPanelMessagesRead({ upTo = _safeNow() } = {}) { + _ensureLoaded(); + _writeLastReadAt(_toNumber(upTo, _safeNow())); + _emitChanged(); +} + +export function upsertPanelMessage(message) { + _ensureLoaded(); + const normalized = _normalizeMessage(message); + _restoreBuiltinMessage(normalized.id); + const idx = _messages.findIndex((m) => m.id === normalized.id); + let stored = null; + if (idx >= 0) { + const prev = _messages[idx]; + stored = { + ...prev, + ...normalized, + // Keep original publish timestamp unless explicitly provided. + createdAt: _toNumber(message?.createdAt, _toNumber(prev?.createdAt, normalized.createdAt)), + }; + _messages[idx] = stored; + } else { + stored = { ...normalized }; + _messages.push(stored); + } + _sortMessages(); + _persistMessages(); + _emitChanged(); + return { ...stored }; +} + +export function addPanelMessage(message) { + return upsertPanelMessage(message); +} + +export function removePanelMessage(id) { + _ensureLoaded(); + const key = String(id || "").trim(); + if (!key) return false; + const next = _messages.filter((m) => m.id !== key); + if (next.length === _messages.length) return false; + _messages = next; + _dismissBuiltinMessage(key); + _persistMessages(); + _emitChanged(); + return true; +} + +export function clearPanelMessages() { + _ensureLoaded(); + const dismissedBuiltinIds = _readDismissedBuiltinIds(); + for (const builtinId of BUILTIN_MESSAGE_IDS) { + dismissedBuiltinIds.add(builtinId); + } + _writeDismissedBuiltinIds(dismissedBuiltinIds); + _messages = []; + _persistMessages(); + _writeLastReadAt(_safeNow()); + _emitChanged(); +} diff --git a/js/features/panel/messages/messagePopoverController.js b/js/features/panel/messages/messagePopoverController.js new file mode 100644 index 00000000..41f494e7 --- /dev/null +++ b/js/features/panel/messages/messagePopoverController.js @@ -0,0 +1,250 @@ +import { t } from "../../../app/i18n.js"; +import { + PANEL_MESSAGES_EVENT, + ensurePanelMessagesReady, + getPanelLastReadAt, + getPanelUnreadCount, + listPanelMessages, + markPanelMessagesRead, +} from "./messageCenter.js"; + +const ALLOWED_MESSAGE_LEVELS = new Set(["info", "success", "warning", "error"]); + +function formatMessageDate(ts) { + const stamp = Number(ts); + if (!Number.isFinite(stamp) || stamp <= 0) return ""; + try { + return new Date(stamp).toLocaleString([], { + year: "numeric", + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return ""; + } +} + +function resolveMessageText(entry, keyField, fallbackField, fallbackText) { + const key = String(entry?.[keyField] || "").trim(); + const fallback = String(entry?.[fallbackField] || fallbackText || "").trim() || String(fallbackText || ""); + return key ? t(key, fallback) : fallback; +} + +function normalizeMessageLevel(value) { + const raw = String(value || "").trim().toLowerCase(); + if (raw === "warn") return "warning"; + if (raw === "danger") return "error"; + return ALLOWED_MESSAGE_LEVELS.has(raw) ? raw : "info"; +} + +function resolveSafeActionUrl(value) { + const raw = String(value || "").trim(); + if (!raw) return ""; + try { + const baseHref = String(globalThis?.window?.location?.href || "http://127.0.0.1/"); + const url = new URL(raw, baseHref); + const protocol = String(url.protocol || "").toLowerCase(); + return protocol === "http:" || protocol === "https:" ? url.href : ""; + } catch { + return ""; + } +} + +function resolveMessageEmoji(entry) { + const explicit = String(entry?.emoji || "").trim(); + if (explicit) return explicit; + + const level = normalizeMessageLevel(entry?.level); + if (level === "success") return "\u2705"; + if (level === "warning") return "\u26A0\uFE0F"; + if (level === "error") return "\u274C"; + return "\uD83D\uDCA1"; +} + +export function bindMessagePopoverController({ + messageBtn = null, + messagePopover = null, + messageList = null, + markReadBtn = null, + popovers = null, + onBeforeToggle = null, + signal = null, +} = {}) { + ensurePanelMessagesReady(); + + const syncExpandedState = () => { + const isOpen = messagePopover?.style?.display === "block"; + try { + messageBtn?.setAttribute("aria-expanded", isOpen ? "true" : "false"); + } catch (e) { console.debug?.(e); } + try { + messagePopover?.setAttribute("aria-hidden", isOpen ? "false" : "true"); + } catch (e) { console.debug?.(e); } + }; + + const renderMessagesPopover = () => { + if (!messageList) return; + const entries = listPanelMessages(); + const lastReadAt = getPanelLastReadAt(); + messageList.replaceChildren(); + if (!entries.length) { + const empty = document.createElement("div"); + empty.className = "mjr-messages-empty"; + empty.textContent = t("msg.noMessages", "No messages for now."); + messageList.appendChild(empty); + return; + } + + for (const entry of entries) { + const item = document.createElement("article"); + item.className = "mjr-message-item"; + const level = normalizeMessageLevel(entry?.level); + item.classList.add(`mjr-message-item--${level}`); + const createdAt = Number(entry?.createdAt || 0); + const unread = createdAt > Number(lastReadAt || 0); + if (unread) item.classList.add("mjr-message-item--unread"); + + const head = document.createElement("div"); + head.className = "mjr-message-item-head"; + + const titleWrap = document.createElement("div"); + titleWrap.className = "mjr-message-item-title-wrap"; + + const emoji = document.createElement("span"); + emoji.className = "mjr-message-item-emoji"; + emoji.textContent = resolveMessageEmoji(entry); + emoji.setAttribute("aria-hidden", "true"); + + const title = document.createElement("div"); + title.className = "mjr-message-item-title"; + title.textContent = resolveMessageText(entry, "titleKey", "title", t("label.info", "Info")); + + const category = document.createElement("span"); + category.className = "mjr-message-item-category"; + category.textContent = resolveMessageText(entry, "categoryKey", "category", t("label.info", "Info")); + + titleWrap.appendChild(emoji); + titleWrap.appendChild(title); + head.appendChild(titleWrap); + head.appendChild(category); + + const meta = document.createElement("div"); + meta.className = "mjr-message-item-meta"; + meta.textContent = formatMessageDate(createdAt); + + const body = document.createElement("div"); + body.className = "mjr-message-item-body"; + body.textContent = resolveMessageText(entry, "bodyKey", "body", ""); + + item.appendChild(head); + if (meta.textContent) item.appendChild(meta); + if (body.textContent) item.appendChild(body); + + const actionUrl = resolveSafeActionUrl(entry?.actionUrl); + const actionLabel = resolveMessageText(entry, "actionLabelKey", "actionLabel", t("label.readMe", "Read Me")); + if (actionUrl && actionLabel) { + const action = document.createElement("a"); + action.className = "mjr-message-item-action"; + action.href = actionUrl; + action.target = "_blank"; + action.rel = "noopener noreferrer"; + action.textContent = actionLabel; + item.appendChild(action); + } + messageList.appendChild(item); + } + }; + + const updateMessageButtonState = () => { + if (!messageBtn) return; + const unreadCount = Number(getPanelUnreadCount() || 0); + const badge = messageBtn.querySelector(".mjr-message-badge"); + const title = unreadCount > 0 + ? t("tooltip.openMessagesUnread", "Messages ({count} unread)", { count: unreadCount }) + : t("tooltip.openMessages", "Messages and updates"); + messageBtn.classList.toggle("mjr-message-has-unread", unreadCount > 0); + messageBtn.title = title; + messageBtn.setAttribute("aria-label", title); + if (!badge) return; + if (unreadCount <= 0) { + badge.style.display = "none"; + badge.textContent = ""; + return; + } + badge.style.display = "inline-flex"; + badge.textContent = unreadCount > 9 ? "9+" : String(unreadCount); + }; + + const refreshMessagesUI = () => { + updateMessageButtonState(); + if (messagePopover?.style?.display === "block") { + renderMessagesPopover(); + } + }; + + const listenerOptions = signal ? { signal } : undefined; + + try { + window.addEventListener(PANEL_MESSAGES_EVENT, refreshMessagesUI, listenerOptions); + } catch (e) { console.debug?.(e); } + try { + if (messageBtn && messagePopover) { + if (!messagePopover.id) messagePopover.id = "mjr-messages-popover"; + messageBtn.setAttribute("aria-haspopup", "dialog"); + messageBtn.setAttribute("aria-controls", messagePopover.id); + } + } catch (e) { console.debug?.(e); } + + let expandedObserver = null; + try { + if (typeof MutationObserver !== "undefined" && messagePopover) { + expandedObserver = new MutationObserver(syncExpandedState); + expandedObserver.observe(messagePopover, { + attributes: true, + attributeFilter: ["style"], + }); + } + } catch (e) { console.debug?.(e); } + try { + signal?.addEventListener?.("abort", () => expandedObserver?.disconnect?.(), { once: true }); + } catch (e) { console.debug?.(e); } + + updateMessageButtonState(); + syncExpandedState(); + + messageBtn?.addEventListener("click", (e) => { + if (!messagePopover || !popovers || !messageBtn) return; + e.stopPropagation(); + renderMessagesPopover(); + try { + if (typeof onBeforeToggle === "function") onBeforeToggle(); + } catch (err) { console.debug?.(err); } + popovers.toggle(messagePopover, messageBtn); + if (messagePopover?.style?.display === "block") { + markPanelMessagesRead(); + } + syncExpandedState(); + updateMessageButtonState(); + }, listenerOptions); + + markReadBtn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + markPanelMessagesRead(); + renderMessagesPopover(); + updateMessageButtonState(); + }, listenerOptions); + + return { + refresh: refreshMessagesUI, + render: renderMessagesPopover, + updateButtonState: updateMessageButtonState, + close: () => { + if (!messagePopover || !popovers) return; + popovers.close(messagePopover); + syncExpandedState(); + }, + }; +} diff --git a/js/features/panel/state/panelState.js b/js/features/panel/state/panelState.js index 3a2dea02..7470d386 100644 --- a/js/features/panel/state/panelState.js +++ b/js/features/panel/state/panelState.js @@ -72,6 +72,10 @@ function savePanelState(state) { // Don't persist transient stats delete toSave.lastGridCount; delete toSave.lastGridTotal; + delete toSave.viewScope; + delete toSave.similarResults; + delete toSave.similarTitle; + delete toSave.similarSourceAssetId; const serialized = JSON.stringify(toSave); if (serialized === _lastSavedSerialized) return; _lastSavedSerialized = serialized; @@ -115,7 +119,13 @@ export function createPanelState() { selectedAssetIds: saved.selectedAssetIds || [], // Sidebar open state for restoring across panel close/reopen. - sidebarOpen: saved.sidebarOpen || false + sidebarOpen: saved.sidebarOpen || false, + + // Transient virtual scopes. + viewScope: "", + similarResults: [], + similarTitle: "", + similarSourceAssetId: "", }; let proxy = null; diff --git a/js/features/panel/views/contextPillsView.js b/js/features/panel/views/contextPillsView.js index c03e08e3..ca3d12eb 100644 --- a/js/features/panel/views/contextPillsView.js +++ b/js/features/panel/views/contextPillsView.js @@ -18,6 +18,7 @@ const _labelSort = (key) => { const _labelScope = (scope) => { const s = String(scope || "").toLowerCase(); + if (s === "similar") return t("scope.similar", "Similar"); if (s === "custom") return t("scope.customBrowser"); if (s === "all") return t("tab.all"); if (s === "input" || s === "inputs") return t("scope.input"); @@ -81,12 +82,14 @@ export function createContextPillsView() { ); const isSortActive = _safeText(state?.sort || "mtime_desc").trim() !== "mtime_desc"; const isCollectionActive = _safeText(state?.collectionId || "").trim().length > 0; - const scopeValue = String(state?.scope || "output").toLowerCase(); + const scopeValue = String(state?.viewScope || state?.scope || "output").toLowerCase(); + const isSimilarScope = scopeValue === "similar"; const isBrowserScope = scopeValue === "custom"; const isScopeActive = scopeValue !== "output"; + const similarSourceId = _safeText(state?.similarSourceAssetId || "").trim(); const anyContext = - isQueryActive || isFilterActive || isSortActive || isCollectionActive || isScopeActive; + isQueryActive || isFilterActive || isSortActive || isCollectionActive || isScopeActive || isSimilarScope; if (!anyContext && !root.childElementCount) { return; } @@ -104,12 +107,23 @@ export function createContextPillsView() { }) ); } + if (isSimilarScope) { + root.appendChild( + _createPill({ + label: t("search.findSimilar", "Find Similar"), + value: similarSourceId + ? t("search.similarReference", "Reference #{id}", { id: similarSourceId }) + : t("scope.similar", "Similar"), + onClear: () => safeActions?.clearSimilarScope?.() + }) + ); + } if (isScopeActive) { root.appendChild( _createPill({ label: t("label.scope"), - value: _labelScope(state?.scope || "output"), - onClear: () => safeActions?.clearScope?.() + value: _labelScope(scopeValue || "output"), + onClear: () => (isSimilarScope ? safeActions?.clearSimilarScope?.() : safeActions?.clearScope?.()) }) ); } diff --git a/js/features/panel/views/headerView.js b/js/features/panel/views/headerView.js index 03b474d7..af647e1f 100644 --- a/js/features/panel/views/headerView.js +++ b/js/features/panel/views/headerView.js @@ -236,6 +236,15 @@ export function createHeaderView() { window.addEventListener(EVENTS.MFV_VISIBILITY_CHANGED, _mfvVisibilityHandler); headerTools.appendChild(mfvBtn); + const messageBtn = createIconButton("pi-info-circle", t("tooltip.openMessages", "Messages and updates")); + messageBtn.classList.add("mjr-message-btn"); + const messageBadge = document.createElement("span"); + messageBadge.className = "mjr-message-badge"; + messageBadge.style.display = "none"; + messageBadge.setAttribute("aria-hidden", "true"); + messageBtn.appendChild(messageBadge); + headerTools.appendChild(messageBtn); + headerRow.appendChild(headerLeft); headerRow.appendChild(headerActions); header.appendChild(headerRow); @@ -287,6 +296,7 @@ export function createHeaderView() { collectionsBtn, pinnedFoldersBtn, mfvBtn, + messageBtn, dispose, }; } diff --git a/js/features/panel/views/iconButton.js b/js/features/panel/views/iconButton.js index 41e462d1..16da8e5a 100644 --- a/js/features/panel/views/iconButton.js +++ b/js/features/panel/views/iconButton.js @@ -3,8 +3,10 @@ export function createIconButton(iconClass, title) { btn.type = "button"; btn.className = "mjr-icon-btn"; btn.title = title; + if (title) btn.setAttribute("aria-label", title); const i = document.createElement("i"); i.className = `pi ${iconClass}`; + i.setAttribute("aria-hidden", "true"); btn.appendChild(i); return btn; } diff --git a/js/features/panel/views/messagePopoverView.js b/js/features/panel/views/messagePopoverView.js new file mode 100644 index 00000000..8dcb3e0c --- /dev/null +++ b/js/features/panel/views/messagePopoverView.js @@ -0,0 +1,73 @@ +import { t } from "../../../app/i18n.js"; + +const GITHUB_REPO_URL = "https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager"; + +export function createMessagePopoverView() { + const messagePopover = document.createElement("div"); + messagePopover.className = "mjr-popover mjr-messages-popover"; + messagePopover.id = "mjr-messages-popover"; + messagePopover.setAttribute("role", "dialog"); + messagePopover.setAttribute("aria-label", t("label.messages", "Messages")); + messagePopover.setAttribute("aria-hidden", "true"); + messagePopover.tabIndex = -1; + messagePopover.style.display = "none"; + messagePopover.style.cssText = "display:none;"; + + const head = document.createElement("div"); + head.className = "mjr-messages-head"; + + const title = document.createElement("div"); + title.className = "mjr-messages-title"; + title.textContent = t("label.messages", "Messages"); + + const actions = document.createElement("div"); + actions.className = "mjr-messages-actions"; + + const starLink = document.createElement("a"); + starLink.className = "mjr-btn mjr-messages-star-link"; + starLink.href = GITHUB_REPO_URL; + starLink.target = "_blank"; + starLink.rel = "noopener noreferrer"; + starLink.title = t("tooltip.starGithub", "Open GitHub and give a star"); + + const starIcon = document.createElement("span"); + starIcon.className = "mjr-messages-star-icon"; + starIcon.textContent = "\u2605"; + starIcon.setAttribute("aria-hidden", "true"); + + const starLabel = document.createElement("span"); + starLabel.className = "mjr-messages-star-label"; + starLabel.textContent = t("btn.giveStar", "Give a star"); + + starLink.appendChild(starIcon); + starLink.appendChild(starLabel); + + const markReadBtn = document.createElement("button"); + markReadBtn.type = "button"; + markReadBtn.className = "mjr-btn mjr-messages-mark-read-btn"; + markReadBtn.textContent = t("btn.markAllRead", "Mark all read"); + markReadBtn.title = t("tooltip.markMessagesRead", "Mark all messages as read"); + + head.appendChild(title); + actions.appendChild(starLink); + actions.appendChild(markReadBtn); + head.appendChild(actions); + + const messageList = document.createElement("div"); + messageList.className = "mjr-messages-list"; + + const emptyState = document.createElement("div"); + emptyState.className = "mjr-messages-empty"; + emptyState.textContent = t("msg.noMessages", "No messages for now."); + + messageList.appendChild(emptyState); + messagePopover.appendChild(head); + messagePopover.appendChild(messageList); + + return { + messagePopover, + messageList, + emptyState, + markReadBtn, + }; +} diff --git a/js/features/panel/views/popoverManager.js b/js/features/panel/views/popoverManager.js index b77808bd..6f375731 100644 --- a/js/features/panel/views/popoverManager.js +++ b/js/features/panel/views/popoverManager.js @@ -17,8 +17,24 @@ export function createPopoverManager(container) { const POPOVER_BOTTOM_SAFETY_PX = 80; const POPOVER_MIN_HEIGHT_PX = 140; + const bindPopoverInteractionGuards = (popover) => { + if (!popover || popover._mjrInteractionGuardsBound) return; + const stopPropagation = (event) => { + try { + event.stopPropagation(); + } catch (e) { console.debug?.(e); } + }; + try { + popover.addEventListener("wheel", stopPropagation, { passive: true }); + popover.addEventListener("mousedown", stopPropagation); + popover.addEventListener("touchmove", stopPropagation, { passive: true }); + popover._mjrInteractionGuardsBound = true; + } catch (e) { console.debug?.(e); } + }; + const attachPopoverToPortal = (popover) => { if (!popover || !(popover instanceof HTMLElement)) return; + bindPopoverInteractionGuards(popover); if (!popover._mjrPortal) { popover._mjrPortal = { parent: popover.parentNode, @@ -90,6 +106,7 @@ export function createPopoverManager(container) { const availableBelow = bottomLimit - top; popover.style.maxHeight = `${Math.max(POPOVER_MIN_HEIGHT_PX, availableBelow)}px`; popover.style.overflow = "auto"; + popover.style.overscrollBehavior = "contain"; popover.style.left = `${Math.round(left)}px`; popover.style.top = `${Math.round(top)}px`; diff --git a/js/features/panel/views/summaryBarView.js b/js/features/panel/views/summaryBarView.js index 721cd54b..08222723 100644 --- a/js/features/panel/views/summaryBarView.js +++ b/js/features/panel/views/summaryBarView.js @@ -11,6 +11,7 @@ const _safeText = (v) => { const _titleScope = (scope) => { const s = String(scope || "").toLowerCase(); + if (s === "similar") return t("scope.similar", "Similar"); if (s === "input" || s === "inputs") return t("scope.input"); if (s === "custom") return t("scope.customBrowser"); if (s === "all") return t("tab.all"); @@ -108,7 +109,8 @@ export function createSummaryBarView() { Math.max(0, Number(state?.lastGridCount ?? 0) || 0) || cardsCount; - const scope = _titleScope(state?.scope || gridContainer?.dataset?.mjrScope || "output"); + const activeScope = String(state?.viewScope || state?.scope || gridContainer?.dataset?.mjrScope || "output").toLowerCase(); + const scope = _titleScope(activeScope); const countPart = total && total >= shown ? `${shown}/${total}` : `${shown}`; const primaryLabel = @@ -127,21 +129,26 @@ export function createSummaryBarView() { } catch (e) { console.debug?.(e); } try { - const alert = context?.duplicatesAlert || null; - const exact = Number(alert?.exactCount || 0) || 0; - const similar = Number(alert?.similarCount || 0) || 0; - const hasAlert = exact > 0 || similar > 0; - if (!hasAlert) { + if (activeScope === "similar") { dupAlert.style.display = "none"; dupAlert.onclick = null; } else { - dupAlert.style.display = ""; - dupAlert.textContent = `${t("summary.duplicates")}: ${exact} | ${t("summary.similar")}: ${similar}`; - dupAlert.onclick = () => { - try { - actions?.onDuplicateAlertClick?.(alert); - } catch (e) { console.debug?.(e); } - }; + const alert = context?.duplicatesAlert || null; + const exact = Number(alert?.exactCount || 0) || 0; + const similar = Number(alert?.similarCount || 0) || 0; + const hasAlert = exact > 0 || similar > 0; + if (!hasAlert) { + dupAlert.style.display = "none"; + dupAlert.onclick = null; + } else { + dupAlert.style.display = ""; + dupAlert.textContent = `${t("summary.duplicates")}: ${exact} | ${t("summary.similar")}: ${similar}`; + dupAlert.onclick = () => { + try { + actions?.onDuplicateAlertClick?.(alert); + } catch (e) { console.debug?.(e); } + }; + } } } catch (e) { console.debug?.(e); } diff --git a/js/features/panel/views/tabsView.js b/js/features/panel/views/tabsView.js index 2003f390..f9e99179 100644 --- a/js/features/panel/views/tabsView.js +++ b/js/features/panel/views/tabsView.js @@ -18,14 +18,17 @@ export function createTabsView() { const tabInputs = makeTab(t("tab.input"), "input", t("tooltip.tab.input")); const tabOutputs = makeTab(t("tab.output"), "output", t("tooltip.tab.output")); const tabCustom = makeTab(t("tab.custom"), "custom", t("tooltip.tab.custom")); + const tabSimilar = makeTab(t("tab.similar", "Similar"), "similar", t("tooltip.tab.similar", "Browse current similar findings")); + tabSimilar.style.display = "none"; tabs.appendChild(tabAll); tabs.appendChild(tabInputs); tabs.appendChild(tabOutputs); tabs.appendChild(tabCustom); + tabs.appendChild(tabSimilar); return { tabs, - tabButtons: { tabAll, tabInputs, tabOutputs, tabCustom } + tabButtons: { tabAll, tabInputs, tabOutputs, tabCustom, tabSimilar } }; } diff --git a/js/features/status/StatusDot.js b/js/features/status/StatusDot.js index 00f753bf..d1dcb7cc 100644 --- a/js/features/status/StatusDot.js +++ b/js/features/status/StatusDot.js @@ -5,6 +5,7 @@ import { get, getToolsStatus, post, resetIndex, getWatcherStatus, forceDeleteDb, listDbBackups, saveDbBackup, restoreDbBackup, vectorBackfill } from "../../api/client.js"; import { ENDPOINTS } from "../../api/endpoints.js"; import { APP_CONFIG } from "../../app/config.js"; +import { comfyConfirm } from "../../app/dialogs.js"; import { comfyToast } from "../../app/toast.js"; import { t } from "../../app/i18n.js"; import { getEnrichmentState, setEnrichmentState } from "../../app/runtimeState.js"; @@ -180,6 +181,16 @@ function emitGlobalGridReload(reason = "status-action") { } catch (e) { console.debug?.(e); } } +async function askKeepVectors(actionLabel = "") { + return await comfyConfirm( + t( + "dialog.vectorsReset.keepQuestion", + "Keep existing AI vectors?\n\nConfirm = keep vectors\nCancel = continue without vectors" + ), + actionLabel || t("dialog.vectorsReset.title", "AI vectors") + ); +} + /** * Create status indicator section */ @@ -362,7 +373,10 @@ export function createStatusIndicator(options = {}) { comfyToast(t("toast.dbRestoreSelect", "Select a DB backup first"), "warning", 2000); return; } - const confirmed = confirm(t("dialog.dbRestore.confirm", "Restore selected DB backup? Current DB will be replaced.")); + const confirmed = await comfyConfirm( + t("dialog.dbRestore.confirm", "Restore selected DB backup? Current DB will be replaced."), + t("btn.dbRestore", "Restore DB") + ); if (!confirmed) return; comfyToast(t("toast.dbRestoreStarted", "DB restore started"), "info", 1800); setMaintenanceActive(true); @@ -453,6 +467,17 @@ export function createStatusIndicator(options = {}) { }; resetBtn.onclick = async (event) => { event.stopPropagation(); + const preserveVectors = await askKeepVectors(t("btn.resetIndex", "Reset index")); + const confirmed = await comfyConfirm( + preserveVectors + ? t( + "dialog.resetIndex.confirmKeepVectors", + "This will reset index data and rescan files while keeping existing AI vectors.\n\nContinue?" + ) + : t("dialog.resetIndex.msg", "This will delete the database and rescan all files. Continue?"), + t("dialog.resetIndex.title", "Reset index?") + ); + if (!confirmed) return; setMaintenanceActive(true); const originalText = resetBtn.textContent; @@ -476,11 +501,12 @@ export function createStatusIndicator(options = {}) { customRootId: isCustomBrowserMode ? null : customRootId, reindex: true, maintenance_force: true, - hard_reset_db: isAll, + hard_reset_db: isAll && !preserveVectors, clear_scan_journal: true, clear_metadata_cache: true, clear_asset_metadata: true, - clear_assets: true, + clear_assets: !preserveVectors, + preserve_vectors: preserveVectors, rebuild_fts: true, }); if (res?.ok) { @@ -593,7 +619,11 @@ export function createStatusIndicator(options = {}) { const errors = Number(res?.data?.errors ?? progress?.errors ?? 0); if (pending) { const jobId = String(res?.data?.job_id || "").trim(); - const msg = `Vector backfill is still running in background${jobId ? ` (job ${jobId.slice(0, 8)})` : ""}.`; + const msg = t( + "toast.vectorBackfillRunning", + "Vector backfill still running in background{job}.", + { job: jobId ? ` (job ${jobId.slice(0, 8)})` : "" } + ); comfyToast(msg, "info", 4200); setActionLog( `Backfill running in background โ€” candidates ${processed}, indexed ${indexed}, skipped ${skipped}, errors ${errors}`, @@ -602,13 +632,21 @@ export function createStatusIndicator(options = {}) { statusDot.style.background = "var(--mjr-status-info, #64B5F6)"; applyStatusHighlight(section, "info"); } else { - comfyToast(`Vector backfill done โ€” processed ${processed}, indexed ${indexed}, skipped ${skipped}`, "success", 3000); + comfyToast( + t( + "toast.vectorBackfillComplete", + "Vector backfill complete! Processed: {processed}, Indexed: {indexed}, Skipped: {skipped}", + { processed, indexed, skipped } + ), + "success", + 3000 + ); setActionLog(`Backfill OK โ€” processed ${processed}, indexed ${indexed}, skipped ${skipped}`, "success"); statusDot.style.background = "var(--mjr-status-success, #4CAF50)"; applyStatusHighlight(section, "success"); } } else { - const err = String(res?.error || "Backfill failed"); + const err = String(res?.error || t("toast.vectorBackfillFailedGeneric", "Backfill failed")); const code = String(res?.code || "").trim(); const status = Number(res?.status || 0) || 0; const detail = [ @@ -671,21 +709,45 @@ export function createStatusIndicator(options = {}) { }; deleteDbBtn.onclick = async (event) => { event.stopPropagation(); - - const confirmed = confirm(t("dialog.dbDelete.confirm")); + const preserveVectors = await askKeepVectors(t("btn.deleteDb", "Delete DB")); + const confirmed = await comfyConfirm( + preserveVectors + ? t( + "dialog.dbDelete.keepVectorsConfirm", + "This will reset index data and keep existing AI vectors. Database files will not be force-deleted.\n\nContinue?" + ) + : t( + "dialog.dbDelete.confirm", + "This will permanently delete the index database and rebuild it from scratch. All ratings, tags, and cached metadata will be lost.\n\nContinue?" + ), + t("btn.deleteDb", "Delete DB") + ); if (!confirmed) return; setMaintenanceActive(true); const originalText = deleteDbBtn.textContent; deleteDbBtn.disabled = true; - deleteDbBtn.textContent = t("btn.deletingDb"); + deleteDbBtn.textContent = preserveVectors ? t("btn.resetting", "Resetting...") : t("btn.deletingDb"); resetBtn.disabled = true; statusDot.style.background = "var(--mjr-status-info, #64B5F6)"; applyStatusHighlight(section, "info"); try { - const res = await forceDeleteDb(); + const res = preserveVectors + ? await resetIndex({ + scope: "all", + reindex: true, + maintenance_force: true, + hard_reset_db: false, + clear_scan_journal: true, + clear_metadata_cache: true, + clear_asset_metadata: true, + clear_assets: false, + preserve_vectors: true, + rebuild_fts: true, + }) + : await forceDeleteDb(); if (res?.ok) { globalThis._mjrCorruptToastShown = false; statusDot.style.background = "var(--mjr-status-success, #4CAF50)"; @@ -702,7 +764,7 @@ export function createStatusIndicator(options = {}) { deleteDbBtn.disabled = false; deleteDbBtn.textContent = originalText; resetBtn.disabled = false; - emitGlobalGridReload("db-force-delete"); + emitGlobalGridReload(preserveVectors ? "index-reset-preserve-vectors" : "db-force-delete"); try { const target = getScanContext ? getScanContext() : null; await updateStatus(statusDot, statusText, capabilities, target, null, { force: true }); diff --git a/js/features/viewer/FloatingViewer.js b/js/features/viewer/FloatingViewer.js index 443d7274..f5737750 100644 --- a/js/features/viewer/FloatingViewer.js +++ b/js/features/viewer/FloatingViewer.js @@ -24,6 +24,7 @@ const MFV_ZOOM_MIN = 0.25; const MFV_ZOOM_MAX = 8; const MFV_ZOOM_FACTOR = 0.0008; // multiplied by deltaY per wheel tick const MFV_RESIZE_EDGE_HIT_PX = 8; +let _mfvInstanceSeq = 0; // Media extensions for explicit kind detection. const VIDEO_EXTS = new Set([".mp4", ".webm", ".mov", ".avi", ".mkv"]); @@ -184,10 +185,13 @@ function _canvasLabel(ctx, text, x, y) { export class FloatingViewer { constructor() { + this._instanceId = ++_mfvInstanceSeq; this.element = null; this.isVisible = false; this._contentEl = null; + this._closeBtn = null; this._modeBtn = null; + this._pinSelect = null; this._liveBtn = null; this._genBtn = null; this._genDropdown = null; @@ -196,6 +200,7 @@ export class FloatingViewer { this._mode = MFV_MODES.SIMPLE; this._mediaA = null; this._mediaB = null; + this._pinnedSlot = null; this._abDividerX = 0.5; // 0..1 // Pan/zoom state @@ -207,7 +212,7 @@ export class FloatingViewer { // AbortController for toolbar/header button click listeners (NM-1). // Aborted in dispose() so listeners are cleaned up without needing named references. - this._btnAC = new AbortController(); + this._btnAC = null; // Generation counter: incremented on every loadMediaA/loadMediaPair call so // stale async metadata enrichment results can be discarded (NM-2). this._refreshGen = 0; @@ -216,6 +221,9 @@ export class FloatingViewer { this._popoutWindow = null; this._popoutBtn = null; this._isPopped = false; + this._popoutCloseHandler = null; + this._popoutCloseTimer = null; + this._popoutRestoreGuard = false; // Preview stream state: button ref + last blob URL for cleanup. this._previewBtn = null; @@ -225,6 +233,10 @@ export class FloatingViewer { // Panel-level listeners and edge-resize state. this._panelAC = new AbortController(); this._resizeState = null; + this._titleId = `mjr-mfv-title-${this._instanceId}`; + this._genDropdownId = `mjr-mfv-gen-dropdown-${this._instanceId}`; + this._docClickHost = null; + this._handleDocClick = null; } // โ”€โ”€ Build DOM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -232,17 +244,22 @@ export class FloatingViewer { render() { const el = document.createElement("div"); el.className = "mjr-mfv"; + el.setAttribute("role", "dialog"); + el.setAttribute("aria-modal", "false"); + el.setAttribute("aria-hidden", "true"); this.element = el; el.appendChild(this._buildHeader()); + el.setAttribute("aria-labelledby", this._titleId); el.appendChild(this._buildToolbar()); this._contentEl = document.createElement("div"); this._contentEl.className = "mjr-mfv-content"; el.appendChild(this._contentEl); - this._initEdgeResize(el); - this._initDrag(el.querySelector(".mjr-mfv-header")); + this._rebindControlHandlers(); + this._bindPanelInteractions(); + this._bindDocumentUiHandlers(); this._refresh(); return el; } @@ -253,19 +270,20 @@ export class FloatingViewer { const title = document.createElement("span"); title.className = "mjr-mfv-header-title"; + title.id = this._titleId; title.textContent = "Majoor Viewer Lite"; const closeBtn = document.createElement("button"); + this._closeBtn = closeBtn; closeBtn.type = "button"; closeBtn.className = "mjr-icon-btn"; - closeBtn.title = t("tooltip.closeViewer", "Close viewer"); + const closeLabel = t("tooltip.closeViewer", "Close viewer"); + closeBtn.title = closeLabel; + closeBtn.setAttribute("aria-label", closeLabel); const _closeBtnIcon = document.createElement("i"); _closeBtnIcon.className = "pi pi-times"; _closeBtnIcon.setAttribute("aria-hidden", "true"); closeBtn.appendChild(_closeBtnIcon); - closeBtn.addEventListener("click", () => { - window.dispatchEvent(new CustomEvent(EVENTS.MFV_CLOSE)); - }, { signal: this._btnAC.signal }); header.appendChild(title); header.appendChild(closeBtn); @@ -280,10 +298,26 @@ export class FloatingViewer { this._modeBtn = document.createElement("button"); this._modeBtn.type = "button"; this._modeBtn.className = "mjr-icon-btn"; - this._modeBtn.addEventListener("click", () => this._cycleMode(), { signal: this._btnAC.signal }); this._updateModeBtnUI(); bar.appendChild(this._modeBtn); + this._pinSelect = document.createElement("select"); + this._pinSelect.className = "mjr-mfv-pin-select"; + this._pinSelect.setAttribute("aria-label", "Pin Reference"); + this._pinSelect.value = this._pinnedSlot || ""; + for (const { value, label } of [ + { value: "", label: "No Pin" }, + { value: "A", label: "Pin A" }, + { value: "B", label: "Pin B" }, + ]) { + const option = document.createElement("option"); + option.value = value; + option.textContent = label; + this._pinSelect.appendChild(option); + } + this._updatePinSelectUI(); + bar.appendChild(this._pinSelect); + // Separator const sep = document.createElement("div"); sep.className = "mjr-mfv-toolbar-sep"; @@ -294,40 +328,32 @@ export class FloatingViewer { this._liveBtn = document.createElement("button"); this._liveBtn.type = "button"; this._liveBtn.className = "mjr-icon-btn"; - this._liveBtn.title = t("tooltip.liveStreamOff", "Live Stream: OFF โ€” click to follow"); this._liveBtn.innerHTML = ''; - this._liveBtn.addEventListener("click", () => { - window.dispatchEvent(new CustomEvent(EVENTS.MFV_LIVE_TOGGLE)); - }, { signal: this._btnAC.signal }); + this._liveBtn.setAttribute("aria-pressed", "false"); + this._liveBtn.setAttribute("aria-label", t("tooltip.liveStreamOff", "Live Stream: OFF โ€” click to follow")); + this._liveBtn.title = t("tooltip.liveStreamOff", "Live Stream: OFF โ€” click to follow"); bar.appendChild(this._liveBtn); // KSampler Preview Stream toggle this._previewBtn = document.createElement("button"); this._previewBtn.type = "button"; this._previewBtn.className = "mjr-icon-btn"; - this._previewBtn.title = t("tooltip.previewStreamOff", "KSampler Preview: OFF โ€” click to stream denoising steps"); this._previewBtn.innerHTML = ''; - this._previewBtn.addEventListener("click", () => { - window.dispatchEvent(new CustomEvent(EVENTS.MFV_PREVIEW_TOGGLE)); - }, { signal: this._btnAC.signal }); + this._previewBtn.setAttribute("aria-pressed", "false"); + this._previewBtn.setAttribute("aria-label", t("tooltip.previewStreamOff", "KSampler Preview: OFF โ€” click to stream denoising steps")); + this._previewBtn.title = t("tooltip.previewStreamOff", "KSampler Preview: OFF โ€” click to stream denoising steps"); bar.appendChild(this._previewBtn); // Gen Info button (shows dropdown with checkboxes) this._genBtn = document.createElement("button"); this._genBtn.type = "button"; this._genBtn.className = "mjr-icon-btn"; + this._genBtn.setAttribute("aria-haspopup", "dialog"); + this._genBtn.setAttribute("aria-expanded", "false"); const _genBtnIcon = document.createElement("i"); _genBtnIcon.className = "pi pi-info-circle"; _genBtnIcon.setAttribute("aria-hidden", "true"); this._genBtn.appendChild(_genBtnIcon); - this._genBtn.addEventListener("click", (e) => { - e.stopPropagation(); - if (this._genDropdown?.classList?.contains("is-visible")) { - this._closeGenDropdown(); - } else { - this._openGenDropdown(); - } - }, { signal: this._btnAC.signal }); bar.appendChild(this._genBtn); this._updateGenBtnUI(); @@ -335,46 +361,117 @@ export class FloatingViewer { this._popoutBtn = document.createElement("button"); this._popoutBtn.type = "button"; this._popoutBtn.className = "mjr-icon-btn"; - this._popoutBtn.title = t("tooltip.popOutViewer", "Pop out to separate window (can move to 2nd monitor)"); + const popoutLabel = t("tooltip.popOutViewer", "Pop out to separate window (can move to 2nd monitor)"); + this._popoutBtn.title = popoutLabel; + this._popoutBtn.setAttribute("aria-label", popoutLabel); + this._popoutBtn.setAttribute("aria-pressed", "false"); const _popoutIcon = document.createElement("i"); _popoutIcon.className = "pi pi-external-link"; _popoutIcon.setAttribute("aria-hidden", "true"); this._popoutBtn.appendChild(_popoutIcon); - this._popoutBtn.addEventListener("click", () => { - window.dispatchEvent(new CustomEvent(EVENTS.MFV_POPOUT)); - }, { signal: this._btnAC.signal }); bar.appendChild(this._popoutBtn); // Download / capture button this._captureBtn = document.createElement("button"); this._captureBtn.type = "button"; this._captureBtn.className = "mjr-icon-btn"; - this._captureBtn.title = t("tooltip.captureView", "Save view as image"); + const captureLabel = t("tooltip.captureView", "Save view as image"); + this._captureBtn.title = captureLabel; + this._captureBtn.setAttribute("aria-label", captureLabel); const _captureBtnIcon = document.createElement("i"); _captureBtnIcon.className = "pi pi-download"; _captureBtnIcon.setAttribute("aria-hidden", "true"); this._captureBtn.appendChild(_captureBtnIcon); - this._captureBtn.addEventListener("click", () => this._captureView(), { signal: this._btnAC.signal }); bar.appendChild(this._captureBtn); - // Close dropdown when clicking outside this._handleDocClick = (ev) => { if (!this._genDropdown) return; - if (ev.target === this._genBtn) return; - if (this._genDropdown.contains(ev.target)) return; + const target = ev?.target; + if (this._genBtn?.contains?.(target)) return; + if (this._genDropdown.contains(target)) return; this._closeGenDropdown(); }; - document.addEventListener("click", this._handleDocClick); + this._bindDocumentUiHandlers(); return bar; } + _rebindControlHandlers() { + try { this._btnAC?.abort(); } catch (e) { console.debug?.(e); } + this._btnAC = new AbortController(); + const signal = this._btnAC.signal; + + this._closeBtn?.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent(EVENTS.MFV_CLOSE)); + }, { signal }); + + this._modeBtn?.addEventListener("click", () => this._cycleMode(), { signal }); + + this._pinSelect?.addEventListener("change", (e) => { + this._pinnedSlot = e?.target?.value || null; + if (this._pinnedSlot && this._mode === MFV_MODES.SIMPLE) { + this.setMode(MFV_MODES.AB); + } + this._updatePinSelectUI(); + }, { signal }); + + this._liveBtn?.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent(EVENTS.MFV_LIVE_TOGGLE)); + }, { signal }); + + this._previewBtn?.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent(EVENTS.MFV_PREVIEW_TOGGLE)); + }, { signal }); + + this._genBtn?.addEventListener("click", (e) => { + e.stopPropagation(); + if (this._genDropdown?.classList?.contains("is-visible")) { + this._closeGenDropdown(); + } else { + this._openGenDropdown(); + } + }, { signal }); + + this._popoutBtn?.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent(EVENTS.MFV_POPOUT)); + }, { signal }); + + this._captureBtn?.addEventListener("click", () => this._captureView(), { signal }); + } + + _resetGenDropdownForCurrentDocument() { + this._closeGenDropdown(); + try { this._genDropdown?.remove?.(); } catch (e) { console.debug?.(e); } + this._genDropdown = null; + this._updateGenBtnUI(); + } + + _bindDocumentUiHandlers() { + if (!this._handleDocClick) return; + const doc = this.element?.ownerDocument || document; + if (this._docClickHost === doc) return; + this._unbindDocumentUiHandlers(); + doc.addEventListener("click", this._handleDocClick); + this._docClickHost = doc; + } + + _unbindDocumentUiHandlers() { + if (!this._docClickHost || !this._handleDocClick) return; + try { this._docClickHost.removeEventListener("click", this._handleDocClick); } catch (e) { console.debug?.(e); } + this._docClickHost = null; + } + + _isGenDropdownOpen() { + return !!this._genDropdown?.classList?.contains("is-visible"); + } + _openGenDropdown() { if (!this.element) return; if (!this._genDropdown) { this._genDropdown = this._buildGenDropdown(); this.element.appendChild(this._genDropdown); } + this._bindDocumentUiHandlers(); const rect = this._genBtn.getBoundingClientRect(); const parentRect = this.element.getBoundingClientRect(); const left = rect.left - parentRect.left; @@ -382,11 +479,13 @@ export class FloatingViewer { this._genDropdown.style.left = `${left}px`; this._genDropdown.style.top = `${top}px`; this._genDropdown.classList.add("is-visible"); + this._updateGenBtnUI(); } _closeGenDropdown() { if (!this._genDropdown) return; this._genDropdown.classList.remove("is-visible"); + this._updateGenBtnUI(); } /** Reflect how many fields are enabled on the gen info button. */ @@ -394,15 +493,28 @@ export class FloatingViewer { if (!this._genBtn) return; const count = this._genInfoSelections.size; const isOn = count > 0; + const isOpen = this._isGenDropdownOpen(); this._genBtn.classList.toggle("is-on", isOn); - this._genBtn.title = isOn - ? `Gen Info (${count} field${count > 1 ? "s" : ""} shown) โ€” click to configure` - : "Gen Info โ€” click to show overlay"; + this._genBtn.classList.toggle("is-open", isOpen); + const label = isOn + ? `Gen Info (${count} field${count > 1 ? "s" : ""} shown)${isOpen ? " โ€” open" : " โ€” click to configure"}` + : `Gen Info${isOpen ? " โ€” open" : " โ€” click to show overlay"}`; + this._genBtn.title = label; + this._genBtn.setAttribute("aria-label", label); + this._genBtn.setAttribute("aria-expanded", String(isOpen)); + if (this._genDropdown) { + this._genBtn.setAttribute("aria-controls", this._genDropdownId); + } else { + this._genBtn.removeAttribute("aria-controls"); + } } _buildGenDropdown() { const d = document.createElement("div"); d.className = "mjr-mfv-gen-dropdown"; + d.id = this._genDropdownId; + d.setAttribute("role", "group"); + d.setAttribute("aria-label", "Generation info fields"); const opts = [ ["prompt", "Prompt"], ["seed", "Seed"], @@ -552,6 +664,22 @@ export class FloatingViewer { this._refresh(); } + getPinnedSlot() { + return this._pinnedSlot; + } + + _updatePinSelectUI() { + if (!this._pinSelect) return; + const pinned = this._pinnedSlot === "A" || this._pinnedSlot === "B"; + this._pinSelect.value = this._pinnedSlot || ""; + this._pinSelect.classList.toggle("is-pinned", pinned); + const label = pinned + ? `Pin Reference: ${this._pinnedSlot}` + : "Pin Reference: Off"; + this._pinSelect.title = label; + this._pinSelect.setAttribute("aria-label", label); + } + _updateModeBtnUI() { if (!this._modeBtn) return; const cfg = { @@ -565,25 +693,33 @@ export class FloatingViewer { _modeBtnIcon.setAttribute("aria-hidden", "true"); this._modeBtn.replaceChildren(_modeBtnIcon); this._modeBtn.title = label; + this._modeBtn.setAttribute("aria-label", label); + this._modeBtn.removeAttribute("aria-pressed"); } // โ”€โ”€ Live Stream UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ setLiveActive(active) { if (!this._liveBtn) return; - this._liveBtn.classList.toggle("mjr-live-active", Boolean(active)); - if (active) { + const isActive = Boolean(active); + this._liveBtn.classList.toggle("mjr-live-active", isActive); + const label = isActive + ? t("tooltip.liveStreamOn", "Live Stream: ON โ€” click to disable") + : t("tooltip.liveStreamOff", "Live Stream: OFF โ€” click to follow"); + this._liveBtn.setAttribute("aria-pressed", String(isActive)); + this._liveBtn.setAttribute("aria-label", label); + if (isActive) { const _liveIconActive = document.createElement("i"); _liveIconActive.className = "pi pi-circle-fill"; _liveIconActive.setAttribute("aria-hidden", "true"); this._liveBtn.replaceChildren(_liveIconActive); - this._liveBtn.title = t("tooltip.liveStreamOn", "Live Stream: ON โ€” click to disable"); + this._liveBtn.title = label; } else { const _liveIconInactive = document.createElement("i"); _liveIconInactive.className = "pi pi-circle"; _liveIconInactive.setAttribute("aria-hidden", "true"); this._liveBtn.replaceChildren(_liveIconInactive); - this._liveBtn.title = t("tooltip.liveStreamOff", "Live Stream: OFF โ€” click to follow"); + this._liveBtn.title = label; } } @@ -592,19 +728,24 @@ export class FloatingViewer { setPreviewActive(active) { this._previewActive = Boolean(active); if (!this._previewBtn) return; - this._previewBtn.classList.toggle("mjr-live-active", this._previewActive); + this._previewBtn.classList.toggle("mjr-preview-active", this._previewActive); + const label = this._previewActive + ? t("tooltip.previewStreamOn", "KSampler Preview: ON โ€” streaming denoising steps") + : t("tooltip.previewStreamOff", "KSampler Preview: OFF โ€” click to stream denoising steps"); + this._previewBtn.setAttribute("aria-pressed", String(this._previewActive)); + this._previewBtn.setAttribute("aria-label", label); if (this._previewActive) { const icon = document.createElement("i"); icon.className = "pi pi-eye"; icon.setAttribute("aria-hidden", "true"); this._previewBtn.replaceChildren(icon); - this._previewBtn.title = t("tooltip.previewStreamOn", "KSampler Preview: ON โ€” streaming denoising steps"); + this._previewBtn.title = label; } else { const icon = document.createElement("i"); icon.className = "pi pi-eye-slash"; icon.setAttribute("aria-hidden", "true"); this._previewBtn.replaceChildren(icon); - this._previewBtn.title = t("tooltip.previewStreamOff", "KSampler Preview: OFF โ€” click to stream denoising steps"); + this._previewBtn.title = label; // Revoke last blob URL when turning off this._revokePreviewBlob(); } @@ -1024,7 +1165,9 @@ export class FloatingViewer { show() { if (!this.element) return; + this._bindDocumentUiHandlers(); this.element.classList.add("is-visible"); + this.element.setAttribute("aria-hidden", "false"); this.isVisible = true; } @@ -1034,7 +1177,9 @@ export class FloatingViewer { // even if hide() is called mid-drag (NM-5). this._destroyPanZoom(); this._stopEdgeResize(); + this._closeGenDropdown(); this.element.classList.remove("is-visible"); + this.element.setAttribute("aria-hidden", "true"); this.isVisible = false; } @@ -1042,8 +1187,8 @@ export class FloatingViewer { /** * Move the viewer into a separate browser window so it can be - * dragged onto a second monitor. Uses adoptNode() to transfer - * the live DOM tree โ€” all JS references and listeners stay valid. + * dragged onto a second monitor. Opens a dedicated same-origin page, + * then adopts the live DOM tree into that page so state/listeners persist. */ popOut() { if (this._isPopped || !this.element) return; @@ -1058,34 +1203,201 @@ export class FloatingViewer { const left = (window.screenX || window.screenLeft) + Math.round((window.outerWidth - w) / 2); const top = (window.screenY || window.screenTop) + Math.round((window.outerHeight - h) / 2); const features = `width=${w},height=${h},left=${left},top=${top},resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no`; - const popup = window.open("", "_mjr_viewer", features); + const popup = window.open(this._getPopoutUrl(), "_mjr_viewer", features); if (!popup) { console.warn("[MFV] Pop-out blocked โ€” allow popups for this site."); return; } this._popoutWindow = popup; this._isPopped = true; + this._popoutRestoreGuard = false; + let mounted = false; + const handlePopupClosing = () => this._schedulePopInFromPopupClose(); + this._popoutCloseHandler = handlePopupClosing; - // ----- Build the popup document ----- - const doc = popup.document; - doc.title = "Majoor Viewer"; + const mountViewer = () => { + if (mounted) return; + const doc = popup.document; + if (!doc) return; + if (!this._ensurePopoutShell(doc)) return; + doc.title = "Majoor Viewer"; + this._installPopoutStyles(doc); + const root = doc.getElementById("mjr-mfv-popout-root") || doc.body; + if (!root) return; + try { root.replaceChildren(); } catch (e) { console.debug?.(e); } + root.appendChild(doc.adoptNode(el)); + el.classList.add("is-visible"); + this.isVisible = true; + mounted = true; + this._resetGenDropdownForCurrentDocument(); + this._rebindControlHandlers(); + this._bindDocumentUiHandlers(); + this._updatePopoutBtnUI(); + }; - // Copy ALL stylesheets from the parent page so themes, PrimeIcons, - // and MFV-specific CSS are available in the popup. - for (const ss of document.querySelectorAll('link[rel="stylesheet"], style')) { - doc.head.appendChild(doc.importNode(ss, true)); + try { + popup.addEventListener("load", mountViewer, { once: true }); + } catch (e) { + console.debug?.("[MFV] pop-out page mount failed", e); + } + if (popup.document?.readyState === "interactive" || popup.document?.readyState === "complete") { + setTimeout(mountViewer, 0); } - // Override styles for the popped-out viewer (fill window, no fixed position) - const overrideStyle = doc.createElement("style"); - overrideStyle.textContent = ` - html, body { - margin: 0; padding: 0; - width: 100%; height: 100%; - overflow: hidden; - background: #1e1e1e; - color: #ddd; + // When the user closes the popup window, automatically pop the viewer back in. + popup.addEventListener("beforeunload", handlePopupClosing); + popup.addEventListener("pagehide", handlePopupClosing); + popup.addEventListener("unload", handlePopupClosing); + this._startPopoutCloseWatch(); + + // Relay keyboard shortcuts from popup back to main window + popup.addEventListener("keydown", (e) => { + const tag = String(e?.target?.tagName || "").toLowerCase(); + if (e?.defaultPrevented || e?.target?.isContentEditable || tag === "input" || tag === "textarea" || tag === "select") { + return; } + window.dispatchEvent(new KeyboardEvent("keydown", { + key: e.key, code: e.code, keyCode: e.keyCode, + ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey, + })); + }); + } + + _clearPopoutCloseWatch() { + if (this._popoutCloseTimer == null) return; + try { window.clearInterval(this._popoutCloseTimer); } catch (e) { console.debug?.(e); } + this._popoutCloseTimer = null; + } + + _startPopoutCloseWatch() { + this._clearPopoutCloseWatch(); + this._popoutCloseTimer = window.setInterval(() => { + if (!this._isPopped) { + this._clearPopoutCloseWatch(); + return; + } + const popup = this._popoutWindow; + if (!popup || popup.closed) { + this._clearPopoutCloseWatch(); + this._schedulePopInFromPopupClose(); + } + }, 250); + } + + _schedulePopInFromPopupClose() { + if (!this._isPopped || this._popoutRestoreGuard) return; + this._popoutRestoreGuard = true; + window.setTimeout(() => { + try { + this.popIn({ closePopupWindow: false }); + } finally { + this._popoutRestoreGuard = false; + } + }, 0); + } + + _getPopoutUrl() { + try { + return new URL("/mjr/viewer/popout", window.location.href).toString(); + } catch { + return "/mjr/viewer/popout"; + } + } + + _buildPopoutShellHtml() { + return ` + + + + + + Majoor Viewer + + + +
+
Preparing viewer...
+
+ +`; + } + + _ensurePopoutShell(doc) { + if (!doc) return false; + try { + if (doc.getElementById("mjr-mfv-popout-root")) return true; + doc.open(); + doc.write(this._buildPopoutShellHtml()); + doc.close(); + return !!doc.getElementById("mjr-mfv-popout-root"); + } catch (e) { + console.debug?.("[MFV] pop-out shell init failed", e); + return false; + } + } + + _installPopoutStyles(doc) { + if (!doc?.head) return; + try { + for (const existing of doc.head.querySelectorAll("[data-mjr-popout-cloned-style='1']")) { + existing.remove(); + } + } catch (e) { console.debug?.(e); } + for (const ss of document.querySelectorAll('link[rel="stylesheet"], style')) { + try { + let clone = null; + if (ss.tagName === "LINK") { + clone = doc.createElement("link"); + for (const attr of Array.from(ss.attributes || [])) { + if (attr?.name === "href") continue; + clone.setAttribute(attr.name, attr.value); + } + clone.setAttribute("href", ss.href || ss.getAttribute("href") || ""); + } else { + clone = doc.importNode(ss, true); + } + clone.setAttribute("data-mjr-popout-cloned-style", "1"); + doc.head.appendChild(clone); + } catch (e) { console.debug?.(e); } + } + const overrideStyle = doc.createElement("style"); + overrideStyle.setAttribute("data-mjr-popout-cloned-style", "1"); + overrideStyle.textContent = ` .mjr-mfv { position: static !important; width: 100% !important; @@ -1100,46 +1412,48 @@ export class FloatingViewer { } `; doc.head.appendChild(overrideStyle); - - // Adopt the existing DOM node into the popup document - doc.body.appendChild(doc.adoptNode(el)); - - // Force visible (it may have been hidden if toggled before pop-out) - el.classList.add("is-visible"); - this.isVisible = true; - - // Update pop-out button to show "pop-in" icon - this._updatePopoutBtnUI(); - - // When the user closes the popup window, automatically pop the viewer back in. - popup.addEventListener("beforeunload", () => this.popIn()); - - // Relay keyboard shortcuts from popup back to main window - popup.addEventListener("keydown", (e) => { - window.dispatchEvent(new KeyboardEvent("keydown", { - key: e.key, code: e.code, keyCode: e.keyCode, - ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey, - })); - }); } /** * Move the viewer back into the main ComfyUI page from the popup window. */ - popIn() { + popIn({ closePopupWindow = true } = {}) { if (!this._isPopped || !this.element) return; + const popup = this._popoutWindow; + const closeHandler = this._popoutCloseHandler; + this._clearPopoutCloseWatch(); + if (popup && closeHandler) { + try { popup.removeEventListener("beforeunload", closeHandler); } catch (e) { console.debug?.(e); } + try { popup.removeEventListener("pagehide", closeHandler); } catch (e) { console.debug?.(e); } + try { popup.removeEventListener("unload", closeHandler); } catch (e) { console.debug?.(e); } + } + this._popoutCloseHandler = null; this._isPopped = false; // Re-adopt the element into the main document and append to body - const adopted = document.adoptNode(this.element); + let adopted = this.element; + try { + adopted = adopted?.ownerDocument === document ? adopted : document.adoptNode(adopted); + } catch (e) { + console.debug?.("[MFV] pop-in adopt failed", e); + } document.body.appendChild(adopted); + this._resetGenDropdownForCurrentDocument(); + this._rebindControlHandlers(); + this._bindPanelInteractions(); + this._bindDocumentUiHandlers(); + adopted.classList.add("is-visible"); + adopted.setAttribute("aria-hidden", "false"); + this.isVisible = true; // Restore inline position styles (they were cleared by the popup override) // The viewer will revert to its normal fixed-position CSS. this._updatePopoutBtnUI(); // Close the popup window if it's still open - try { this._popoutWindow?.close(); } catch (e) { console.debug?.(e); } + if (closePopupWindow) { + try { popup?.close(); } catch (e) { console.debug?.(e); } + } this._popoutWindow = null; } @@ -1151,13 +1465,17 @@ export class FloatingViewer { } this._popoutBtn.classList.toggle("mjr-popin-active", this._isPopped); const icon = this._popoutBtn.querySelector("i") || document.createElement("i"); + const label = this._isPopped + ? t("tooltip.popInViewer", "Return viewer to ComfyUI window") + : t("tooltip.popOutViewer", "Pop out to separate window (can move to 2nd monitor)"); if (this._isPopped) { icon.className = "pi pi-sign-in"; - this._popoutBtn.title = t("tooltip.popInViewer", "Return viewer to ComfyUI window"); } else { icon.className = "pi pi-external-link"; - this._popoutBtn.title = t("tooltip.popOutViewer", "Pop out to separate window (can move to 2nd monitor)"); } + this._popoutBtn.title = label; + this._popoutBtn.setAttribute("aria-label", label); + this._popoutBtn.setAttribute("aria-pressed", String(this._isPopped)); if (!this._popoutBtn.contains(icon)) { this._popoutBtn.replaceChildren(icon); } @@ -1207,6 +1525,15 @@ export class FloatingViewer { this.element.style.cursor = ""; } + _bindPanelInteractions() { + if (!this.element) return; + this._stopEdgeResize(); + try { this._panelAC?.abort(); } catch (e) { console.debug?.(e); } + this._panelAC = new AbortController(); + this._initEdgeResize(this.element); + this._initDrag(this.element.querySelector(".mjr-mfv-header")); + } + _initEdgeResize(el) { if (!el) return; const resolveDir = (e) => { @@ -1319,9 +1646,11 @@ export class FloatingViewer { _initDrag(handle) { if (!handle) return; + const signal = this._panelAC?.signal; handle.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; if (e.target.closest("button")) return; // Don't drag when clicking buttons + if (e.target.closest("select")) return; if (this._isPopped || !this.element) return; // Let edge-resize take precedence when pointer is near panel borders. const edgeDir = this._getResizeDirectionFromPoint( @@ -1352,7 +1681,7 @@ export class FloatingViewer { }; handle.addEventListener("pointermove", onMove); handle.addEventListener("pointerup", onUp); - }); + }, signal ? { signal } : undefined); } // โ”€โ”€ Canvas capture โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -1621,6 +1950,7 @@ export class FloatingViewer { dispose() { this._destroyPanZoom(); this._stopEdgeResize(); + this._clearPopoutCloseWatch(); try { this._panelAC?.abort(); this._panelAC = null; } catch (e) { console.debug?.(e); } // Abort all button click listeners in one call (NM-1). try { this._btnAC?.abort(); this._btnAC = null; } catch (e) { console.debug?.(e); } @@ -1630,11 +1960,13 @@ export class FloatingViewer { try { this.element?.remove(); } catch (e) { console.debug?.(e); } this.element = null; this._contentEl = null; + this._closeBtn = null; this._modeBtn = null; + this._pinSelect = null; this._liveBtn = null; this._popoutBtn = null; this._captureBtn = null; - try { document.removeEventListener("click", this._handleDocClick); } catch (e) { console.debug?.(e); } + this._unbindDocumentUiHandlers(); try { this._genDropdown?.remove(); } catch (e) { console.debug?.(e); } this._mediaA = null; this._mediaB = null; diff --git a/js/features/viewer/floatingViewerManager.js b/js/features/viewer/floatingViewerManager.js index 21b030ca..952d681b 100644 --- a/js/features/viewer/floatingViewerManager.js +++ b/js/features/viewer/floatingViewerManager.js @@ -40,6 +40,18 @@ function _cancelFetch() { _fetchAC = null; } +function _emitVisibilityChanged(visible) { + window.dispatchEvent(new CustomEvent(EVENTS.MFV_VISIBILITY_CHANGED, { + detail: { visible: Boolean(visible) }, + })); +} + +function _syncViewerControls(inst) { + if (!inst) return; + inst.setLiveActive(_liveActive); + inst.setPreviewActive(_previewActive); +} + /** * When only 1 asset is selected and the MFV is in a compare mode, * look up the adjacent card in the rendered grid DOM to use as slot B. @@ -78,22 +90,30 @@ async function _loadFromIds(selectedIds) { _fetchAC = ac; try { + const pinnedSlot = _instance.getPinnedSlot(); let ids = selectedIds.slice(0, 2); - // Compare-mode fallback: if only 1 asset, auto-pick the adjacent grid item for slot B. const mode = _instance._mode; - if (ids.length === 1 && (mode === MFV_MODES.AB || mode === MFV_MODES.SIDE)) { + if (pinnedSlot && (mode === MFV_MODES.AB || mode === MFV_MODES.SIDE)) { + // With a pinned side, only one new asset is needed. + ids = [ids[0]]; + } else if (ids.length === 1 && (mode === MFV_MODES.AB || mode === MFV_MODES.SIDE)) { + // Compare-mode fallback: if only 1 asset, auto-pick the adjacent grid item for slot B. const adjId = _findAdjacentGridId(ids[0]); if (adjId) ids = [ids[0], adjId]; } - const result = await getAssetsBatch(ids); + const result = await getAssetsBatch(ids, ac ? { signal: ac.signal } : {}); if (ac?.signal.aborted) return; if (!result?.ok || !Array.isArray(result.data) || !result.data.length) return; if (!_instance) return; // disposed while fetching const assets = result.data; - if (ids.length >= 2 && assets.length >= 2) { + if (pinnedSlot === "A" && _instance._mediaA) { + _instance.loadMediaPair(_instance._mediaA, assets[0]); + } else if (pinnedSlot === "B" && _instance._mediaB) { + _instance.loadMediaPair(assets[0], _instance._mediaB); + } else if (ids.length >= 2 && assets.length >= 2) { _instance.loadMediaPair(assets[0], assets[1]); } else { _instance.loadMediaA(assets[0], { autoMode: true }); @@ -164,17 +184,17 @@ export const floatingViewerManager = { open() { const inst = _getInstance(); inst.show(); - inst.setLiveActive(_liveActive); // Sync button visual state + _syncViewerControls(inst); _bindSelectionListener(); // KEY FIX: immediately show whatever is selected in the grid. _syncCurrentGridSelection(); - window.dispatchEvent(new CustomEvent(EVENTS.MFV_VISIBILITY_CHANGED, { detail: { visible: true } })); + _emitVisibilityChanged(true); }, close() { if (_instance) _instance.hide(); _unbindSelectionListener(); - window.dispatchEvent(new CustomEvent(EVENTS.MFV_VISIBILITY_CHANGED, { detail: { visible: false } })); + _emitVisibilityChanged(false); }, toggle() { @@ -192,9 +212,12 @@ export const floatingViewerManager = { */ upsertWithContent(fileData) { const inst = _getInstance(); + const wasVisible = Boolean(inst.isVisible); inst.show(); + _syncViewerControls(inst); _bindSelectionListener(); inst.loadMediaA(fileData, { autoMode: true }); + if (!wasVisible) _emitVisibilityChanged(true); }, setLiveActive(active) { @@ -243,10 +266,13 @@ export const floatingViewerManager = { feedPreviewBlob(blob) { if (!_previewActive) return; const inst = _getInstance(); + const wasVisible = Boolean(inst.isVisible); if (!inst.isVisible) { inst.show(); } + _syncViewerControls(inst); inst.loadPreviewBlob(blob); + if (!wasVisible) _emitVisibilityChanged(true); }, }; diff --git a/js/tests/floating_viewer.vitest.mjs b/js/tests/floating_viewer.vitest.mjs new file mode 100644 index 00000000..dda10bae --- /dev/null +++ b/js/tests/floating_viewer.vitest.mjs @@ -0,0 +1,169 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../app/i18n.js", () => ({ + t: (_key, fallback) => fallback, +})); + +vi.mock("../app/events.js", () => ({ + EVENTS: {}, +})); + +vi.mock("../api/endpoints.js", () => ({ + buildViewURL: vi.fn(() => ""), + buildAssetViewURL: vi.fn(() => ""), +})); + +vi.mock("../features/viewer/genInfo.js", () => ({ + ensureViewerMetadataAsset: vi.fn(async (asset) => asset), +})); + +vi.mock("../api/client.js", () => ({ + getAssetMetadata: vi.fn(async () => ({})), + getFileMetadataScoped: vi.fn(async () => ({})), +})); + +vi.mock("../components/sidebar/parsers/geninfoParser.js", () => ({ + normalizeGenerationMetadata: vi.fn((meta) => meta), +})); + +describe("FloatingViewer", () => { + beforeEach(() => { + globalThis.document = { + adoptNode: vi.fn((node) => node), + body: { + appendChild: vi.fn(), + }, + createElement: vi.fn(() => ({ + className: "", + setAttribute: vi.fn(), + appendChild: vi.fn(), + replaceChildren: vi.fn(), + })), + }; + }); + + it("rebinds panel interactions when popping back into the main document", async () => { + const { FloatingViewer } = await import("../features/viewer/FloatingViewer.js"); + + const popup = { + removeEventListener: vi.fn(), + close: vi.fn(), + }; + const element = { + ownerDocument: { name: "popup-doc" }, + classList: { + add: vi.fn(), + }, + setAttribute: vi.fn(), + }; + const viewer = { + _isPopped: true, + element, + _popoutWindow: popup, + _popoutCloseHandler: vi.fn(), + _clearPopoutCloseWatch: vi.fn(), + _resetGenDropdownForCurrentDocument: vi.fn(), + _rebindControlHandlers: vi.fn(), + _bindPanelInteractions: vi.fn(), + _bindDocumentUiHandlers: vi.fn(), + _updatePopoutBtnUI: vi.fn(), + isVisible: false, + }; + + FloatingViewer.prototype.popIn.call(viewer, { closePopupWindow: false }); + + expect(document.adoptNode).toHaveBeenCalledWith(element); + expect(document.body.appendChild).toHaveBeenCalledWith(element); + expect(viewer._clearPopoutCloseWatch).toHaveBeenCalledTimes(1); + expect(popup.removeEventListener).toHaveBeenCalledTimes(3); + expect(viewer._resetGenDropdownForCurrentDocument).toHaveBeenCalledTimes(1); + expect(viewer._rebindControlHandlers).toHaveBeenCalledTimes(1); + expect(viewer._bindPanelInteractions).toHaveBeenCalledTimes(1); + expect(viewer._bindDocumentUiHandlers).toHaveBeenCalledTimes(1); + expect(viewer._updatePopoutBtnUI).toHaveBeenCalledTimes(1); + expect(element.classList.add).toHaveBeenCalledWith("is-visible"); + expect(element.setAttribute).toHaveBeenCalledWith("aria-hidden", "false"); + expect(viewer.isVisible).toBe(true); + expect(viewer._isPopped).toBe(false); + expect(viewer._popoutWindow).toBeNull(); + expect(popup.close).not.toHaveBeenCalled(); + }); + + it("refreshes drag and resize bindings when interactions are rebound", async () => { + const { FloatingViewer } = await import("../features/viewer/FloatingViewer.js"); + + const oldController = { + abort: vi.fn(), + }; + const header = { nodeName: "HEADER" }; + const element = { + querySelector: vi.fn(() => header), + }; + const viewer = { + element, + _panelAC: oldController, + _stopEdgeResize: vi.fn(), + _initEdgeResize: vi.fn(), + _initDrag: vi.fn(), + }; + + FloatingViewer.prototype._bindPanelInteractions.call(viewer); + + expect(viewer._stopEdgeResize).toHaveBeenCalledTimes(1); + expect(oldController.abort).toHaveBeenCalledTimes(1); + expect(viewer._panelAC).toBeInstanceOf(AbortController); + expect(viewer._panelAC).not.toBe(oldController); + expect(viewer._initEdgeResize).toHaveBeenCalledWith(element); + expect(element.querySelector).toHaveBeenCalledWith(".mjr-mfv-header"); + expect(viewer._initDrag).toHaveBeenCalledWith(header); + }); + + it("rebinds toolbar and header controls with a fresh abort controller", async () => { + const { FloatingViewer } = await import("../features/viewer/FloatingViewer.js"); + + const oldController = { + abort: vi.fn(), + }; + const closeBtn = { addEventListener: vi.fn() }; + const modeBtn = { addEventListener: vi.fn() }; + const pinSelect = { addEventListener: vi.fn() }; + const liveBtn = { addEventListener: vi.fn() }; + const previewBtn = { addEventListener: vi.fn() }; + const genBtn = { addEventListener: vi.fn() }; + const popoutBtn = { addEventListener: vi.fn() }; + const captureBtn = { addEventListener: vi.fn() }; + const viewer = { + _btnAC: oldController, + _closeBtn: closeBtn, + _modeBtn: modeBtn, + _pinSelect: pinSelect, + _liveBtn: liveBtn, + _previewBtn: previewBtn, + _genBtn: genBtn, + _popoutBtn: popoutBtn, + _captureBtn: captureBtn, + _genDropdown: null, + _pinnedSlot: null, + _mode: "simple", + _cycleMode: vi.fn(), + _updatePinSelectUI: vi.fn(), + _closeGenDropdown: vi.fn(), + _openGenDropdown: vi.fn(), + _captureView: vi.fn(), + setMode: vi.fn(), + }; + + FloatingViewer.prototype._rebindControlHandlers.call(viewer); + + expect(oldController.abort).toHaveBeenCalledTimes(1); + expect(viewer._btnAC).toBeInstanceOf(AbortController); + expect(closeBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(modeBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(pinSelect.addEventListener).toHaveBeenCalledWith("change", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(liveBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(previewBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(genBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(popoutBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + expect(captureBtn.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), expect.objectContaining({ signal: viewer._btnAC.signal })); + }); +}); diff --git a/js/tests/floating_viewer_manager.vitest.mjs b/js/tests/floating_viewer_manager.vitest.mjs new file mode 100644 index 00000000..663c8383 --- /dev/null +++ b/js/tests/floating_viewer_manager.vitest.mjs @@ -0,0 +1,324 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => { + const MFV_MODES = { + SIMPLE: "simple", + AB: "ab", + SIDE: "side", + }; + + let viewerMode = MFV_MODES.SIMPLE; + let lastViewer = null; + let activeGrid = null; + let pinnedSlot = null; + + const getAssetsBatchMock = vi.fn(); + const getSelectedIdSetMock = vi.fn(); + const reportErrorMock = vi.fn(); + + class FloatingViewerMock { + constructor() { + this._mode = viewerMode; + this._mediaA = null; + this._mediaB = null; + this._pinnedSlot = pinnedSlot; + this.isVisible = false; + this.loadMediaA = vi.fn((asset, opts) => { + this._mediaA = asset; + this._lastLoadMediaA = [asset, opts]; + }); + this.loadMediaPair = vi.fn((assetA, assetB) => { + this._mediaA = assetA; + this._mediaB = assetB; + this._lastLoadMediaPair = [assetA, assetB]; + }); + this.setLiveActive = vi.fn(); + this.setPreviewActive = vi.fn(); + this.loadPreviewBlob = vi.fn(); + lastViewer = this; + } + + render() { + return { nodeName: "DIV" }; + } + + show() { + this.isVisible = true; + } + + hide() { + this.isVisible = false; + } + + get isPopped() { + return false; + } + + getPinnedSlot() { + return this._pinnedSlot; + } + + popOut() {} + + popIn() {} + + } + + return { + MFV_MODES, + FloatingViewerMock, + getAssetsBatchMock, + getSelectedIdSetMock, + reportErrorMock, + getActiveGridContainer() { + return activeGrid; + }, + reset() { + viewerMode = MFV_MODES.SIMPLE; + lastViewer = null; + activeGrid = null; + pinnedSlot = null; + getAssetsBatchMock.mockReset(); + getSelectedIdSetMock.mockReset(); + reportErrorMock.mockReset(); + }, + setActiveGrid(grid) { + activeGrid = grid; + }, + setViewerMode(mode) { + viewerMode = mode; + }, + setPinnedSlot(slot) { + pinnedSlot = slot || null; + }, + getLastViewer() { + return lastViewer; + }, + }; +}); + +vi.mock("../features/viewer/FloatingViewer.js", () => ({ + FloatingViewer: state.FloatingViewerMock, + MFV_MODES: state.MFV_MODES, +})); + +vi.mock("../api/client.js", () => ({ + getAssetsBatch: state.getAssetsBatchMock, +})); + +vi.mock("../features/panel/AssetsManagerPanel.js", () => ({ + getActiveGridContainer: () => state.getActiveGridContainer(), +})); + +vi.mock("../features/grid/GridSelectionManager.js", () => ({ + getSelectedIdSet: state.getSelectedIdSetMock, +})); + +vi.mock("../utils/logging.js", () => ({ + reportError: state.reportErrorMock, +})); + +function createWindowStub() { + const listeners = new Map(); + return { + addEventListener(type, handler) { + if (!listeners.has(type)) listeners.set(type, new Set()); + listeners.get(type).add(handler); + }, + removeEventListener(type, handler) { + listeners.get(type)?.delete(handler); + }, + dispatchEvent(event) { + const handlers = Array.from(listeners.get(event?.type) || []); + for (const handler of handlers) { + handler(event); + } + return true; + }, + }; +} + +function createGrid(ids = []) { + return { + querySelectorAll() { + return ids.map((id) => ({ dataset: { mjrAssetId: String(id) } })); + }, + }; +} + +async function flushAsyncWork() { + await Promise.resolve(); + await Promise.resolve(); +} + +beforeEach(() => { + vi.resetModules(); + state.reset(); + globalThis.window = createWindowStub(); + globalThis.document = { + body: { + appendChild: vi.fn(), + }, + }; + globalThis.CustomEvent = class { + constructor(type, init = {}) { + this.type = type; + this.detail = init.detail; + } + }; +}); + +describe("floatingViewerManager", () => { + it("loads the current selection when no pin is active", async () => { + state.setActiveGrid(createGrid(["101"])); + state.getSelectedIdSetMock.mockReturnValue(new Set(["101"])); + state.getAssetsBatchMock.mockResolvedValue({ + ok: true, + data: [{ id: 101, filename: "one.png" }], + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.open(); + await flushAsyncWork(); + + const viewer = state.getLastViewer(); + expect(viewer).toBeTruthy(); + expect(viewer.loadMediaA).toHaveBeenCalledWith( + { id: 101, filename: "one.png" }, + { autoMode: true }, + ); + expect(state.reportErrorMock).not.toHaveBeenCalled(); + }); + + it("keeps compare-mode fallback working when no pin is active", async () => { + state.setViewerMode(state.MFV_MODES.AB); + state.setActiveGrid(createGrid(["10", "11", "12"])); + state.getSelectedIdSetMock.mockReturnValue(new Set(["11"])); + state.getAssetsBatchMock.mockResolvedValue({ + ok: true, + data: [ + { id: 11, filename: "left.png" }, + { id: 12, filename: "right.png" }, + ], + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.open(); + await flushAsyncWork(); + + const viewer = state.getLastViewer(); + expect(viewer.loadMediaPair).toHaveBeenCalledWith( + { id: 11, filename: "left.png" }, + { id: 12, filename: "right.png" }, + ); + expect(state.getAssetsBatchMock).toHaveBeenCalledWith( + ["11", "12"], + expect.objectContaining({ signal: expect.any(Object) }), + ); + expect(state.reportErrorMock).not.toHaveBeenCalled(); + }); + + it("preserves pinned slot A and updates slot B from new selections", async () => { + state.setViewerMode(state.MFV_MODES.AB); + state.setPinnedSlot("A"); + state.setActiveGrid(createGrid(["201", "202"])); + state.getSelectedIdSetMock.mockReturnValue(new Set()); + state.getAssetsBatchMock.mockResolvedValue({ + ok: true, + data: [{ id: 202, filename: "candidate.png" }], + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.open(); + + const viewer = state.getLastViewer(); + viewer._mediaA = { id: 9001, filename: "reference-a.png" }; + + window.dispatchEvent(new CustomEvent("mjr:selection-changed", { + detail: { selectedIds: ["202"] }, + })); + await flushAsyncWork(); + + expect(viewer.loadMediaPair).toHaveBeenCalledWith( + { id: 9001, filename: "reference-a.png" }, + { id: 202, filename: "candidate.png" }, + ); + expect(state.getAssetsBatchMock).toHaveBeenCalledWith( + ["202"], + expect.objectContaining({ signal: expect.any(Object) }), + ); + }); + + it("preserves pinned slot B and updates slot A from new selections", async () => { + state.setViewerMode(state.MFV_MODES.AB); + state.setPinnedSlot("B"); + state.setActiveGrid(createGrid(["301", "302"])); + state.getSelectedIdSetMock.mockReturnValue(new Set()); + state.getAssetsBatchMock.mockResolvedValue({ + ok: true, + data: [{ id: 301, filename: "candidate-a.png" }], + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.open(); + + const viewer = state.getLastViewer(); + viewer._mediaB = { id: 9002, filename: "reference-b.png" }; + + window.dispatchEvent(new CustomEvent("mjr:selection-changed", { + detail: { selectedIds: ["301"] }, + })); + await flushAsyncWork(); + + expect(viewer.loadMediaPair).toHaveBeenCalledWith( + { id: 301, filename: "candidate-a.png" }, + { id: 9002, filename: "reference-b.png" }, + ); + expect(state.getAssetsBatchMock).toHaveBeenCalledWith( + ["301"], + expect.objectContaining({ signal: expect.any(Object) }), + ); + }); + + it("emits visibility and syncs control states when live stream auto-opens the viewer", async () => { + const seen = []; + window.addEventListener("mjr:mfv-visibility-changed", (event) => { + seen.push(Boolean(event?.detail?.visible)); + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.setLiveActive(true); + floatingViewerManager.upsertWithContent({ filename: "live-output.png", type: "output" }); + + const viewer = state.getLastViewer(); + expect(viewer).toBeTruthy(); + expect(viewer.isVisible).toBe(true); + expect(viewer.setLiveActive).toHaveBeenLastCalledWith(true); + expect(viewer.setPreviewActive).toHaveBeenLastCalledWith(false); + expect(viewer.loadMediaA).toHaveBeenCalledWith( + { filename: "live-output.png", type: "output" }, + { autoMode: true }, + ); + expect(seen).toEqual([true]); + }); + + it("emits visibility and syncs preview state when preview stream auto-opens the viewer", async () => { + const seen = []; + const blob = { fake: "blob" }; + window.addEventListener("mjr:mfv-visibility-changed", (event) => { + seen.push(Boolean(event?.detail?.visible)); + }); + + const { floatingViewerManager } = await import("../features/viewer/floatingViewerManager.js"); + floatingViewerManager.setPreviewActive(true); + floatingViewerManager.feedPreviewBlob(blob); + + const viewer = state.getLastViewer(); + expect(viewer).toBeTruthy(); + expect(viewer.isVisible).toBe(true); + expect(viewer.setLiveActive).toHaveBeenLastCalledWith(false); + expect(viewer.setPreviewActive).toHaveBeenLastCalledWith(true); + expect(viewer.loadPreviewBlob).toHaveBeenCalledWith(blob); + expect(seen).toEqual([true]); + }); +}); diff --git a/js/tests/grid_controller_semantic_mode.vitest.mjs b/js/tests/grid_controller_semantic_mode.vitest.mjs new file mode 100644 index 00000000..5713cb65 --- /dev/null +++ b/js/tests/grid_controller_semantic_mode.vitest.mjs @@ -0,0 +1,113 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const apiState = vi.hoisted(() => ({ + hybridSearch: vi.fn(), + vectorSearch: vi.fn(), + vectorStats: vi.fn(), +})); + +vi.mock("../api/client.js", () => ({ + hybridSearch: apiState.hybridSearch, + vectorSearch: apiState.vectorSearch, + vectorStats: apiState.vectorStats, +})); + +vi.mock("../app/toast.js", () => ({ + comfyToast: vi.fn(), +})); + +vi.mock("../app/i18n.js", () => ({ + t: (_key, fallback, vars) => { + if (!vars) return fallback; + return String(fallback || "").replace(/\{(\w+)\}/g, (_m, key) => String(vars[key] ?? "")); + }, +})); + +vi.mock("../app/settings.js", () => ({ + loadMajoorSettings: () => ({ ai: { vectorSearchEnabled: true } }), +})); + +function createGridContainer() { + return { + dataset: {}, + dispatchEvent: vi.fn(), + }; +} + +describe("gridController semantic mode", () => { + beforeEach(() => { + vi.clearAllMocks(); + globalThis.window = {}; + globalThis.CustomEvent = class { + constructor(type, init = {}) { + this.type = type; + this.detail = init.detail; + } + }; + }); + + it("uses hybrid AI search when the semantic toggle is active", async () => { + const { createGridController } = await import("../features/panel/controllers/gridController.js"); + + apiState.hybridSearch.mockResolvedValue({ + ok: true, + data: [{ id: 3, asset_id: 3, filename: "dino.png" }], + }); + apiState.vectorSearch.mockResolvedValue({ ok: true, data: [] }); + apiState.vectorStats.mockResolvedValue({ ok: true, data: { total: 33 } }); + + const loadAssets = vi.fn(async () => ({ ok: true, count: 0, total: 0 })); + const loadAssetsFromList = vi.fn(async (_grid, assets) => ({ + ok: true, + count: assets.length, + total: assets.length, + })); + + const controller = createGridController({ + gridContainer: createGridContainer(), + loadAssets, + loadAssetsFromList, + getCollectionAssets: vi.fn(), + disposeGrid: vi.fn(), + getQuery: () => "dinosaure", + searchInputEl: { dataset: { mjrSemanticMode: "1" } }, + state: { + scope: "output", + customRootId: "", + currentFolderRelativePath: "", + kindFilter: "", + workflowOnly: false, + minRating: 0, + minSizeMB: 0, + maxSizeMB: 0, + resolutionCompare: "gte", + minWidth: 0, + minHeight: 0, + maxWidth: 0, + maxHeight: 0, + workflowType: "", + dateRangeFilter: "", + dateExactFilter: "", + sort: "mtime_desc", + selectedAssetIds: [], + activeAssetId: "", + collectionId: "", + collectionName: "", + viewScope: "", + }, + }); + + await controller.reloadGrid(); + + expect(apiState.hybridSearch).toHaveBeenCalledWith("dinosaure", expect.objectContaining({ + topK: 100, + scope: "output", + })); + expect(loadAssetsFromList).toHaveBeenCalledWith( + expect.any(Object), + [{ id: 3, asset_id: 3, filename: "dino.png" }], + expect.objectContaining({ reset: true }) + ); + expect(loadAssets).not.toHaveBeenCalled(); + }); +}); diff --git a/js/tests/message_center.vitest.mjs b/js/tests/message_center.vitest.mjs new file mode 100644 index 00000000..c8aa56c8 --- /dev/null +++ b/js/tests/message_center.vitest.mjs @@ -0,0 +1,115 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +class MemoryStorage { + constructor() { + this.store = new Map(); + } + + clear() { + this.store.clear(); + } + + getItem(key) { + return this.store.has(key) ? this.store.get(key) : null; + } + + removeItem(key) { + this.store.delete(key); + } + + setItem(key, value) { + this.store.set(key, String(value)); + } +} + +function installRuntimeGlobals() { + const eventTarget = new EventTarget(); + eventTarget.location = { href: "http://127.0.0.1:8188/" }; + globalThis.window = eventTarget; + globalThis.localStorage = new MemoryStorage(); + globalThis.CustomEvent = class CustomEvent extends Event { + constructor(type, init = {}) { + super(type); + this.detail = init.detail; + } + }; +} + +async function loadMessageCenter() { + vi.resetModules(); + return import("../features/panel/messages/messageCenter.js"); +} + +describe("messageCenter", () => { + beforeEach(() => { + installRuntimeGlobals(); + }); + + it("sanitizes unsupported levels to safe values", async () => { + const { addPanelMessage, listPanelMessages } = await loadMessageCenter(); + + addPanelMessage({ id: "bad-level", title: "Bad", level: "error urgent" }); + addPanelMessage({ id: "warn-level", title: "Warn", level: "warn" }); + + const bad = listPanelMessages().find((message) => message.id === "bad-level"); + const warn = listPanelMessages().find((message) => message.id === "warn-level"); + + expect(bad?.level).toBe("info"); + expect(warn?.level).toBe("warning"); + }); + + it("keeps builtin removals dismissed across reloads", async () => { + const builtinId = "whats-new-2026-03-05-pin-reference"; + const firstLoad = await loadMessageCenter(); + + firstLoad.ensurePanelMessagesReady(); + expect(firstLoad.listPanelMessages().map((message) => message.id)).toContain(builtinId); + expect(firstLoad.removePanelMessage(builtinId)).toBe(true); + expect(firstLoad.listPanelMessages().map((message) => message.id)).not.toContain(builtinId); + + const secondLoad = await loadMessageCenter(); + secondLoad.ensurePanelMessagesReady(); + expect(secondLoad.listPanelMessages().map((message) => message.id)).not.toContain(builtinId); + }); + + it("keeps builtin announcements cleared after clear all", async () => { + const firstLoad = await loadMessageCenter(); + + firstLoad.ensurePanelMessagesReady(); + expect(firstLoad.listPanelMessages().length).toBeGreaterThan(0); + firstLoad.clearPanelMessages(); + expect(firstLoad.listPanelMessages()).toEqual([]); + + const secondLoad = await loadMessageCenter(); + secondLoad.ensurePanelMessagesReady(); + expect(secondLoad.listPanelMessages()).toEqual([]); + }); + + it("returns the stored merged message when updating an existing entry", async () => { + const { addPanelMessage, listPanelMessages, upsertPanelMessage } = await loadMessageCenter(); + + addPanelMessage({ + id: "existing-message", + title: "Initial title", + createdAt: 123456, + level: "info", + }); + + const updated = upsertPanelMessage({ + id: "existing-message", + title: "Updated title", + level: "success", + }); + const stored = listPanelMessages().find((message) => message.id === "existing-message"); + + expect(updated.createdAt).toBe(123456); + expect(updated.title).toBe("Updated title"); + expect(updated.level).toBe("success"); + expect(stored).toMatchObject({ + id: "existing-message", + title: "Updated title", + createdAt: 123456, + level: "success", + }); + }); +}); diff --git a/js/tests/scope_controller.vitest.mjs b/js/tests/scope_controller.vitest.mjs new file mode 100644 index 00000000..af307173 --- /dev/null +++ b/js/tests/scope_controller.vitest.mjs @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createScopeController } from "../features/panel/controllers/scopeController.js"; + +function makeButton(scope) { + const classes = new Set(); + return { + dataset: { scope }, + style: { display: "" }, + classList: { + toggle(name, active) { + if (active) classes.add(name); + else classes.delete(name); + }, + has(name) { + return classes.has(name); + }, + }, + }; +} + +describe("scopeController similar scope", () => { + it("activates a temporary similar scope and clears it cleanly", async () => { + const tabButtons = { + tabAll: makeButton("all"), + tabInputs: makeButton("input"), + tabOutputs: makeButton("output"), + tabCustom: makeButton("custom"), + tabSimilar: makeButton("similar"), + }; + const state = { + scope: "output", + viewScope: "", + similarResults: [{ asset_id: 7 }], + similarTitle: "Similar to asset #42 (1 results)", + similarSourceAssetId: "42", + collectionId: "", + collectionName: "", + currentFolderRelativePath: "", + customRootId: "", + }; + const reloadGrid = vi.fn(async () => {}); + const onChanged = vi.fn(); + const onScopeChanged = vi.fn(); + const onBeforeReload = vi.fn(); + const popovers = { close: vi.fn() }; + const customMenuBtn = { style: { display: "" } }; + const customPopover = { style: { display: "" } }; + + const controller = createScopeController({ + state, + tabButtons, + customMenuBtn, + customPopover, + popovers, + reloadGrid, + onChanged, + onScopeChanged, + onBeforeReload, + }); + + controller.setActiveTabStyles(); + expect(tabButtons.tabSimilar.style.display).toBe(""); + expect(tabButtons.tabOutputs.classList.has("is-active")).toBe(true); + + await controller.setScope("similar"); + expect(state.viewScope).toBe("similar"); + expect(tabButtons.tabSimilar.classList.has("is-active")).toBe(true); + expect(onScopeChanged).not.toHaveBeenCalled(); + expect(onBeforeReload).toHaveBeenCalledTimes(1); + expect(reloadGrid).toHaveBeenCalledTimes(1); + + await controller.clearSimilarScope({ reload: false }); + expect(state.viewScope).toBe(""); + expect(state.similarResults).toEqual([]); + expect(state.similarTitle).toBe(""); + expect(state.similarSourceAssetId).toBe(""); + expect(tabButtons.tabSimilar.style.display).toBe("none"); + }); +}); diff --git a/js/theme/theme-comfy.css b/js/theme/theme-comfy.css index b22a555e..d8d24488 100644 --- a/js/theme/theme-comfy.css +++ b/js/theme/theme-comfy.css @@ -179,6 +179,37 @@ opacity: 1; } +.mjr-assets-manager .mjr-icon-btn.mjr-message-btn { + position: relative; + overflow: visible; +} + +.mjr-assets-manager .mjr-icon-btn.mjr-message-has-unread { + border-color: color-mix(in srgb, var(--mjr-accent) 75%, var(--mjr-border)); + background: color-mix(in srgb, var(--mjr-surface-2) 72%, var(--mjr-accent) 11%); +} + +.mjr-assets-manager .mjr-message-badge { + position: absolute; + top: -5px; + right: -6px; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--mjr-surface-1) 55%, rgba(255,255,255,0.3)); + background: #ef5350; + color: #fff; + font-size: 10px; + font-weight: 700; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + pointer-events: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); +} + .mjr-assets-manager .mjr-icon-btn .pi { font-size: 13px; line-height: 1; @@ -237,6 +268,9 @@ border: 1px solid var(--mjr-border, var(--border-color, rgba(255, 255, 255, 0.12))); box-shadow: 0 12px 28px rgba(0,0,0,0.35); z-index: var(--mjr-popover-z-index); + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; } .mjr-filter-popover { @@ -257,6 +291,175 @@ padding: 10px; } +.mjr-messages-popover { + min-width: 320px; + width: min(380px, calc(100vw - 24px)); + padding: 10px; +} + +.mjr-messages-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.mjr-messages-title { + flex: 1 1 auto; + min-width: 0; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + color: color-mix(in srgb, var(--content-fg, #ddd) 82%, var(--mjr-muted)); +} + +.mjr-messages-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + flex-wrap: wrap; +} + +.mjr-messages-mark-read-btn { + padding: 4px 8px; + font-size: 11px; + line-height: 1.2; +} + +.mjr-messages-star-link { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + font-size: 11px; + line-height: 1.2; + text-decoration: none; + color: var(--content-fg, #f1f1f1); + border-color: color-mix(in srgb, #f3c84b 42%, var(--mjr-border)); + background: color-mix(in srgb, #f3c84b 12%, var(--mjr-surface-2)); +} + +.mjr-messages-star-link:hover, +.mjr-messages-star-link:focus-visible { + text-decoration: none; +} + +.mjr-messages-star-icon { + color: #f3c84b; + font-size: 12px; + line-height: 1; +} + +.mjr-messages-star-label { + white-space: nowrap; +} + +.mjr-messages-list { + display: grid; + gap: 8px; +} + +.mjr-messages-empty { + padding: 8px 6px; + font-size: 12px; + opacity: 0.72; +} + +.mjr-message-item { + display: grid; + gap: 5px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--mjr-border) 86%, transparent); + background: color-mix(in srgb, var(--mjr-surface-2) 58%, transparent); + padding: 8px; +} + +.mjr-message-item--unread { + border-color: color-mix(in srgb, var(--mjr-accent) 58%, var(--mjr-border)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mjr-accent) 26%, transparent); +} + +.mjr-message-item--warning { + border-color: color-mix(in srgb, #f0b85a 62%, var(--mjr-border)); +} + +.mjr-message-item--error { + border-color: color-mix(in srgb, #f36b6b 62%, var(--mjr-border)); +} + +.mjr-message-item--success { + border-color: color-mix(in srgb, #58c07d 62%, var(--mjr-border)); +} + +.mjr-message-item-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.mjr-message-item-title-wrap { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1 1 auto; +} + +.mjr-message-item-emoji { + flex: 0 0 auto; + font-size: 16px; + line-height: 1; +} + +.mjr-message-item-title { + min-width: 0; + font-weight: 600; + color: var(--content-fg, #f1f1f1); +} + +.mjr-message-item-category { + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.03em; + background: color-mix(in srgb, var(--mjr-accent) 20%, var(--mjr-surface-2)); + color: color-mix(in srgb, var(--content-fg, #fff) 86%, transparent); + white-space: nowrap; +} + +.mjr-message-item-meta { + font-size: 11px; + opacity: 0.72; +} + +.mjr-message-item-body { + font-size: 12px; + opacity: 0.92; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} + +.mjr-message-item-action { + font-size: 11px; + font-weight: 600; + color: var(--mjr-accent, var(--comfy-accent, #5fb3ff)); + text-decoration: none; + width: fit-content; +} + +.mjr-message-item-action:hover { + text-decoration: underline; +} + .mjr-filter-group { border: 1px solid color-mix(in srgb, var(--mjr-border) 85%, transparent); background: color-mix(in srgb, var(--mjr-surface-2) 70%, transparent); @@ -340,32 +543,32 @@ } /* Scrollbars (match ComfyUI feel) */ -.mjr-assets-manager .mjr-am-browse { +.mjr-assets-manager .mjr-am-grid-scroll { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--mjr-border) 85%, var(--content-fg, #ddd)) transparent; } -.mjr-assets-manager .mjr-am-browse::-webkit-scrollbar { +.mjr-assets-manager .mjr-am-grid-scroll::-webkit-scrollbar { width: 10px; height: 10px; } -.mjr-assets-manager .mjr-am-browse::-webkit-scrollbar-track { +.mjr-assets-manager .mjr-am-grid-scroll::-webkit-scrollbar-track { background: transparent; } -.mjr-assets-manager .mjr-am-browse::-webkit-scrollbar-thumb { +.mjr-assets-manager .mjr-am-grid-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--mjr-surface-2) 55%, var(--content-fg, #ddd)); border: 2px solid transparent; background-clip: padding-box; border-radius: 999px; } -.mjr-assets-manager .mjr-am-browse:hover::-webkit-scrollbar-thumb { +.mjr-assets-manager .mjr-am-grid-scroll:hover::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--mjr-surface-2) 70%, var(--content-fg, #ddd)); } -.mjr-assets-manager .mjr-am-browse::-webkit-scrollbar-corner { +.mjr-assets-manager .mjr-am-grid-scroll::-webkit-scrollbar-corner { background: transparent; } @@ -815,8 +1018,10 @@ .mjr-assets-manager .mjr-am-browse { margin-top: 0; - /* Keep cards away from the vertical scrollbar */ - padding-right: 10px; +} + +.mjr-assets-manager .mjr-am-grid-scroll { + padding-right: 4px; scrollbar-gutter: stable; } @@ -1336,7 +1541,7 @@ /* Space between sidebar and grid */ .mjr-assets-manager .mjr-am-browse { - gap: 10px; + gap: 0; } /* Border based on position */ @@ -2347,62 +2552,119 @@ display: flex; align-items: center; justify-content: space-between; - padding: 4px 8px; + padding: 6px 10px; background: var(--mjr-surface-1); border-bottom: 1px solid var(--mjr-border); cursor: move; user-select: none; flex-shrink: 0; gap: 6px; - min-height: 30px; + min-height: 34px; } .mjr-mfv-header-title { - font-size: 11px; - opacity: 0.6; + font-size: 12px; + font-weight: 600; + opacity: 0.78; pointer-events: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; - letter-spacing: 0.2px; + letter-spacing: 0.28px; } /* โ”€โ”€ Toolbar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .mjr-mfv-toolbar { display: flex; align-items: center; - gap: 3px; - padding: 3px 7px; + gap: 4px; + padding: 4px 8px; background: color-mix(in srgb, var(--mjr-surface-0) 50%, var(--mjr-surface-1) 50%); border-bottom: 1px solid var(--mjr-border); flex-shrink: 0; - min-height: 28px; + min-height: 36px; } .mjr-mfv-toolbar-sep { width: 1px; - height: 14px; + height: 18px; background: var(--mjr-border); - margin: 0 2px; + margin: 0 3px; flex-shrink: 0; } +.mjr-mfv-pin-select { + height: 30px; + min-width: 88px; + border-radius: 7px; + border: 1px solid var(--mjr-border); + background: var(--mjr-surface-2); + color: var(--content-fg, #ddd); + outline: none; + padding: 0 28px 0 10px; + font-size: 12px; + font-weight: 500; + line-height: 28px; + flex-shrink: 0; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + color-scheme: dark; + background-image: + linear-gradient(45deg, transparent 50%, currentColor 50%), + linear-gradient(135deg, currentColor 50%, transparent 50%); + background-position: + calc(100% - 14px) calc(50% - 1px), + calc(100% - 9px) calc(50% - 1px); + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; + transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease; +} + +.mjr-mfv-pin-select:hover { + border-color: color-mix(in srgb, var(--mjr-accent) 45%, var(--mjr-border)); + background-color: color-mix(in srgb, var(--mjr-surface-2) 80%, var(--mjr-accent) 8%); +} + +.mjr-mfv-pin-select:focus { + border-color: color-mix(in srgb, var(--mjr-accent) 55%, var(--mjr-border)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--mjr-accent) 28%, transparent); +} + +.mjr-mfv-pin-select:focus-visible { + border-color: color-mix(in srgb, var(--mjr-accent) 55%, var(--mjr-border)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--mjr-accent) 28%, transparent); +} + +.mjr-mfv-pin-select.is-pinned { + border-color: color-mix(in srgb, var(--mjr-accent) 58%, #ffffff 12%); + background-color: color-mix(in srgb, var(--mjr-surface-2) 42%, var(--mjr-accent) 58%); + color: #eef7ff; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mjr-accent) 24%, transparent); +} + +.mjr-mfv-pin-select option { + background: var(--mjr-surface-1, #1c1c1c); + color: var(--content-fg, #ddd); +} + /* โ”€โ”€ Icon buttons inside MFV (outside .mjr-assets-manager scope) โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .mjr-mfv .mjr-icon-btn { - width: 26px; - height: 26px; + width: 30px; + height: 30px; display: inline-flex; align-items: center; justify-content: center; - border-radius: 6px; + border-radius: 7px; border: 1px solid var(--mjr-border); background: var(--mjr-surface-2); color: var(--content-fg, #ddd); cursor: pointer; padding: 0; flex-shrink: 0; - transition: transform 120ms ease, background-color 120ms ease, border-color 120ms ease, color 120ms ease; + transition: transform 120ms ease, background-color 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease; } .mjr-mfv .mjr-icon-btn:hover { @@ -2411,8 +2673,14 @@ transform: translateY(-1px); } +.mjr-mfv .mjr-icon-btn:focus-visible { + outline: none; + border-color: color-mix(in srgb, var(--mjr-accent) 58%, var(--mjr-border)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--mjr-accent) 24%, transparent); +} + .mjr-mfv .mjr-icon-btn .pi { - font-size: 12px; + font-size: 13px; line-height: 1; opacity: 0.9; } @@ -2421,18 +2689,122 @@ border-color: color-mix(in srgb, var(--mjr-status-success) 60%, var(--mjr-border)); color: var(--mjr-status-success); background: color-mix(in srgb, var(--mjr-surface-2) 80%, var(--mjr-status-success) 12%); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--mjr-status-success) 25%, transparent); +} + +.mjr-mfv .mjr-icon-btn.mjr-live-active .pi { + opacity: 1; +} + +.mjr-mfv .mjr-icon-btn.mjr-preview-active { + border-color: color-mix(in srgb, var(--mjr-accent) 60%, var(--mjr-border)); + color: var(--mjr-accent); + background: color-mix(in srgb, var(--mjr-surface-2) 80%, var(--mjr-accent) 12%); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--mjr-accent) 25%, transparent); +} + +.mjr-mfv .mjr-icon-btn.mjr-preview-active .pi { + opacity: 1; } /* Pop-in mode (viewer popped out): raise button contrast/brightness */ +.mjr-mfv.mjr-mfv--popped { + background: #262626; + border-color: color-mix(in srgb, var(--mjr-border) 86%, rgba(255,255,255,0.14)); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.52); +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-header { + background: linear-gradient( + 180deg, + rgba(74, 74, 74, 0.98), + rgba(52, 52, 52, 0.98) + ); + border-bottom-color: color-mix(in srgb, var(--mjr-border) 88%, rgba(255,255,255,0.12)); +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-header-title { + opacity: 0.96; + color: #eef4ff; +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-toolbar { + background: linear-gradient( + 180deg, + rgba(48, 48, 48, 0.98), + rgba(28, 28, 28, 0.98) + ); + border-bottom-color: color-mix(in srgb, var(--mjr-border) 88%, rgba(255,255,255,0.12)); +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-toolbar-sep { + background: color-mix(in srgb, var(--mjr-border) 84%, rgba(255,255,255,0.16)); +} + .mjr-mfv.mjr-mfv--popped .mjr-icon-btn { - border-color: color-mix(in srgb, var(--mjr-border) 40%, #ffffff 60%); - background: color-mix(in srgb, var(--mjr-surface-2) 65%, #ffffff 18%); - color: color-mix(in srgb, var(--content-fg, #ddd) 75%, #ffffff 25%); + border-color: color-mix(in srgb, var(--mjr-border) 84%, rgba(255,255,255,0.16)); + background: linear-gradient( + 180deg, + rgba(88, 88, 88, 0.94), + rgba(66, 66, 66, 0.98) + ); + color: var(--content-fg, #ddd) !important; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04); } .mjr-mfv.mjr-mfv--popped .mjr-icon-btn:hover { - border-color: color-mix(in srgb, var(--mjr-accent) 50%, #ffffff 50%); - background: color-mix(in srgb, var(--mjr-surface-2) 55%, #ffffff 30%); + border-color: color-mix(in srgb, var(--mjr-accent) 52%, var(--mjr-border)); + background: linear-gradient( + 180deg, + rgba(91, 120, 148, 0.96), + rgba(56, 82, 106, 0.98) + ); + color: #f4f8ff !important; +} + +.mjr-mfv.mjr-mfv--popped .mjr-icon-btn .pi { + opacity: 1; + color: inherit !important; +} + +.mjr-mfv.mjr-mfv--popped .mjr-icon-btn.mjr-live-active { + border-color: color-mix(in srgb, var(--mjr-status-success) 60%, #ffffff 40%); + background: linear-gradient(180deg, rgba(76, 175, 80, 0.62), rgba(76, 175, 80, 0.34)); + color: #ffffff !important; + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.12), + 0 0 0 1px color-mix(in srgb, var(--mjr-status-success) 24%, transparent), + 0 0 18px rgba(76, 175, 80, 0.18); +} + +.mjr-mfv.mjr-mfv--popped .mjr-icon-btn.mjr-preview-active { + border-color: color-mix(in srgb, var(--mjr-accent) 58%, #ffffff 42%); + background: linear-gradient(180deg, rgba(95, 179, 255, 0.62), rgba(95, 179, 255, 0.34)); + color: #ffffff !important; + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.12), + 0 0 0 1px color-mix(in srgb, var(--mjr-accent) 24%, transparent), + 0 0 18px rgba(95, 179, 255, 0.18); +} + +.mjr-mfv.mjr-mfv--popped .mjr-pin-select, +.mjr-mfv.mjr-mfv--popped .mjr-mfv-pin-select { + border-color: color-mix(in srgb, var(--mjr-border) 84%, rgba(255,255,255,0.16)); + background-color: rgba(74, 74, 74, 0.96); + color: var(--content-fg, #ddd); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04); +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-pin-select:hover, +.mjr-mfv.mjr-mfv--popped .mjr-mfv-pin-select:focus { + border-color: color-mix(in srgb, var(--mjr-accent) 52%, var(--mjr-border)); + background-color: rgba(66, 96, 124, 0.98); +} + +.mjr-mfv.mjr-mfv--popped .mjr-mfv-pin-select.is-pinned { + border-color: color-mix(in srgb, var(--mjr-accent) 58%, #ffffff 42%); + background: linear-gradient(180deg, rgba(95,179,255,0.60), rgba(95,179,255,0.34)); + color: #ffffff; } .mjr-mfv .mjr-icon-btn.mjr-popin-active { @@ -2487,15 +2859,25 @@ position: absolute; top: 0; bottom: 0; - width: 3px; + width: 0.5px; cursor: col-resize; z-index: 10; transform: translateX(-50%); background: rgba(255, 255, 255, 0.88); - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.4); + box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.4); transition: background 100ms ease; } +.mjr-mfv-ab-divider::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 12px; + transform: translateX(-50%); +} + .mjr-mfv-ab-divider:hover, .mjr-mfv-ab-divider:active { background: var(--mjr-accent, #5fb3ff); @@ -2754,6 +3136,19 @@ box-shadow: 0 0 0 1px color-mix(in srgb, var(--mjr-accent, #5fb3ff) 25%, transparent); } +.mjr-mfv .mjr-icon-btn.is-open { + border-color: color-mix(in srgb, var(--mjr-accent, #5fb3ff) 62%, #ffffff 12%); + color: #eef7ff; + background: linear-gradient(180deg, rgba(95, 179, 255, 0.28), rgba(95, 179, 255, 0.14)); + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.08), + 0 0 0 1px color-mix(in srgb, var(--mjr-accent, #5fb3ff) 26%, transparent); +} + +.mjr-mfv.mjr-mfv--popped .mjr-icon-btn.is-open { + color: #ffffff !important; +} + /* Gen info dropdown panel */ .mjr-mfv-gen-dropdown { position: absolute; diff --git a/js/utils/deleteGuard.js b/js/utils/deleteGuard.js index 20fe4b43..0cce3eec 100644 --- a/js/utils/deleteGuard.js +++ b/js/utils/deleteGuard.js @@ -1,11 +1,13 @@ import { APP_CONFIG } from "../app/config.js"; import { comfyConfirm } from "../app/dialogs.js"; +import { t } from "../app/i18n.js"; export async function confirmDeletion(count, label) { if (!APP_CONFIG.DELETE_CONFIRMATION) return true; - const target = count > 1 ? `${count} files` : `"${label || "this file"}"`; const message = count > 1 - ? `Delete ${count} selected files?` - : `Delete ${target}?`; - return comfyConfirm(message, "Majoor: Confirm delete"); + ? t("dialog.deleteSelectedFiles", "Delete {count} selected files?", { count }) + : t("dialog.deleteSingleFile", "Delete \"{label}\"?", { + label: String(label || t("label.thisFile", "this file")), + }); + return comfyConfirm(message, t("dialog.confirmDeleteTitle", "Majoor: Confirm delete")); } diff --git a/list.json b/list.json new file mode 100644 index 00000000..92d2980e --- /dev/null +++ b/list.json @@ -0,0 +1 @@ +{"ok": true, "data": {"assets": [{"id": 6, "filename": "ComfyU_00003_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyu_00003_.png", "kind": "image", "width": 1280, "height": 720, "duration": null, "size": 930777, "mtime": 1772672877, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image shows a silhouette of a dinosaur wearing headphones. The dinosaur is standing on its hind legs and appears to be listening to music. The background is a beautiful orange and pink sunset, with the sun setting in the distance. The overall mood of the image is peaceful and serene.", "auto_tags": ["3d-render"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 5, "filename": "ComfyU_00002_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyu_00002_.png", "kind": "image", "width": 832, "height": 1216, "duration": null, "size": 1419652, "mtime": 1772668041, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image is a digital illustration of a piece of cheese floating in the sky. The cheese is yellow in color and has a rectangular shape with rounded edges. It appears to be made of cheese and has small holes on its surface. The background is a deep purple color with small white dots scattered throughout. In the center of the image, there is a pink nebula, which is visible in the bottom right corner. The nebula is a bright pink color and is slightly blurred, giving the image a dreamy, ethereal quality.", "auto_tags": [], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 0, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 4, "filename": "ComfyU_00001_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyu_00001_.png", "kind": "image", "width": 1280, "height": 720, "duration": null, "size": 930777, "mtime": 1772662110, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image shows a silhouette of a dinosaur wearing headphones. The dinosaur is standing on its hind legs and appears to be listening to music. The background is a beautiful orange and pink sunset, with the sun setting in the distance. The overall mood of the image is peaceful and serene.", "auto_tags": ["3d-render"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 3, "filename": "ComfyUI_00003_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyui_00003_.png", "kind": "image", "width": 1280, "height": 720, "duration": null, "size": 930780, "mtime": 1772661621, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image shows a silhouette of a dinosaur wearing headphones. The dinosaur is standing on its hind legs and appears to be listening to music. The background is a beautiful orange and pink sunset, with the sun setting in the distance. The overall mood of the image is peaceful and serene.", "auto_tags": ["3d-render"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 2, "filename": "ComfyUI_00002_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyui_00002_.png", "kind": "image", "width": 512, "height": 512, "duration": null, "size": 319318, "mtime": 1772661222, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image is an underwater photograph of a soccer ball floating in the ocean. The ball is white with black stripes and has small holes in the center. It is surrounded by a group of small fish, including orange and blue fish, swimming around it. The background is dark blue, and there are rocks and coral visible in the bottom right corner of the image. The water is clear and the sun is shining brightly, creating a beautiful reflection on the surface of the water.", "auto_tags": ["nature", "3d-render"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 1, "filename": "ComfyUI_00001_.png", "subfolder": "", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\comfyui_00001_.png", "kind": "image", "width": 864, "height": 1536, "duration": null, "size": 1299001, "mtime": 1772660636, "rating": 0, "tags": [], "tags_text": "image_2d101a55jpg_553e93b706.jpg", "enhanced_caption": "The image shows a man sitting at a table in a restaurant. He is wearing a dark suit and a brown fedora. He has a serious expression on his face and is looking off to the side. The table is covered with a white tablecloth and there is a wine glass on the table. In the background, there are buildings and lights that appear to be lit up. A hand is visible on the left side of the image, reaching out towards the man.", "auto_tags": ["portrait"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 13, "filename": "ComfyUI_00002_test.png", "subfolder": "test", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\test\\comfyui_00002_test.png", "kind": "image", "width": 832, "height": 1216, "duration": null, "size": 1441882, "mtime": 1772386739, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "", "auto_tags": [], "has_ai_enhanced_caption": 0, "has_ai_auto_tags": 0, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 16, "filename": "Image_2d101a55jpg_eb711ec7a7.jpeg", "subfolder": "test", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\test\\image_2d101a55jpg_eb711ec7a7.jpeg", "kind": "image", "width": 864, "height": 1536, "duration": null, "size": 209749, "mtime": 1772132899, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "", "auto_tags": ["portrait", "food", "nsfw"], "has_ai_enhanced_caption": 0, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 0, "has_generation_data": 0, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 15, "filename": "Image_2d101a55jpg_553e93b706.jpeg", "subfolder": "test", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\test\\image_2d101a55jpg_553e93b706.jpeg", "kind": "image", "width": 864, "height": 1536, "duration": null, "size": 240103, "mtime": 1772132887, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "The image shows a man sitting at a table in a restaurant. He is wearing a dark suit and a brown fedora. He has a serious expression on his face and is looking off to the side. The table is covered with a white tablecloth and there is a wine glass on the table. In the background, there are buildings and lights that appear to be lit up. A hand is visible on the left side of the image, reaching out towards the man.", "auto_tags": ["portrait"], "has_ai_enhanced_caption": 1, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 0, "has_generation_data": 0, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}, {"id": 23, "filename": "260207_Test__00014_.png", "subfolder": "drive-download-20260208T181559Z-3-001", "filepath": "c:\\users\\ewald\\documents\\comfyui_vscode\\comfyui_windows_portable\\comfyui\\output\\drive-download-20260208t181559z-3-001\\260207_test__00014_.png", "kind": "image", "width": 1280, "height": 720, "duration": null, "size": 957948, "mtime": 1770574596, "rating": 0, "tags": [], "tags_text": "", "enhanced_caption": "", "auto_tags": ["3d-render"], "has_ai_enhanced_caption": 0, "has_ai_auto_tags": 1, "has_ai_vector": 1, "has_ai_info": 1, "has_workflow": 1, "has_generation_data": 1, "generation_time_ms": null, "file_creation_time": null, "file_birth_time": null, "type": "output"}], "limit": 10, "offset": 0, "query": "*", "total": 23, "sort": "mtime_desc", "scope": "output"}, "error": null, "code": "OK", "meta": {}} \ No newline at end of file diff --git a/mjr_am_backend/adapters/db/sqlite_facade.py b/mjr_am_backend/adapters/db/sqlite_facade.py index 5405c5a2..d0d8402c 100644 --- a/mjr_am_backend/adapters/db/sqlite_facade.py +++ b/mjr_am_backend/adapters/db/sqlite_facade.py @@ -444,6 +444,16 @@ class Sqlite: def __init__(self, db_path: str, max_connections: int | None = None, timeout: float = 30.0, *, attach: dict[str, str] | None = None): self.db_path = Path(db_path) self._attach_dbs: dict[str, str] = dict(attach) if attach else {} + if "vec" not in self._attach_dbs: + # Keep vector DB colocated with the primary DB by default so schema + # self-heal works even when callers don't pass attach={"vec": ...}. + default_vec = self.db_path.with_name("vectors.sqlite") + try: + if default_vec.resolve(strict=False) != self.db_path.resolve(strict=False): + self._attach_dbs["vec"] = str(default_vec) + except Exception: + if default_vec != self.db_path: + self._attach_dbs["vec"] = str(default_vec) user_config = self._load_user_db_config() max_conn = ( int(max_connections) diff --git a/mjr_am_backend/config.py b/mjr_am_backend/config.py index d0ebbba1..052887c6 100644 --- a/mjr_am_backend/config.py +++ b/mjr_am_backend/config.py @@ -479,16 +479,43 @@ VECTOR_INDEX_DIR = str(VECTOR_INDEX_DIR_PATH) # Embedding dimensionality (SigLIP SO400M โ†’ 1152, base โ†’ 768). VECTOR_EMBEDDING_DIM = _env_int(1152, "MJR_AM_VECTOR_DIM", min_value=64, max_value=4096) +# Pre-warm the text-query model path and Faiss index during startup prewarm. +VECTOR_PREWARM_ON_STARTUP = _env_bool( + True, + "MJR_AM_VECTOR_PREWARM_ON_STARTUP", + "MAJOOR_VECTOR_PREWARM_ON_STARTUP", +) + # Auto-tagging: minimum cosine similarity to assign a tag. VECTOR_AUTOTAG_THRESHOLD = _env_float( - 0.25, + 0.06, "MJR_AM_VECTOR_AUTOTAG_THRESHOLD", min_value=0.0, max_value=1.0, ) +# Semantic text search: score floor and relative cutoff versus best hit. +VECTOR_TEXT_SEARCH_MIN_SCORE = _env_float( + 0.02, + "MJR_AM_VECTOR_TEXT_SEARCH_MIN_SCORE", + min_value=0.0, + max_value=1.0, +) +VECTOR_TEXT_SEARCH_RELATIVE_RATIO = _env_float( + 0.70, + "MJR_AM_VECTOR_TEXT_SEARCH_RELATIVE_RATIO", + min_value=0.0, + max_value=1.0, +) + # Maximum number of similar results returned by "Find Similar". VECTOR_SIMILAR_TOPK = _env_int(20, "MJR_AM_VECTOR_SIMILAR_TOPK", min_value=1, max_value=200) +VECTOR_SIMILAR_MIN_SCORE = _env_float( + 0.60, + "MJR_AM_VECTOR_SIMILAR_MIN_SCORE", + min_value=0.0, + max_value=1.0, +) # Number of threads used by Faiss for search (0 = automatic). VECTOR_FAISS_NPROBE = _env_int(0, "MJR_AM_VECTOR_FAISS_NPROBE", min_value=0, max_value=128) diff --git a/mjr_am_backend/features/index/index_prepare_ops.py b/mjr_am_backend/features/index/index_prepare_ops.py index a15e743f..e3f14311 100644 --- a/mjr_am_backend/features/index/index_prepare_ops.py +++ b/mjr_am_backend/features/index/index_prepare_ops.py @@ -15,6 +15,18 @@ from .metadata_helpers import MetadataHelpers from .scan_batch_utils import compute_state_hash, is_incremental_unchanged, normalize_filepath_str +async def _asset_missing_vector(scanner: Any, *, asset_id: int) -> bool: + checker = getattr(scanner, "_asset_has_vector", None) + if not callable(checker): + return False + try: + return not bool(await checker(asset_id=asset_id)) + except TypeError: + return not bool(await checker(asset_id)) + except Exception: + return False + + async def prepare_metadata_for_entry( scanner: Any, *, @@ -142,6 +154,9 @@ async def maybe_skip_prepare_for_incremental( cached_result = await refresh_entry_from_cached_metadata(scanner, prepare_ctx=prepare_ctx, file_path=file_path, fast=fast) if cached_result is not None: return cached_result + existing_id = int(prepare_ctx.get("existing_id") or 0) + if existing_id and await _asset_missing_vector(scanner, asset_id=existing_id): + return None if await scanner._asset_has_rich_metadata(asset_id=prepare_ctx["existing_id"]): return Result.Ok({"action": "skipped"}) return None @@ -160,9 +175,13 @@ async def should_skip_by_journal_state( return False if str(prepare_ctx["journal_state_hash"]) != prepare_ctx["state_hash"]: return False - if fast: - return True existing_id = int(prepare_ctx.get("existing_id") or 0) + if fast: + if existing_id and await _asset_missing_vector(scanner, asset_id=existing_id): + return False + return True + if existing_id and await _asset_missing_vector(scanner, asset_id=existing_id): + return False return bool(existing_id and await scanner._asset_has_rich_metadata(asset_id=existing_id)) diff --git a/mjr_am_backend/features/index/scanner.py b/mjr_am_backend/features/index/scanner.py index d84681a1..b96494df 100644 --- a/mjr_am_backend/features/index/scanner.py +++ b/mjr_am_backend/features/index/scanner.py @@ -9,7 +9,7 @@ from typing import Any from ...adapters.db.sqlite import Sqlite from ...config import MAX_TO_ENRICH_ITEMS, is_vector_index_on_scan_enabled -from ...shared import get_logger +from ...shared import FileKind, get_logger from ..metadata import MetadataService from .fs_walker import SCAN_IOPS_LIMIT, FileSystemWalker from .index_batching import append_batch_metadata_entries, existing_map_for_batch, journal_map_for_batch, prefetch_batch_cache_and_rich_meta, prefetch_metadata_cache_rows, prefetch_rich_metadata_rows, prepare_batch_entries, prepare_single_batch_entry, resolve_existing_state_for_batch @@ -108,37 +108,93 @@ class IndexScanner: added_ids=added_ids, is_fatal_db_error=_is_fatal_db_error, ) - self._schedule_added_image_vector_index(prev_added_count=prev_added_count, added_ids=added_ids) + self._schedule_prepared_vector_index( + prepared=prepared, + prev_added_count=prev_added_count, + added_ids=added_ids, + ) def set_vector_services(self, vector_service: Any, vector_searcher: Any | None = None) -> None: self._vector_service = vector_service self._vector_searcher = vector_searcher + async def _asset_has_vector(self, *, asset_id: int) -> bool: + try: + asset_id_int = int(asset_id) + except Exception: + return False + if asset_id_int <= 0: + return False + result = await self.db.aquery( + """ + SELECT 1 + FROM vec.asset_embeddings + WHERE asset_id = ? + AND vector IS NOT NULL + AND length(vector) > 0 + LIMIT 1 + """, + (asset_id_int,), + ) + return bool(result.ok and result.data) + def _schedule_added_image_vector_index(self, *, prev_added_count: int, added_ids: list[int] | None) -> None: + self._schedule_prepared_vector_index( + prepared=None, + prev_added_count=prev_added_count, + added_ids=added_ids, + ) + + def _schedule_prepared_vector_index( + self, + *, + prepared: list[dict[str, Any]] | None, + prev_added_count: int, + added_ids: list[int] | None, + ) -> None: if not is_vector_index_on_scan_enabled(): return - if self._vector_service is None or not isinstance(added_ids, list) or len(added_ids) <= prev_added_count: + if self._vector_service is None: return - new_ids: list[int] = [] - for aid in added_ids[prev_added_count:]: + candidate_ids: list[int] = [] + seen_ids: set[int] = set() + + def _append_asset_id(raw_id: Any) -> None: try: - parsed_id = int(aid) + parsed_id = int(raw_id) except Exception: - continue - if parsed_id > 0: - new_ids.append(parsed_id) - if not new_ids: + return + if parsed_id <= 0 or parsed_id in seen_ids: + return + seen_ids.add(parsed_id) + candidate_ids.append(parsed_id) + + if isinstance(added_ids, list): + for aid in added_ids[prev_added_count:]: + _append_asset_id(aid) + + if isinstance(prepared, list): + for entry in prepared: + action = str(entry.get("action") or "").strip().lower() + if action not in {"refresh", "updated"}: + continue + _append_asset_id(entry.get("asset_id")) + + if not candidate_ids: return try: - task = asyncio.create_task(self._index_added_image_vectors(asset_ids=new_ids)) + task = asyncio.create_task(self._index_missing_asset_vectors(asset_ids=candidate_ids)) self._vector_index_tasks.add(task) task.add_done_callback(self._vector_index_tasks.discard) except Exception as exc: logger.debug("Scanner vector indexing scheduling failed: %s", exc) async def _index_added_image_vectors(self, *, asset_ids: list[int]) -> None: + await self._index_missing_asset_vectors(asset_ids=asset_ids) + + async def _index_missing_asset_vectors(self, *, asset_ids: list[int]) -> None: if self._vector_service is None or not isinstance(asset_ids, list) or not asset_ids: return @@ -148,8 +204,11 @@ class IndexScanner: """ SELECT a.id, a.filepath, a.kind, m.metadata_raw FROM assets a + LEFT JOIN vec.asset_embeddings ae ON ae.asset_id = a.id LEFT JOIN asset_metadata m ON a.id = m.asset_id - WHERE a.kind IN ('image', 'video') AND {IN_CLAUSE} + WHERE a.kind IN ('image', 'video') + AND (ae.asset_id IS NULL OR ae.vector IS NULL OR length(ae.vector) = 0) + AND {IN_CLAUSE} """, "a.id", asset_ids, @@ -195,6 +254,8 @@ class IndexScanner: aid = int(entry.get("asset_id") or 0) try: filepath = str(entry.get("filepath") or "") + entry_kind_raw = str(entry.get("kind") or "").strip().lower() + entry_kind: FileKind = "video" if entry_kind_raw == "video" else "image" if not filepath: skipped += 1 continue @@ -204,7 +265,7 @@ class IndexScanner: self._vector_service, asset_id=aid, filepath=filepath, - kind=str(entry.get("kind") or "image"), + kind=entry_kind, metadata_raw=entry.get("metadata_raw"), ), timeout=_VECTOR_INDEX_PER_ASSET_TIMEOUT_S, diff --git a/mjr_am_backend/features/index/vector_indexer.py b/mjr_am_backend/features/index/vector_indexer.py index 924ddabe..66d8fef7 100644 --- a/mjr_am_backend/features/index/vector_indexer.py +++ b/mjr_am_backend/features/index/vector_indexer.py @@ -52,14 +52,35 @@ _A1111_PARAMS_RE = re.compile( re.IGNORECASE, ) +# Regex to split prompts into semantic segments (commas, periods, semicolons) +_SEGMENT_SPLIT_RE = re.compile(r"[,;.]+") +_MIN_SEGMENT_CHARS = 4 # ignore segments shorter than this + # โ”€โ”€ Score calibration constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# Raw SigLIP2 cosine similarity for well-aligned image-text pairs typically -# falls in the 0.15โ€“0.45 range. We remap this to a 0โ€“1 scale so that -# the UI percentage is meaningful to users. -_CALIB_LOW = 0.15 # raw score mapped to 0 (floor) -_CALIB_HIGH = 0.45 # raw score mapped to 1 (ceiling) +# SigLIP2 native features (get_image_features / get_text_features) produce +# L2-normalised embeddings whose cosine similarity for well-aligned pairs +# typically falls in the โˆ’0.06 to +0.05 range (sigmoid loss โ†’ lower raw +# values than CLIP). We remap this band to a 0โ€“1 UI scale. +_CALIB_LOW = 0.00 # raw score mapped to 0 (floor) +_CALIB_HIGH = 0.06 # raw score mapped to 1 (ceiling) _NEG_PENALTY_WEIGHT = 0.25 # how much negative-prompt match penalises the score +# โ”€โ”€ Multi-signal fusion weights โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +_W_IMAGE_TEXT = 0.60 # weight for imageโ†”text multi-segment score +_W_CAPTION_TEXT = 0.20 # weight for enhanced_captionโ†”prompt text similarity +_W_SEMANTIC = 0.20 # weight for semantic dimension decomposition +_SEGMENT_MAX_BOOST = 0.10 # bonus factor for best-matching segment + +# โ”€โ”€ Semantic dimension categories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Each dimension has a prefix used to construct a probing prompt. +_SEMANTIC_DIMENSIONS: dict[str, str] = { + "subject": "a photo depicting", + "style": "an artwork in the style of", + "medium": "created using the medium of", + "mood": "an image with the mood of", + "color": "an image with the color palette of", +} + # โ”€โ”€ Canonical auto-tag vocabulary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Each entry maps a human-readable tag to a text prompt. # The prompt is intentionally verbose to improve multimodal classification accuracy. @@ -165,20 +186,24 @@ async def index_asset_vector( vector = emb_result.data - # 2. Prompt-image alignment score (optional) - aesthetic = await _compute_prompt_alignment(vs, vector, metadata_raw) + # 2. Enhanced caption (Florence-2) for image assets (best-effort). + # Generated BEFORE alignment so it can contribute to the score. + caption: str | None = None + if kind == "image": + caption = await _generate_and_store_caption(db, vs, asset_id, filepath) - # 3. Persist embedding + # 3. Prompt-image alignment score (optional, uses caption if available) + aesthetic = await _compute_prompt_alignment( + vs, vector, metadata_raw, enhanced_caption=caption, + ) + + # 4. Persist embedding blob = vector_to_blob(vector) store_result = await _store_embedding(db, asset_id, blob, aesthetic, vs._model_name) if not store_result.ok: return store_result - # 3b. Enhanced caption (Florence-2) for image assets (best-effort). - if kind == "image": - await _try_store_enhanced_caption(db, vs, asset_id, filepath) - - # 4. Auto-tagging + # 5. Auto-tagging await _apply_autotags(db, vs, asset_id, vector) return Result.Ok(True) @@ -233,18 +258,37 @@ async def compute_prompt_alignment( prompt: str, *, negative_prompt: str | None = None, + enhanced_caption: str | None = None, ) -> Result[float]: """Compute calibrated alignment between an image embedding and a prompt. Returns a float in [0, 1]. Higher means the image closely matches the - textual prompt. If *negative_prompt* is provided, its similarity to the - image is subtracted (weighted) to penalise unwanted content leaking in. - """ - text_result = await vs.get_text_embedding(prompt) - if not text_result.ok or not text_result.data: - return Result.Err("METADATA_FAILED", text_result.error or "Text embedding failed") - raw_pos = VectorService.cosine_similarity(asset_embedding, text_result.data) + textual prompt. + Three signals are fused: + 1. **Multi-segment imageโ†”text** โ€” the prompt is split into segments; + each segment is scored against the image embedding, then combined + as a length-weighted average with a best-segment bonus. + 2. **Captionโ†”prompt text** โ€” if an enhanced caption is available, its + text embedding is compared to the prompt embedding for a textโ†”text + coherence signal. + 3. **Semantic decomposition** โ€” key concepts (subject, style, medium, + mood, color) are extracted and each dimension is scored separately + against the image embedding. + + If *negative_prompt* is provided, its similarity to the image is + subtracted (weighted) to penalise unwanted content leaking in. + """ + # โ”€โ”€ 1. Multi-segment imageโ†”text score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + raw_image_text = await _multi_segment_score(vs, asset_embedding, prompt) + if raw_image_text is None: + # Fallback: single-embedding score + text_result = await vs.get_text_embedding(prompt) + if not text_result.ok or not text_result.data: + return Result.Err("METADATA_FAILED", text_result.error or "Text embedding failed") + raw_image_text = VectorService.cosine_similarity(asset_embedding, text_result.data) + + # โ”€โ”€ Negative prompt penalty โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ neg_penalty = 0.0 if negative_prompt: neg_result = await vs.get_text_embedding(negative_prompt) @@ -252,10 +296,181 @@ async def compute_prompt_alignment( raw_neg = VectorService.cosine_similarity(asset_embedding, neg_result.data) neg_penalty = max(0.0, raw_neg) * _NEG_PENALTY_WEIGHT - calibrated = _calibrate_score(raw_pos - neg_penalty) + adjusted_image_text = raw_image_text - neg_penalty + + # โ”€โ”€ 2. Captionโ†”prompt text similarity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + caption_score: float | None = None + if enhanced_caption and enhanced_caption.strip(): + caption_score = await _caption_prompt_similarity(vs, enhanced_caption, prompt) + + # โ”€โ”€ 3. Semantic decomposition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + semantic_score = await _semantic_dimension_score(vs, asset_embedding, prompt) + + # โ”€โ”€ Weighted fusion โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + total_weight = _W_IMAGE_TEXT + weighted_sum = adjusted_image_text * _W_IMAGE_TEXT + + if caption_score is not None: + weighted_sum += caption_score * _W_CAPTION_TEXT + total_weight += _W_CAPTION_TEXT + + if semantic_score is not None: + weighted_sum += semantic_score * _W_SEMANTIC + total_weight += _W_SEMANTIC + + fused_raw = weighted_sum / total_weight if total_weight > 0 else adjusted_image_text + calibrated = _calibrate_score(fused_raw) return Result.Ok(round(calibrated, 4)) +async def _multi_segment_score( + vs: VectorService, + asset_embedding: list[float], + prompt: str, +) -> float | None: + """Split prompt into segments and compute a weighted similarity score. + + Each segment is embedded independently and scored against the image. + The final score is a length-weighted average with a bonus for the + best-matching segment, which captures the "main subject" better. + """ + segments = [s.strip() for s in _SEGMENT_SPLIT_RE.split(prompt) if len(s.strip()) >= _MIN_SEGMENT_CHARS] + if len(segments) <= 1: + # Not worth splitting โ€” return None to use the full prompt + return None + + scores: list[tuple[float, int]] = [] # (similarity, char_length) + for seg in segments: + emb = await vs.get_text_embedding(seg) + if emb.ok and emb.data: + sim = VectorService.cosine_similarity(asset_embedding, emb.data) + scores.append((sim, len(seg))) + + if not scores: + return None + + # Length-weighted average + total_len = sum(length for _, length in scores) + if total_len == 0: + return None + weighted_avg = sum(sim * length for sim, length in scores) / total_len + + # Boost from best segment + best_sim = max(sim for sim, _ in scores) + return weighted_avg + (best_sim - weighted_avg) * _SEGMENT_MAX_BOOST + + +async def _caption_prompt_similarity( + vs: VectorService, + caption: str, + prompt: str, +) -> float | None: + """Textโ†”text cosine similarity between enhanced caption and prompt.""" + cap_emb = await vs.get_text_embedding(caption) + if not cap_emb.ok or not cap_emb.data: + return None + prompt_emb = await vs.get_text_embedding(prompt) + if not prompt_emb.ok or not prompt_emb.data: + return None + return VectorService.cosine_similarity(cap_emb.data, prompt_emb.data) + + +async def _semantic_dimension_score( + vs: VectorService, + asset_embedding: list[float], + prompt: str, +) -> float | None: + """Score the image against semantic concept dimensions extracted from the prompt. + + For each semantic dimension (subject, style, medium, mood, color), + we construct a probing prompt " " and + measure its similarity to the image embedding. The final score is + the average of the individual dimension scores (only counting + dimensions where a concept was actually extracted). + """ + concepts = _extract_semantic_concepts(prompt) + if not concepts: + return None + + dim_scores: list[float] = [] + for dim, concept in concepts.items(): + prefix = _SEMANTIC_DIMENSIONS.get(dim, "") + probe = f"{prefix} {concept}" if prefix else concept + emb = await vs.get_text_embedding(probe) + if emb.ok and emb.data: + sim = VectorService.cosine_similarity(asset_embedding, emb.data) + dim_scores.append(sim) + + return sum(dim_scores) / len(dim_scores) if dim_scores else None + + +def _extract_semantic_concepts(prompt: str) -> dict[str, str]: + """Heuristic extraction of semantic dimensions from a prompt. + + Attempts to identify subject, style, medium, mood, and color palette + concepts by matching known keywords. Returns a dict of dimensionโ†’concept + for the dimensions that were detected. + """ + lower = prompt.lower() + concepts: dict[str, str] = {} + + # Subject: first segment (usually the main subject) + segments = [s.strip() for s in _SEGMENT_SPLIT_RE.split(prompt) if len(s.strip()) >= _MIN_SEGMENT_CHARS] + if segments: + concepts["subject"] = segments[0] + + # Style keywords + style_kws = ( + "anime", "manga", "photorealistic", "realistic", "cartoon", "oil painting", + "watercolor", "impressionist", "surrealist", "art nouveau", "art deco", + "pixel art", "comic", "3d render", "concept art", "digital art", + "cinematic", "studio ghibli", "ukiyo-e", "pop art", "minimalist", + "gothic", "baroque", "renaissance", "cubist", "futuristic", + ) + for kw in style_kws: + if kw in lower: + concepts["style"] = kw + break + + # Medium keywords (longer/compound keywords first to avoid substring + # matches like "painting" taking precedence over "watercolor painting"). + medium_kws = ( + "oil painting", "watercolor", "photograph", "photo", "illustration", + "painting", "drawing", "sketch", "render", "sculpture", "collage", + "engraving", "print", "pencil", "charcoal", "pastel", "acrylic", + "fresco", "mosaic", "tapestry", + ) + for kw in medium_kws: + if kw in lower: + concepts["medium"] = kw + break + + # Mood keywords + mood_kws = ( + "dark", "bright", "moody", "cheerful", "mysterious", "serene", + "dramatic", "peaceful", "ethereal", "gloomy", "vibrant", "melancholic", + "nostalgic", "whimsical", "epic", "dreamy", "eerie", "cozy", + "romantic", "surreal", "ominous", "tranquil", + ) + for kw in mood_kws: + if kw in lower: + concepts["mood"] = kw + break + + # Color palette keywords + color_kws = ( + "monochrome", "black and white", "pastel colors", "neon", "warm tones", + "cool tones", "golden hour", "blue hour", "sepia", "high contrast", + "muted colors", "saturated", "desaturated", "earth tones", + ) + for kw in color_kws: + if kw in lower: + concepts["color"] = kw + break + + return concepts + + async def generate_enhanced_prompt( db: Sqlite, vs: VectorService, @@ -314,6 +529,8 @@ async def _compute_prompt_alignment( vs: VectorService, vector: list[float], metadata_raw: dict[str, Any] | None, + *, + enhanced_caption: str | None = None, ) -> float | None: """Extract the original prompt from metadata and compute alignment.""" if not metadata_raw: @@ -324,7 +541,11 @@ async def _compute_prompt_alignment( return None negative = _extract_negative_prompt(metadata_raw) - result = await compute_prompt_alignment(vs, vector, prompt, negative_prompt=negative) + result = await compute_prompt_alignment( + vs, vector, prompt, + negative_prompt=negative, + enhanced_caption=enhanced_caption, + ) return result.data if result.ok else None @@ -522,6 +743,34 @@ async def _try_store_enhanced_caption( logger.debug("Enhanced caption generation skipped for asset %d: %s", asset_id, exc) +async def _generate_and_store_caption( + db: Sqlite, + vs: VectorService, + asset_id: int, + filepath: str, +) -> str | None: + """Generate, store, and return the enhanced caption (or None on failure).""" + try: + generated = await vs.generate_enhanced_caption(filepath) + if not generated.ok or not generated.data: + return None + fallback_title = ( + Path(filepath).stem.replace("_", " ").replace("-", " ").strip() + or "Untitled" + ) + caption = _normalise_title_caption( + str(generated.data).strip(), + fallback_title=fallback_title, + ) + if not caption: + return None + await _store_enhanced_caption(db, asset_id, caption) + return caption + except Exception as exc: + logger.debug("Enhanced caption generation skipped for asset %d: %s", asset_id, exc) + return None + + async def _apply_autotags( db: Sqlite, vs: VectorService, diff --git a/mjr_am_backend/features/index/vector_searcher.py b/mjr_am_backend/features/index/vector_searcher.py index 638dd656..3199e0c2 100644 --- a/mjr_am_backend/features/index/vector_searcher.py +++ b/mjr_am_backend/features/index/vector_searcher.py @@ -53,6 +53,25 @@ class VectorSearcher: """Mark the in-memory index as stale (will be rebuilt on next query).""" self._dirty = True + async def prewarm_index(self) -> Result[dict[str, Any]]: + """Build the in-memory Faiss index ahead of the first semantic query.""" + try: + await self._ensure_index() + except Exception as exc: + return Result.Err("SERVICE_UNAVAILABLE", f"Vector index warmup failed: {exc}") + + total = 0 + try: + total = int(getattr(self._index, "ntotal", 0) or 0) + except Exception: + total = 0 + + return Result.Ok({ + "loaded": bool(self._index is not None), + "total": total, + "dirty": bool(self._dirty), + }) + async def _ensure_index(self) -> None: """Build (or rebuild) the Faiss index from the database.""" if not self._dirty and self._index is not None: diff --git a/mjr_am_backend/features/index/vector_service.py b/mjr_am_backend/features/index/vector_service.py index 27cfdf75..9412cd10 100644 --- a/mjr_am_backend/features/index/vector_service.py +++ b/mjr_am_backend/features/index/vector_service.py @@ -236,14 +236,69 @@ def _extract_frame_at(video_path: str, timestamp: float) -> PILImage.Image | Non return None +def _extract_keyframes_cv2(video_path: str, interval: float) -> list[PILImage.Image]: + """Fallback: extract key-frames using OpenCV when ffprobe/ffmpeg are unavailable.""" + try: + import cv2 + from PIL import Image as PILImage # noqa: F811 + except ImportError: + return [] + + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + logger.debug("cv2.VideoCapture failed to open %s", video_path) + return [] + + try: + fps = cap.get(cv2.CAP_PROP_FPS) or 24.0 + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0) + duration = total_frames / fps if fps > 0 and total_frames > 0 else 0.0 + + if duration <= 0: + # Try a single frame + ret, frame_bgr = cap.read() + if ret and frame_bgr is not None: + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + return [PILImage.fromarray(frame_rgb)] + return [] + + timestamps: list[float] = [] + t = 0.0 + while t < duration: + timestamps.append(t) + t += interval + if duration > interval and (duration / 2) not in timestamps: + timestamps.append(duration / 2) + timestamps.sort() + + frames: list[PILImage.Image] = [] + for ts in timestamps: + frame_no = int(ts * fps) + cap.set(cv2.CAP_PROP_POS_FRAMES, frame_no) + ret, frame_bgr = cap.read() + if ret and frame_bgr is not None: + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + frames.append(PILImage.fromarray(frame_rgb)) + return frames + finally: + cap.release() + + def extract_keyframes(video_path: str, interval: float | None = None) -> list[PILImage.Image]: - """Return a list of key-frame PIL Images sampled every *interval* seconds.""" + """Return a list of key-frame PIL Images sampled every *interval* seconds. + + Tries ffprobe/ffmpeg first, then falls back to OpenCV (cv2) if the + ffmpeg-based extraction yields no frames. + """ interval = interval or VECTOR_VIDEO_KEYFRAME_INTERVAL duration = _extract_video_duration(video_path) if duration is None or duration <= 0: # Fallback: try a single frame at t=0 frame = _extract_frame_at(video_path, 0.0) - return [frame] if frame else [] + if frame: + return [frame] + # ffmpeg unavailable โ€” try OpenCV + return _extract_keyframes_cv2(video_path, interval) timestamps = [] t = 0.0 @@ -260,6 +315,9 @@ def extract_keyframes(video_path: str, interval: float | None = None) -> list[PI frame = _extract_frame_at(video_path, ts) if frame is not None: frames.append(frame) + # If ffmpeg extraction failed for every frame, fall back to OpenCV + if not frames: + frames = _extract_keyframes_cv2(video_path, interval) return frames @@ -299,6 +357,8 @@ class VectorService: self._prompt_model: Any | None = None self._prompt_processor: Any | None = None self._prompt_lock = asyncio.Lock() + self._text_query_warmed = False + self._text_query_warm_lock = asyncio.Lock() self._dim = VECTOR_EMBEDDING_DIM self._last_error: str = "" self._last_error_at: str | None = None @@ -779,12 +839,40 @@ class VectorService: self._last_error = "" self._last_error_at = None + async def prewarm_text_queries(self, probe_text: str = "warmup") -> Result[dict[str, Any]]: + """Preload the text-query path so the first AI search is not cold.""" + if self._text_query_warmed: + return Result.Ok(self.get_runtime_status()) + + async with self._text_query_warm_lock: + if self._text_query_warmed: + return Result.Ok(self.get_runtime_status()) + + result = await self.get_text_embedding(probe_text) + if not result.ok or not result.data: + return Result.Err( + result.code or "SERVICE_UNAVAILABLE", + result.error or "Vector text-query warmup failed", + ) + + self._text_query_warmed = True + return Result.Ok(self.get_runtime_status()) + def get_runtime_status(self) -> dict[str, Any]: + siglip_loaded = bool(self._siglip_model is not None and self._siglip_processor is not None) + sentence_transformer_loaded = bool(self._model is not None) + prompt_model_loaded = bool(self._prompt_model is not None and self._prompt_processor is not None) + video_model_loaded = bool(self._video_model is not None and self._video_processor is not None) return { "model_name": self._model_name, "video_model_name": self._video_model_name, "prompt_model_name": self._prompt_model_name, - "loaded": bool(self._model is not None), + "loaded": bool(sentence_transformer_loaded or siglip_loaded), + "sentence_transformer_loaded": sentence_transformer_loaded, + "siglip_loaded": siglip_loaded, + "video_model_loaded": video_model_loaded, + "prompt_model_loaded": prompt_model_loaded, + "text_query_warmed": bool(self._text_query_warmed), "dim": int(self._dim), "last_error": self._last_error or None, "last_error_at": self._last_error_at, @@ -1167,6 +1255,36 @@ class VectorService: if not frames: return Result.Err("UNSUPPORTED", "No frames could be extracted from video") + # Use the same native SigLIP2 path that works for image embeddings + # to encode each keyframe, then average. + if self._use_native_siglip(): + try: + import numpy as np + import torch + + processor, native_model = await self._ensure_siglip_components() + + def _embed_frames_native() -> list[float]: + vecs = [] + with torch.inference_mode(): + for frame in frames: + inputs = processor(images=[frame], return_tensors="pt") + feats = native_model.get_image_features(**inputs) + arr = feats.detach().cpu().numpy()[0] + vecs.append(_normalise_vector(arr)) + mean_vec = np.mean(vecs, axis=0) + vec = _normalise_vector(mean_vec) + return _coerce_vector_dim(vec, self._dim) + + vec = await asyncio.to_thread(_embed_frames_native) + self._clear_error() + return Result.Ok(vec) + except Exception as exc: + self._record_error(f"Video embedding failed for {path.name}: {exc}") + logger.debug("Native SigLIP video embedding failed for %s: %s", path.name, exc) + return Result.Err("METADATA_FAILED", f"Video embedding failed: {exc}") + + # Legacy SentenceTransformer fallback try: import numpy as np # noqa: F811 @@ -1201,17 +1319,19 @@ class VectorService: path.name, batch_exc, ) + + def _encode_single_frame(frame_obj: Any) -> Any: + return _encode_quiet( + model, + cast(Any, [frame_obj]), + convert_to_numpy=True, + batch_size=1, + show_progress_bar=False, + ) + vecs = [] for frame in frames: - encoded = await asyncio.to_thread( - lambda fr=frame: _encode_quiet( - model, - cast(Any, [fr]), - convert_to_numpy=True, - batch_size=1, - show_progress_bar=False, - ) - ) + encoded = await asyncio.to_thread(_encode_single_frame, frame) vecs.append(_coerce_first_vector(encoded)) mean_vec = np.mean(vecs, axis=0) @@ -1538,73 +1658,38 @@ class VectorService: if not rgb_frames: raise RuntimeError("No RGB frames available for X-CLIP embedding") - def _get_field(container: Any, field: str) -> Any: - try: - getter = getattr(container, "get", None) - if callable(getter): - return getter(field) - except Exception: - pass - try: - return container[field] - except Exception: - pass - try: - return getattr(container, field) - except Exception: - return None - - def _try_processor(**kwargs: Any) -> Any: - try: - return processor(**kwargs) - except Exception: - return None + # X-CLIP expects exactly 8 frames; pad by repeating if fewer. + target_nframes = 8 + while len(rgb_frames) < target_nframes: + rgb_frames.append(rgb_frames[len(rgb_frames) % len(rgb_frames)]) with torch.inference_mode(): - inputs: Any | None = None - pixel_values: Any | None = None - # Some processor/model combos return non-mapping values for - # `images=[...]` (seen in the field). Probe fallback signatures - # and extract `pixel_values` defensively. - processor_calls: list[dict[str, Any]] = [ - {"images": rgb_frames, "return_tensors": "pt", "padding": True}, - {"videos": [rgb_frames], "return_tensors": "pt", "padding": True}, - {"videos": rgb_frames, "return_tensors": "pt", "padding": True}, - {"images": rgb_frames[0], "return_tensors": "pt"}, - ] - for kwargs in processor_calls: - out = _try_processor(**kwargs) - if out is None: - continue - inputs = out - pixel_values = _get_field(out, "pixel_values") - if pixel_values is None and hasattr(out, "shape") and hasattr(out, "dtype"): - pixel_values = out - if pixel_values is not None: - break + # XCLIPProcessor uses images= (not videos=) to build + # pixel_values with shape [batch, num_frames, C, H, W]. + inputs = processor(images=rgb_frames, return_tensors="pt") + pixel_values = inputs.get("pixel_values") + if pixel_values is None: + raise RuntimeError("X-CLIP processor returned no pixel_values") - if hasattr(model, "get_image_features"): - if pixel_values is None: - raise RuntimeError("X-CLIP processor returned no pixel_values") - feats = model.get_image_features(pixel_values=pixel_values) + if hasattr(model, "get_video_features"): + # get_video_features may return a tuple in some + # transformers versions; handle both cases. + out = model.get_video_features(pixel_values=pixel_values) + if isinstance(out, tuple): + feats = out[0] + else: + feats = out else: - model_inputs: dict[str, Any] = {} - if isinstance(inputs, Mapping): - try: - model_inputs = dict(inputs) - except Exception: - model_inputs = {} - if not model_inputs and pixel_values is not None: - model_inputs = {"pixel_values": pixel_values} - if not model_inputs: - raise RuntimeError("X-CLIP processor returned no usable model inputs") - out = model(**model_inputs) - feats = getattr(out, "pooler_output", None) + out = model(pixel_values=pixel_values) + feats = getattr(out, "video_embeds", None) + if feats is None: + feats = getattr(out, "pooler_output", None) if feats is None: feats = getattr(out, "last_hidden_state", None) if feats is None: raise RuntimeError("X-CLIP output does not expose usable features") feats = feats.mean(dim=1) + arr = feats.detach().cpu().numpy() mean_vec = np.mean(arr, axis=0) vec = _normalise_vector(mean_vec) diff --git a/mjr_am_backend/routes/core/services.py b/mjr_am_backend/routes/core/services.py index 62f2602d..f1c68b90 100644 --- a/mjr_am_backend/routes/core/services.py +++ b/mjr_am_backend/routes/core/services.py @@ -5,6 +5,7 @@ import asyncio import threading from typing import Any +from mjr_am_backend.config import VECTOR_PREWARM_ON_STARTUP from mjr_am_backend.deps import build_services from mjr_am_backend.shared import Result, get_logger @@ -100,6 +101,41 @@ def get_services_error(): return _services_error +async def _run_vector_prewarm_step(label: str, runner: Any) -> None: + try: + result = await runner() + except Exception as exc: + logger.warning("%s failed during startup prewarm: %s", label, exc) + return + + if not getattr(result, "ok", False): + logger.warning("%s failed during startup prewarm: %s", label, getattr(result, "error", None) or "unknown error") + return + + logger.info("%s prewarmed", label) + + +async def _prewarm_vector_components(services: dict[str, Any] | None) -> None: + if not VECTOR_PREWARM_ON_STARTUP or not isinstance(services, dict): + return + + vector_service = services.get("vector_service") + vector_searcher = services.get("vector_searcher") + steps: list[asyncio.Future[Any] | asyncio.Task[Any] | Any] = [] + + prewarm_text = getattr(vector_service, "prewarm_text_queries", None) + if callable(prewarm_text): + steps.append(_run_vector_prewarm_step("Vector text-query model", prewarm_text)) + + prewarm_index = getattr(vector_searcher, "prewarm_index", None) + if callable(prewarm_index): + steps.append(_run_vector_prewarm_step("Vector search index", prewarm_index)) + + if steps: + await asyncio.gather(*steps) + + async def prewarm_services() -> None: """Pre-warm services in the background so the first user request is fast.""" - await _build_services() + services = await _build_services() + await _prewarm_vector_components(services) diff --git a/mjr_am_backend/routes/handlers/assets_impl.py b/mjr_am_backend/routes/handlers/assets_impl.py index 26b01baf..c5b821b1 100644 --- a/mjr_am_backend/routes/handlers/assets_impl.py +++ b/mjr_am_backend/routes/handlers/assets_impl.py @@ -3,6 +3,7 @@ Asset management endpoints: ratings, tags, service retry. """ import asyncio import errno +import html as _html_mod import json import os import re @@ -58,6 +59,7 @@ logger = get_logger(__name__) MAX_TAGS_PER_ASSET = 50 MAX_TAG_LENGTH = 100 MAX_RENAME_LENGTH = 255 +USER_GUIDE_FILE_NAME = "user_guide.html" # P2-E-05/06 delegation to extracted modules. from ..assets import filename_validator as _fv @@ -76,6 +78,63 @@ _pg_build_download_response = _pg.build_download_response _pg_safe_download_filename = _pg.safe_download_filename +def _resolve_local_user_guide_path() -> Path: + try: + return (Path(__file__).resolve().parents[3] / USER_GUIDE_FILE_NAME).resolve(strict=False) + except Exception: + return Path(USER_GUIDE_FILE_NAME) + + +def _resolve_docs_dir() -> Path: + """Return the absolute path to the docs/ folder in the project root.""" + try: + return (Path(__file__).resolve().parents[3] / "docs").resolve(strict=False) + except Exception: + return Path("docs") + + +# Allowed filename pattern for doc files (letters, digits, hyphens, underscores, dots). +_DOC_FILENAME_RE = re.compile(r"^[A-Za-z0-9_\-]+\.md$") + +_DOCS_HTML_TEMPLATE = """\ + + + + + +{title} - Majoor Assets Manager Docs + + + +
+ ← Back to User Guide +

{title}

+
+
{content}
+ + +""" + def register_asset_routes(routes: web.RouteTableDef) -> None: """Register asset CRUD routes (get, delete, rename).""" def _filepath_db_keys(path_value: str | Path | None) -> tuple[str, ...]: @@ -732,6 +791,47 @@ def register_asset_routes(routes: web.RouteTableDef) -> None: ) return _json_response(result) + @routes.get("/mjr/am/user-guide") + async def open_local_user_guide(_request): + """ + Serve the local user_guide.html from this custom node folder. + """ + guide_path = _resolve_local_user_guide_path() + try: + if not guide_path.is_file(): + return web.Response(status=404, text="User guide not found") + return web.FileResponse(path=str(guide_path)) + except Exception as exc: + return _json_response( + Result.Err("FS_ERROR", _safe_error_message(exc, "Failed to open user guide")) + ) + + @routes.get("/mjr/am/docs/{filename}") + async def serve_doc_file(request): + """Serve a markdown documentation file from the docs/ folder.""" + filename = request.match_info.get("filename", "") + if not _DOC_FILENAME_RE.match(filename): + return web.Response(status=400, text="Invalid document filename") + docs_dir = _resolve_docs_dir() + doc_path = (docs_dir / filename).resolve() + # Path traversal guard: resolved path must stay inside docs_dir. + if not str(doc_path).startswith(str(docs_dir.resolve())): + return web.Response(status=403, text="Access denied") + if not doc_path.is_file(): + return web.Response(status=404, text="Document not found") + try: + content = doc_path.read_text(encoding="utf-8") + title = filename.replace("_", " ").replace(".md", "") + html_page = _DOCS_HTML_TEMPLATE.format( + title=_html_mod.escape(title), + content=_html_mod.escape(content), + ) + return web.Response(text=html_page, content_type="text/html") + except Exception as exc: + return _json_response( + Result.Err("FS_ERROR", _safe_error_message(exc, "Failed to read document")) + ) + @routes.get("/mjr/am/routes") async def list_routes(request): """List all available API routes.""" @@ -759,6 +859,8 @@ def register_asset_routes(routes: web.RouteTableDef) -> None: {"method": "POST", "path": "/mjr/am/assets/rename", "description": "Alias: rename a single asset file"}, {"method": "POST", "path": "/mjr/am/open-in-folder", "description": "Open asset in OS file manager"}, {"method": "GET", "path": "/mjr/am/tags", "description": "Get all unique tags for autocomplete"}, + {"method": "GET", "path": "/mjr/am/user-guide", "description": "Open local Assets Manager user guide"}, + {"method": "GET", "path": "/mjr/am/docs/{filename}", "description": "Serve a markdown documentation file"}, {"method": "POST", "path": "/mjr/am/retry-services", "description": "Retry service initialization"}, {"method": "GET", "path": "/mjr/am/routes", "description": "List all available routes (this endpoint)"}, ] diff --git a/mjr_am_backend/routes/handlers/db_maintenance.py b/mjr_am_backend/routes/handlers/db_maintenance.py index 8ad65c64..7cb1bf0e 100644 --- a/mjr_am_backend/routes/handlers/db_maintenance.py +++ b/mjr_am_backend/routes/handlers/db_maintenance.py @@ -661,7 +661,7 @@ async def _backfill_missing_asset_vectors( scope: str = "output", custom_root_id: str = "", on_progress: Any | None = None, -) -> Result[dict[str, int]]: +) -> Result[dict[str, int | str | None]]: """ Generate embeddings for existing assets that currently have no vector. diff --git a/mjr_am_backend/routes/handlers/hybrid_search.py b/mjr_am_backend/routes/handlers/hybrid_search.py index 25bdc5b3..2b3e37c2 100644 --- a/mjr_am_backend/routes/handlers/hybrid_search.py +++ b/mjr_am_backend/routes/handlers/hybrid_search.py @@ -19,11 +19,16 @@ import re from typing import Any from aiohttp import web +from ...config import VECTOR_TEXT_SEARCH_MIN_SCORE, VECTOR_TEXT_SEARCH_RELATIVE_RATIO from ...shared import Result, get_logger from ..core import _json_response, _require_services from ..core.security import _check_rate_limit -from .vector_search import _hydrate_vector_results, _require_vector_services +from .vector_search import ( + _filter_text_search_hits, + _hydrate_vector_results, + _require_vector_services, +) logger = get_logger(__name__) @@ -96,10 +101,20 @@ async def _filter_semantic_hits( filters: dict[str, Any], scope: str, custom_root_id: str | None, + min_score: float, + relative_ratio: float, ) -> list[dict[str, Any]]: if db is None or not isinstance(hits, list) or not hits: return hits if isinstance(hits, list) else [] + hits = _filter_text_search_hits( + list(hits), + min_score=min_score, + relative_ratio=relative_ratio, + ) + if not hits: + return [] + asset_ids: list[int] = [] for hit in hits: try: @@ -380,6 +395,8 @@ def register_hybrid_search_routes(routes: web.RouteTableDef) -> None: filters=inline_filters, scope=scope, custom_root_id=custom_root_id, + min_score=VECTOR_TEXT_SEARCH_MIN_SCORE, + relative_ratio=VECTOR_TEXT_SEARCH_RELATIVE_RATIO, ) except Exception: return [] diff --git a/mjr_am_backend/routes/handlers/scan.py b/mjr_am_backend/routes/handlers/scan.py index 933e7ef0..db5d3626 100644 --- a/mjr_am_backend/routes/handlers/scan.py +++ b/mjr_am_backend/routes/handlers/scan.py @@ -21,7 +21,7 @@ except Exception: folder_paths = _FolderPathsStub() # type: ignore -from mjr_am_backend.adapters.db.schema import rebuild_fts +from mjr_am_backend.adapters.db.schema import purge_orphan_vec_embeddings, rebuild_fts from mjr_am_backend.config import ( COLLECTIONS_DIR_PATH, INDEX_DIR_PATH, @@ -876,6 +876,7 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: reindex: bool (default: true) clear_scan_journal: bool (default: true) clear_metadata_cache: bool (default: true) + preserve_vectors: bool (default: false) rebuild_fts: bool (default: true) incremental: bool (default: false) fast: bool (default: true) @@ -941,6 +942,17 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: clear_metadata_cache = _bool_option(("clear_metadata_cache", "clearMetadataCache"), True) clear_asset_metadata = _bool_option(("clear_asset_metadata", "clearAssetMetadata"), True) clear_assets_table = _bool_option(("clear_assets", "clearAssets"), True) + preserve_vectors = _bool_option( + ( + "preserve_vectors", + "preserveVectors", + "keep_vectors", + "keepVectors", + "keep_embeddings", + "keepEmbeddings", + ), + False, + ) rebuild_fts_flag = _bool_option(("rebuild_fts", "rebuildFts"), True) incremental = _bool_option(("incremental",), False) fast = _bool_option(("fast",), True) @@ -965,6 +977,11 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: ), False, ) + if preserve_vectors: + # Preserve embeddings by keeping the assets table (asset_id continuity) + # and never escalating to a hard DB-file reset. + clear_assets_table = False + hard_reset_db = False output_root = await _runtime_output_root(svc) target_roots: list[dict[str, str | None]] = [] @@ -1253,6 +1270,19 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: if not res.ok: # Fallback: if scoped clear hits a malformed DB, escalate to hard adapter reset. if _is_db_malformed_result(res): + if preserve_vectors: + _emit_maintenance_status( + "failed", + "error", + "Database is malformed; preserving vectors is not possible. Retry with full reset.", + operation="reset_index", + ) + return _json_response( + Result.Err( + "DB_MALFORMED", + "Database is malformed; preserving vectors is not possible. Retry with full reset.", + ) + ) try: if scan_lock is not None and hasattr(scan_lock, "__aenter__"): async with scan_lock: @@ -1285,6 +1315,17 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: _emit_maintenance_status("failed", "error", res.error or "Index reset failed", operation="reset_index") return _json_response(res) + vectors_purged: bool | None = None + if clear_assets_table: + try: + purge_res = await purge_orphan_vec_embeddings(db) + vectors_purged = bool(purge_res.ok) + if not purge_res.ok: + logger.warning("Failed to purge orphan vectors during index reset: %s", purge_res.error) + except Exception as exc: + vectors_purged = False + logger.warning("Exception while purging orphan vectors during index reset: %s", exc) + # FULL RESET CLEANUP: VACUUM and Physical Files if scope == "all" and (clear_scan_journal or clear_metadata_cache): # 1. Vacuum DB to reclaim space and rebuild file @@ -1302,8 +1343,14 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: # IMPORTANT: when adding a new persistent state file to the index directory, # register its name (or prefix) below to prevent it from being wiped on # a full reindex. Using an explicit constant avoids silent data loss. - _KEEP_PREFIXES: frozenset[str] = frozenset({"assets.sqlite"}) # DB + WAL/SHM + keep_prefixes = {"assets.sqlite"} # DB + WAL/SHM + if preserve_vectors: + # Keep vectors DB (+ WAL/SHM) when user opted to preserve embeddings. + keep_prefixes.add("vectors.sqlite") + _KEEP_PREFIXES: frozenset[str] = frozenset(keep_prefixes) _KEEP_NAMES: frozenset[str] = frozenset({"custom_roots.json"}) + if preserve_vectors: + _KEEP_NAMES = frozenset(set(_KEEP_NAMES) | {"vectors"}) for item in INDEX_DIR_PATH.iterdir(): if any(item.name.startswith(p) for p in _KEEP_PREFIXES): continue @@ -1408,6 +1455,8 @@ def register_scan_routes(routes: web.RouteTableDef) -> None: Result.Ok( { "cleared": cleared, + "preserve_vectors": bool(preserve_vectors), + "vectors_purged": vectors_purged, "scan_summary": scan_summary, "rebuild_fts": rebuild_status, } diff --git a/mjr_am_backend/routes/handlers/vector_search.py b/mjr_am_backend/routes/handlers/vector_search.py index 0f8d5277..957811ab 100644 --- a/mjr_am_backend/routes/handlers/vector_search.py +++ b/mjr_am_backend/routes/handlers/vector_search.py @@ -12,7 +12,13 @@ import math from typing import Any from aiohttp import web -from ...config import VECTOR_SIMILAR_TOPK, is_vector_search_enabled +from ...config import ( + VECTOR_SIMILAR_MIN_SCORE, + VECTOR_SIMILAR_TOPK, + VECTOR_TEXT_SEARCH_MIN_SCORE, + VECTOR_TEXT_SEARCH_RELATIVE_RATIO, + is_vector_search_enabled, +) from ...shared import Result, get_logger from ..core import _json_response, _require_services @@ -188,6 +194,102 @@ async def _filter_hits_by_scope( return filtered +async def _filter_similar_hits( + db: Any, + hits: list[dict[str, Any]], + *, + query_asset_id: int, + min_score: float, +) -> list[dict[str, Any]]: + if not isinstance(hits, list) or not hits: + return [] + + score_floor = max(0.0, min(1.0, float(min_score or 0.0))) + filtered = [] + for hit in hits: + try: + score = float(hit.get("score") or 0.0) + except Exception: + score = 0.0 + if score < score_floor: + continue + filtered.append(hit) + if not filtered or db is None: + return filtered + + source_row = await db.aquery( + "SELECT LOWER(COALESCE(kind, '')) AS kind FROM assets WHERE id = ?", + (int(query_asset_id),), + ) + if not source_row.ok or not source_row.data: + return filtered + source_kind = str(source_row.data[0].get("kind") or "").strip().lower() + if not source_kind: + return filtered + + asset_ids: list[int] = [] + for hit in filtered: + try: + aid = int(hit.get("asset_id") or 0) + except Exception: + aid = 0 + if aid > 0: + asset_ids.append(aid) + if not asset_ids: + return [] + + placeholders = ",".join("?" for _ in asset_ids) + rows = await db.aquery( + f"SELECT id AS asset_id FROM assets WHERE id IN ({placeholders}) AND LOWER(COALESCE(kind, '')) = ?", + tuple(asset_ids + [source_kind]), + ) + if not rows.ok or not rows.data: + return [] + + allowed = set() + for row in rows.data: + try: + allowed.add(int(row.get("asset_id") or 0)) + except Exception: + continue + if not allowed: + return [] + return [hit for hit in filtered if int(hit.get("asset_id") or 0) in allowed] + + +def _filter_text_search_hits( + hits: list[dict[str, Any]], + *, + min_score: float, + relative_ratio: float, +) -> list[dict[str, Any]]: + if not isinstance(hits, list) or not hits: + return [] + + ratio = max(0.0, min(1.0, float(relative_ratio or 0.0))) + floor = max(0.0, min(1.0, float(min_score or 0.0))) + + filtered: list[dict[str, Any]] = [] + best_score = 0.0 + for hit in hits: + try: + score = float(hit.get("score") or 0.0) + except Exception: + score = 0.0 + if score > best_score: + best_score = score + + threshold = max(floor, best_score * ratio) + for hit in hits: + try: + score = float(hit.get("score") or 0.0) + except Exception: + score = 0.0 + if score >= threshold: + filtered.append(hit) + return filtered + + def _require_vector_services(services: dict[str, Any]): """Return (vector_searcher, error_response | None).""" if not is_vector_search_enabled(): @@ -365,6 +467,11 @@ def register_vector_search_routes(routes: web.RouteTableDef) -> None: # Hydrate results with full asset data for grid rendering if result.ok and result.data: db = services_dict.get("db") + result.data = _filter_text_search_hits( + list(result.data or []), + min_score=VECTOR_TEXT_SEARCH_MIN_SCORE, + relative_ratio=VECTOR_TEXT_SEARCH_RELATIVE_RATIO, + ) result.data = (await _filter_hits_by_scope( db, list(result.data or []), @@ -427,6 +534,12 @@ def register_vector_search_routes(routes: web.RouteTableDef) -> None: # Hydrate results with full asset data for grid rendering if result.ok and result.data: db = services_dict.get("db") + result.data = await _filter_similar_hits( + db, + list(result.data or []), + query_asset_id=asset_id, + min_score=VECTOR_SIMILAR_MIN_SCORE, + ) result.data = (await _filter_hits_by_scope( db, list(result.data or []), diff --git a/mjr_am_backend/routes/handlers/viewer.py b/mjr_am_backend/routes/handlers/viewer.py index c7235144..380a8c55 100644 --- a/mjr_am_backend/routes/handlers/viewer.py +++ b/mjr_am_backend/routes/handlers/viewer.py @@ -23,9 +23,65 @@ from ..core import ( logger = get_logger(__name__) +_POPOUT_HTML = """ + + + + + + Majoor Viewer + + + +
+
Preparing viewerโ€ฆ
+
+ + +""" + def register_viewer_routes(routes: web.RouteTableDef) -> None: """Register viewer info and file-serving routes.""" + @routes.get("/mjr/viewer/popout") + async def viewer_popout_page(_request: web.Request): + return web.Response(text=_POPOUT_HTML, content_type="text/html") + @routes.get("/mjr/am/viewer/info") async def viewer_info(request: web.Request): """ diff --git a/requirements.txt b/requirements.txt index 7c43e074..5efce825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,15 @@ protobuf>=3.20.0,<6.0.0 # timm is required by Florence-2 model implementation timm>=0.9.0,<2.0.0 +# einops is required by Florence-2 vision encoder +einops>=0.7.0,<1.0.0 + +# sentencepiece is required by the SigLIP2 tokenizer (SiglipTokenizer) +sentencepiece>=0.1.99,<1.0.0 + +# OpenCV headless for video frame extraction fallback when ffprobe/ffmpeg are unavailable +opencv-python-headless>=4.8.0,<5.0.0 + # Efficient approximate nearest-neighbour search (CPU-only build) faiss-cpu>=1.7.0,<2.0.0 diff --git a/tests/assets/z-image_00024_.png b/tests/assets/z-image_00024_.png new file mode 100644 index 0000000000000000000000000000000000000000..b6983f3f34a7e9a6c112162b1e457090a9b5d825 GIT binary patch literal 365085 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

#Z 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�-0W~YTD`k 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%0Dty(e*4#6 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∨mQeFxDkc1PXb6(zi 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


p>)Rg*q1=kuw{on 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 None: assert required is None assert err is not None assert err.code == "SERVICE_UNAVAILABLE" + + +@pytest.mark.asyncio +async def test_prewarm_services_warms_vector_components(monkeypatch) -> None: + calls = {"text": 0, "index": 0} + + class _Db: + async def aclose(self): + return None + + class _VectorService: + async def prewarm_text_queries(self): + calls["text"] += 1 + return Result.Ok({"loaded": True}) + + class _VectorSearcher: + async def prewarm_index(self): + calls["index"] += 1 + return Result.Ok({"loaded": True, "total": 0}) + + async def _build_ok(): + return Result.Ok({ + "db": _Db(), + "vector_service": _VectorService(), + "vector_searcher": _VectorSearcher(), + }) + + monkeypatch.setattr(svc, "build_services", _build_ok) + monkeypatch.setattr(svc, "VECTOR_PREWARM_ON_STARTUP", True) + svc._services = None + svc._services_error = None + + await svc.prewarm_services() + + assert calls == {"text": 1, "index": 1} + + +@pytest.mark.asyncio +async def test_prewarm_services_swallow_vector_warmup_failures(monkeypatch) -> None: + class _Db: + async def aclose(self): + return None + + class _VectorService: + async def prewarm_text_queries(self): + raise RuntimeError("boom") + + class _VectorSearcher: + async def prewarm_index(self): + return Result.Err("SERVICE_UNAVAILABLE", "no index") + + async def _build_ok(): + return Result.Ok({ + "db": _Db(), + "vector_service": _VectorService(), + "vector_searcher": _VectorSearcher(), + }) + + monkeypatch.setattr(svc, "build_services", _build_ok) + monkeypatch.setattr(svc, "VECTOR_PREWARM_ON_STARTUP", True) + svc._services = None + svc._services_error = None + + await svc.prewarm_services() + + assert isinstance(svc._services, dict) diff --git a/tests/features/test_assets_impl_routes.py b/tests/features/test_assets_impl_routes.py index 913f152e..b90d5ef7 100644 --- a/tests/features/test_assets_impl_routes.py +++ b/tests/features/test_assets_impl_routes.py @@ -312,7 +312,6 @@ async def test_get_tags_success_and_error(monkeypatch): resp2 = await (await app.router.resolve(req2)).handler(req2) assert _json(resp2).get("code") == "DB_ERROR" - @pytest.mark.asyncio async def test_delete_asset_validation_paths(monkeypatch, tmp_path: Path): app = _app() diff --git a/tests/features/test_hybrid_search_routes.py b/tests/features/test_hybrid_search_routes.py index 9d641d52..f189a6d0 100644 --- a/tests/features/test_hybrid_search_routes.py +++ b/tests/features/test_hybrid_search_routes.py @@ -82,6 +82,58 @@ async def test_hybrid_search_filters_semantic_hits_by_scope(monkeypatch) -> None assert data[0].get("_matchType") == "semantic" +@pytest.mark.asyncio +async def test_hybrid_search_filters_weak_semantic_tail(monkeypatch) -> None: + class _DB: + async def aquery(self, sql: str, params=()): + if "FROM assets_fts" in sql: + return Result.Ok([]) + if "SELECT a.id AS asset_id" in sql and "WHERE a.id IN" in sql: + return Result.Ok( + [ + {"asset_id": 2}, + {"asset_id": 3}, + {"asset_id": 4}, + ] + ) + return Result.Ok([]) + + class _Searcher: + async def search_by_text(self, _query: str, *, top_k: int = 20): + assert top_k >= 20 + return Result.Ok( + [ + {"asset_id": 2, "score": 0.14}, + {"asset_id": 3, "score": 0.10}, + {"asset_id": 4, "score": 0.05}, + ] + ) + + async def _require_services(): + return {"db": _DB(), "vector_searcher": _Searcher()}, None + + async def _hydrate_passthrough(_services, result): + return Result.Ok(result.data or []) + + monkeypatch.setattr(hybrid_search, "_require_services", _require_services) + monkeypatch.setattr(hybrid_search, "_hydrate_vector_results", _hydrate_passthrough) + monkeypatch.setattr(hybrid_search, "_check_rate_limit", lambda *_args, **_kwargs: (True, None)) + monkeypatch.setattr(hybrid_search, "VECTOR_TEXT_SEARCH_MIN_SCORE", 0.02) + monkeypatch.setattr(hybrid_search, "VECTOR_TEXT_SEARCH_RELATIVE_RATIO", 0.7) + + app = _build_hybrid_app() + req = make_mocked_request("GET", "/mjr/am/search/hybrid?q=dinosaure&scope=output&top_k=5", app=app) + match = await app.router.resolve(req) + req._match_info = match + resp = await match.handler(req) + body = json.loads(resp.text) + + assert body.get("ok") is True + data = body.get("data") or [] + assert [item.get("asset_id") for item in data] == [2, 3] + assert all(item.get("_matchType") == "semantic" for item in data) + + @pytest.mark.asyncio async def test_hybrid_search_scope_all_does_not_force_source_all(monkeypatch) -> None: seen = {"checked": False} diff --git a/tests/features/test_index_prepare_ops.py b/tests/features/test_index_prepare_ops.py index 0e17f81b..dc0c7e94 100644 --- a/tests/features/test_index_prepare_ops.py +++ b/tests/features/test_index_prepare_ops.py @@ -33,6 +33,27 @@ async def test_should_skip_by_journal_state_checks_rich_metadata_when_not_fast() assert await ipo.should_skip_by_journal_state(scanner, prepare_ctx=prepare_ctx, incremental=True, fast=False) +@pytest.mark.asyncio +async def test_should_skip_by_journal_state_keeps_missing_vector_assets_active() -> None: + async def _has_rich_metadata(*, asset_id): + return asset_id == 5 + + async def _asset_has_vector(*, asset_id): + return False + + scanner = SimpleNamespace( + _asset_has_rich_metadata=_has_rich_metadata, + _asset_has_vector=_asset_has_vector, + ) + prepare_ctx = {"journal_state_hash": "s", "state_hash": "s", "existing_id": 5} + assert await ipo.should_skip_by_journal_state( + scanner, + prepare_ctx=prepare_ctx, + incremental=True, + fast=False, + ) is False + + @pytest.mark.asyncio async def test_refresh_entry_from_cached_metadata_returns_refresh_entry(monkeypatch) -> None: async def _cached(_db, _fp, _state_hash): diff --git a/tests/features/test_index_prepare_ops_extra.py b/tests/features/test_index_prepare_ops_extra.py index b2029139..a2835ecc 100644 --- a/tests/features/test_index_prepare_ops_extra.py +++ b/tests/features/test_index_prepare_ops_extra.py @@ -106,6 +106,16 @@ async def test_should_skip_no_existing_id(): assert await ipo.should_skip_by_journal_state(scanner, prepare_ctx=ctx, incremental=True, fast=False) is False +@pytest.mark.asyncio +async def test_should_skip_fast_false_when_vector_missing(): + async def _asset_has_vector(*, asset_id): + return False + + scanner = SimpleNamespace(_asset_has_vector=_asset_has_vector) + ctx = {"journal_state_hash": "h", "state_hash": "h", "existing_id": 1} + assert await ipo.should_skip_by_journal_state(scanner, prepare_ctx=ctx, incremental=True, fast=True) is False + + # โ”€โ”€โ”€ refresh_entry_from_cached_metadata โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @pytest.mark.asyncio @@ -176,6 +186,34 @@ async def test_maybe_skip_incremental_unchanged_rich_metadata(monkeypatch): assert result is not None and result.data == {"action": "skipped"} +@pytest.mark.asyncio +async def test_maybe_skip_incremental_unchanged_rich_metadata_but_missing_vector(monkeypatch): + async def _should_skip(scanner, *, prepare_ctx, incremental, fast): + return False + + async def _refresh_cached(scanner, *, prepare_ctx, file_path, fast): + return None + + async def _has_rich(*, asset_id): + return True + + async def _asset_has_vector(*, asset_id): + return False + + monkeypatch.setattr(ipo, "should_skip_by_journal_state", _should_skip) + monkeypatch.setattr(ipo, "refresh_entry_from_cached_metadata", _refresh_cached) + + scanner = SimpleNamespace( + _asset_has_rich_metadata=_has_rich, + _asset_has_vector=_asset_has_vector, + ) + ctx = {"state_hash": "h", "journal_state_hash": "diff", "existing_id": 1, "mtime": 5, "size": 10, "existing_mtime": 5} + result = await ipo.maybe_skip_prepare_for_incremental( + scanner, prepare_ctx=ctx, incremental=True, fast=False, file_path=Path("x.png") + ) + assert result is None + + @pytest.mark.asyncio async def test_maybe_skip_not_incremental_unchanged_returns_none(monkeypatch): async def _should_skip(scanner, *, prepare_ctx, incremental, fast): diff --git a/tests/features/test_index_scanner_vector_auto.py b/tests/features/test_index_scanner_vector_auto.py index 261d336e..938ada33 100644 --- a/tests/features/test_index_scanner_vector_auto.py +++ b/tests/features/test_index_scanner_vector_auto.py @@ -83,3 +83,30 @@ async def test_schedule_added_vectors_respects_scan_toggle(monkeypatch) -> None: await asyncio.sleep(0) assert called["n"] == 0 + + +@pytest.mark.asyncio +async def test_schedule_prepared_vectors_includes_refreshed_and_updated_assets(monkeypatch) -> None: + scanner = IndexScanner(_DbStub([]), metadata_service=object(), scan_lock=asyncio.Lock()) + scanner.set_vector_services(vector_service=object(), vector_searcher=None) + + captured = {"asset_ids": None} + + async def _fake_index(*, asset_ids): + captured["asset_ids"] = list(asset_ids) + + monkeypatch.setattr(scanner, "_index_missing_asset_vectors", _fake_index) + + scanner._schedule_prepared_vector_index( + prepared=[ + {"action": "refresh", "asset_id": 4}, + {"action": "updated", "asset_id": 5}, + {"action": "skipped", "asset_id": 6}, + {"action": "updated", "asset_id": 4}, + ], + prev_added_count=1, + added_ids=[3, 7, 5], + ) + await asyncio.sleep(0) + + assert captured["asset_ids"] == [7, 5, 4] diff --git a/tests/features/test_prompt_alignment_multisignal.py b/tests/features/test_prompt_alignment_multisignal.py new file mode 100644 index 00000000..d98f7f72 --- /dev/null +++ b/tests/features/test_prompt_alignment_multisignal.py @@ -0,0 +1,205 @@ +"""Tests for the multi-signal prompt alignment improvements. + +Covers: + 1. Multi-segment scoring (prompt split by commas) + 2. Enhanced caption โ†” prompt text fusion + 3. Semantic dimension decomposition +""" + +import pytest + +from mjr_am_backend.features.index import vector_indexer as m +from mjr_am_backend.features.index.vector_service import VectorService +from mjr_am_backend.shared import Result + + +# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +class _VS: + """Deterministic VectorService stub. + + Returns a fixed vector for text/image. If *text_vectors* is supplied, + the stub returns different vectors per prompt string, allowing tests + to exercise per-segment scoring. + """ + + def __init__(self, *, text_vectors: dict[str, list[float]] | None = None): + self._model_name = "test" + self._default_vec = [1.0, 0.0] + self._text_vectors = text_vectors or {} + + async def get_text_embedding(self, text: str) -> Result[list[float]]: + vec = self._text_vectors.get(text, self._default_vec) + return Result.Ok(vec) + + async def get_image_embedding(self, _path: str) -> Result[list[float]]: + return Result.Ok(self._default_vec) + + +# โ”€โ”€ _extract_semantic_concepts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def test_extract_semantic_concepts_full(): + prompt = "a beautiful woman, anime style, watercolor painting, dreamy, neon colors" + concepts = m._extract_semantic_concepts(prompt) + assert concepts["subject"] == "a beautiful woman" + assert concepts["style"] == "anime" + assert concepts["medium"] == "watercolor" # "watercolor" before generic "painting" + assert concepts["mood"] == "dreamy" + assert concepts["color"] == "neon" + + +def test_extract_semantic_concepts_partial(): + prompt = "a car on a highway, impressionist" + concepts = m._extract_semantic_concepts(prompt) + assert "subject" in concepts + assert concepts["style"] == "impressionist" + assert "medium" not in concepts + assert "mood" not in concepts + + +def test_extract_semantic_concepts_no_matches(): + prompt = "xyzzy123" + concepts = m._extract_semantic_concepts(prompt) + assert concepts == {"subject": "xyzzy123"} + + +# โ”€โ”€ _multi_segment_score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +@pytest.mark.asyncio +async def test_multi_segment_returns_none_for_single_segment(): + vs = _VS() + result = await m._multi_segment_score(vs, [1.0, 0.0], "a single prompt") + assert result is None + + +@pytest.mark.asyncio +async def test_multi_segment_splits_and_scores(): + vs = _VS() + prompt = "a beautiful woman, sunset landscape, dramatic lighting" + result = await m._multi_segment_score(vs, [1.0, 0.0], prompt) + assert result is not None + assert isinstance(result, float) + # All segments map to the same default vector โ†’ cosine 1.0 for each + assert result >= 0.99 + + +@pytest.mark.asyncio +async def test_multi_segment_ignores_short_segments(): + vs = _VS() + prompt = "a beautiful woman, x, y, z, dramatic scene" + result = await m._multi_segment_score(vs, [1.0, 0.0], prompt) + assert result is not None + # Only "a beautiful woman" and "dramatic scene" survive (>= 4 chars) + + +# โ”€โ”€ _caption_prompt_similarity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +@pytest.mark.asyncio +async def test_caption_similarity_same_vectors(): + vs = _VS() + result = await m._caption_prompt_similarity(vs, "a beautiful sunset", "sunset scene") + assert result is not None + assert result >= 0.99 # Same embedding โ†’ cosine 1.0 + + +@pytest.mark.asyncio +async def test_caption_similarity_different_vectors(): + vs = _VS(text_vectors={ + "a cat sitting on a mat": [1.0, 0.0], + "fireworks at night": [0.0, 1.0], + }) + score = await m._caption_prompt_similarity(vs, "a cat sitting on a mat", "fireworks at night") + assert score is not None + # Orthogonal vectors โ†’ cosine 0.0 + assert abs(score) < 0.01 + + +# โ”€โ”€ _semantic_dimension_score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +@pytest.mark.asyncio +async def test_semantic_score_with_concepts(): + vs = _VS() + prompt = "a warrior, anime style, dark mood, oil painting" + score = await m._semantic_dimension_score(vs, [1.0, 0.0], prompt) + assert score is not None + assert isinstance(score, float) + + +@pytest.mark.asyncio +async def test_semantic_score_no_concepts(): + # Single short segment with no known keywords + vs = _VS() + score = await m._semantic_dimension_score(vs, [1.0, 0.0], "xy") + # "xy" is < 4 chars โ†’ no segments โ†’ no subject โ†’ empty concepts + assert score is None + + +# โ”€โ”€ compute_prompt_alignment (full pipeline) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +@pytest.mark.asyncio +async def test_alignment_with_caption_boosts_score(): + """When an enhanced caption is provided, it contributes to the score.""" + vs = _VS() + prompt = "a beautiful sunset over mountains" + emb = [1.0, 0.0] + + without_caption = await m.compute_prompt_alignment(vs, emb, prompt) + with_caption = await m.compute_prompt_alignment( + vs, emb, prompt, enhanced_caption="golden sunset over mountain range", + ) + + assert without_caption.ok + assert with_caption.ok + # With identical vectors everywhere, both should be ~1.0 + assert with_caption.data >= 0.99 + + +@pytest.mark.asyncio +async def test_alignment_with_segments_and_negative(): + vs = _VS(text_vectors={ + "a warrior in armor": [1.0, 0.0], + "dark forest background": [1.0, 0.0], + "ugly deformed": [0.0, 1.0], # orthogonal = no penalty + }) + prompt = "a warrior in armor, dark forest background" + emb = [1.0, 0.0] + + result = await m.compute_prompt_alignment( + vs, emb, prompt, negative_prompt="ugly deformed", + ) + assert result.ok + # Negative prompt is orthogonal โ†’ no penalty, score should be ~1.0 + assert result.data >= 0.95 + + +@pytest.mark.asyncio +async def test_alignment_negative_penalty_reduces_score(): + """When the negative prompt vector matches the image, it should lower the score.""" + import math + + # Use vectors whose raw cosine falls inside the SigLIP2 calibration band + # [0.00, 0.06]. cos(87ยฐ) โ‰ˆ 0.052 โ†’ calibrates to ~0.87. + # neg vector = img vector โ†’ cosine 1.0 โ†’ penalty = 1.0 * 0.25 = 0.25 + angle = math.radians(87) + prompt_vec = [math.cos(angle), math.sin(angle)] # cos โ‰ˆ 0.052 + img_vec = [1.0, 0.0] + neg_vec = [1.0, 0.0] # Identical to image โ†’ max penalty + + class _VSAngle: + _model_name = "test" + + async def get_text_embedding(self, text: str) -> Result[list[float]]: + if text == "ugly distorted": + return Result.Ok(neg_vec) + return Result.Ok(prompt_vec) + + vs = _VSAngle() + prompt = "a beautiful portrait" + + no_neg = await m.compute_prompt_alignment(vs, img_vec, prompt) + with_neg = await m.compute_prompt_alignment( + vs, img_vec, prompt, negative_prompt="ugly distorted", + ) + + assert no_neg.ok and with_neg.ok + assert with_neg.data < no_neg.data diff --git a/tests/features/test_scan_routes_more.py b/tests/features/test_scan_routes_more.py index c88ce5eb..12a8143d 100644 --- a/tests/features/test_scan_routes_more.py +++ b/tests/features/test_scan_routes_more.py @@ -1692,6 +1692,262 @@ async def test_reset_index_non_malformed_clear_error_returns_error(monkeypatch, assert payload.get("code") == "DB_ERROR" +@pytest.mark.asyncio +async def test_reset_index_preserve_vectors_skips_assets_clear_and_hard_reset(monkeypatch, tmp_path: Path) -> None: + out_root = tmp_path / "out" + out_root.mkdir() + executed_sql: list[str] = [] + purge_calls = {"n": 0} + + class _Tx: + ok = True + error = None + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + class _DB: + def __init__(self): + self.areset_calls = 0 + + def atransaction(self, mode="immediate"): + _ = mode + return _Tx() + + async def aexecute(self, sql, params=None): + _ = params + executed_sql.append(" ".join(str(sql).split())) + return Result.Ok(1) + + async def avacuum(self): + return Result.Ok({}) + + async def areset(self): + self.areset_calls += 1 + return Result.Ok({}) + + class _Index: + async def stop_enrichment(self, clear_queue=True): + _ = clear_queue + + async def scan_directory(self, *_args, **_kwargs): + return Result.Ok({"scanned": 0, "added": 0, "updated": 0, "skipped": 0, "errors": 0}) + + db = _DB() + + async def _runtime_output_root(_svc): + return str(out_root) + + async def _require_services(): + return {"db": db, "index": _Index()}, None + + async def _read_json(_request): + return Result.Ok( + { + "scope": "output", + "reindex": False, + "hard_reset_db": True, + "clear_scan_journal": True, + "clear_metadata_cache": True, + "clear_asset_metadata": True, + "clear_assets": True, + "preserve_vectors": True, + "rebuild_fts": False, + } + ) + + async def _resolve_security_prefs(_svc): + return {} + + monkeypatch.setattr(scan_mod, "_csrf_error", lambda _request: None) + monkeypatch.setattr(scan_mod, "_require_write_access", lambda _request: Result.Ok({})) + monkeypatch.setattr(scan_mod, "_require_services", _require_services) + monkeypatch.setattr(scan_mod, "_resolve_security_prefs", _resolve_security_prefs) + monkeypatch.setattr(scan_mod, "_require_operation_enabled", lambda *_args, **_kwargs: Result.Ok({})) + monkeypatch.setattr(scan_mod, "is_db_maintenance_active", lambda: False) + monkeypatch.setattr(scan_mod, "_read_json", _read_json) + monkeypatch.setattr(scan_mod, "_runtime_output_root", _runtime_output_root) + async def _purge_orphans(_db): + purge_calls["n"] += 1 + return Result.Ok(True) + + monkeypatch.setattr(scan_mod, "purge_orphan_vec_embeddings", _purge_orphans) + + app = _app() + req = make_mocked_request("POST", "/mjr/am/index/reset", app=app) + match = await app.router.resolve(req) + resp = await match.handler(req) + payload = json.loads(resp.text) + + assert payload.get("ok") is True + assert payload.get("data", {}).get("preserve_vectors") is True + assert db.areset_calls == 0 + assert purge_calls["n"] == 0 + assert not any(sql.startswith("DELETE FROM assets") for sql in executed_sql) + assert any("DELETE FROM scan_journal" in sql for sql in executed_sql) + assert any("DELETE FROM metadata_cache" in sql for sql in executed_sql) + + +@pytest.mark.asyncio +async def test_reset_index_preserve_vectors_malformed_returns_error_without_hard_reset(monkeypatch, tmp_path: Path) -> None: + out_root = tmp_path / "out" + out_root.mkdir() + + class _Tx: + ok = True + error = None + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + class _DB: + def __init__(self): + self.areset_calls = 0 + + def atransaction(self, mode="immediate"): + _ = mode + return _Tx() + + async def aexecute(self, sql, params=None): + _ = (sql, params) + return Result.Err("DB_ERROR", "database disk image is malformed") + + async def areset(self): + self.areset_calls += 1 + return Result.Ok({}) + + class _Index: + async def stop_enrichment(self, clear_queue=True): + _ = clear_queue + + db = _DB() + + async def _runtime_output_root(_svc): + return str(out_root) + + async def _require_services(): + return {"db": db, "index": _Index()}, None + + async def _read_json(_request): + return Result.Ok( + { + "scope": "output", + "reindex": False, + "preserve_vectors": True, + "clear_scan_journal": True, + } + ) + + async def _resolve_security_prefs(_svc): + return {} + + monkeypatch.setattr(scan_mod, "_csrf_error", lambda _request: None) + monkeypatch.setattr(scan_mod, "_require_write_access", lambda _request: Result.Ok({})) + monkeypatch.setattr(scan_mod, "_require_services", _require_services) + monkeypatch.setattr(scan_mod, "_resolve_security_prefs", _resolve_security_prefs) + monkeypatch.setattr(scan_mod, "_require_operation_enabled", lambda *_args, **_kwargs: Result.Ok({})) + monkeypatch.setattr(scan_mod, "is_db_maintenance_active", lambda: False) + monkeypatch.setattr(scan_mod, "_read_json", _read_json) + monkeypatch.setattr(scan_mod, "_runtime_output_root", _runtime_output_root) + + app = _app() + req = make_mocked_request("POST", "/mjr/am/index/reset", app=app) + match = await app.router.resolve(req) + resp = await match.handler(req) + payload = json.loads(resp.text) + + assert payload.get("ok") is False + assert payload.get("code") == "DB_MALFORMED" + assert db.areset_calls == 0 + + +@pytest.mark.asyncio +async def test_reset_index_without_preserve_vectors_purges_orphan_vectors(monkeypatch, tmp_path: Path) -> None: + out_root = tmp_path / "out" + out_root.mkdir() + purge_calls = {"n": 0} + + class _Tx: + ok = True + error = None + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + class _DB: + def atransaction(self, mode="immediate"): + _ = mode + return _Tx() + + async def aexecute(self, sql, params=None): + _ = (sql, params) + return Result.Ok(1) + + async def avacuum(self): + return Result.Ok({}) + + class _Index: + async def stop_enrichment(self, clear_queue=True): + _ = clear_queue + + async def _runtime_output_root(_svc): + return str(out_root) + + async def _require_services(): + return {"db": _DB(), "index": _Index()}, None + + async def _read_json(_request): + return Result.Ok( + { + "scope": "output", + "reindex": False, + "clear_scan_journal": True, + "clear_metadata_cache": True, + "clear_asset_metadata": True, + "clear_assets": True, + "preserve_vectors": False, + "rebuild_fts": False, + } + ) + + async def _resolve_security_prefs(_svc): + return {} + + async def _purge_orphans(_db): + purge_calls["n"] += 1 + return Result.Ok(True) + + monkeypatch.setattr(scan_mod, "_csrf_error", lambda _request: None) + monkeypatch.setattr(scan_mod, "_require_write_access", lambda _request: Result.Ok({})) + monkeypatch.setattr(scan_mod, "_require_services", _require_services) + monkeypatch.setattr(scan_mod, "_resolve_security_prefs", _resolve_security_prefs) + monkeypatch.setattr(scan_mod, "_require_operation_enabled", lambda *_args, **_kwargs: Result.Ok({})) + monkeypatch.setattr(scan_mod, "is_db_maintenance_active", lambda: False) + monkeypatch.setattr(scan_mod, "_read_json", _read_json) + monkeypatch.setattr(scan_mod, "_runtime_output_root", _runtime_output_root) + monkeypatch.setattr(scan_mod, "purge_orphan_vec_embeddings", _purge_orphans) + + app = _app() + req = make_mocked_request("POST", "/mjr/am/index/reset", app=app) + match = await app.router.resolve(req) + resp = await match.handler(req) + payload = json.loads(resp.text) + + assert payload.get("ok") is True + assert payload.get("data", {}).get("preserve_vectors") is False + assert payload.get("data", {}).get("vectors_purged") is True + assert purge_calls["n"] == 1 + + @pytest.mark.asyncio async def test_upload_input_csrf_and_auth(monkeypatch) -> None: app = _app() diff --git a/tests/features/test_vector_search_routes.py b/tests/features/test_vector_search_routes.py index eaeaca8d..0f27d7b1 100644 --- a/tests/features/test_vector_search_routes.py +++ b/tests/features/test_vector_search_routes.py @@ -171,6 +171,58 @@ async def test_vector_search_route_filters_hits_by_scope(monkeypatch) -> None: assert body.get("data") == [{"asset_id": 2, "score": 0.93}] +@pytest.mark.asyncio +async def test_vector_search_route_filters_weak_score_tail(monkeypatch) -> None: + class _DB: + async def aquery(self, sql: str, params=()): + if "SELECT a.id AS asset_id FROM assets a WHERE" in sql: + return Result.Ok( + [ + {"asset_id": 1}, + {"asset_id": 2}, + {"asset_id": 3}, + ] + ) + return Result.Ok([]) + + class _Searcher: + async def search_by_text(self, _query: str, *, top_k: int = 20): + assert top_k >= 20 + return Result.Ok( + [ + {"asset_id": 1, "score": 0.14}, + {"asset_id": 2, "score": 0.10}, + {"asset_id": 3, "score": 0.05}, + ] + ) + + async def _require_services(): + return {"vector_searcher": _Searcher(), "db": _DB()}, None + + async def _hydrate_passthrough(_services, result): + return Result.Ok(result.data or []) + + monkeypatch.setattr(vector_search, "_require_services", _require_services) + monkeypatch.setattr(vector_search, "_hydrate_vector_results", _hydrate_passthrough) + monkeypatch.setattr(vector_search, "is_vector_search_enabled", lambda: True) + monkeypatch.setattr(vector_search, "_check_rate_limit", lambda *_args, **_kwargs: (True, None)) + monkeypatch.setattr(vector_search, "VECTOR_TEXT_SEARCH_MIN_SCORE", 0.02) + monkeypatch.setattr(vector_search, "VECTOR_TEXT_SEARCH_RELATIVE_RATIO", 0.7) + + app = _build_vector_app() + req = make_mocked_request("GET", "/mjr/am/vector/search?q=dinosaure&scope=output&top_k=5", app=app) + match = await app.router.resolve(req) + req._match_info = match + resp = await match.handler(req) + body = json.loads(resp.text) + + assert body.get("ok") is True + assert body.get("data") == [ + {"asset_id": 1, "score": 0.14}, + {"asset_id": 2, "score": 0.1}, + ] + + @pytest.mark.asyncio async def test_vector_similar_route_success(monkeypatch) -> None: class _Searcher: @@ -239,6 +291,51 @@ async def test_vector_similar_route_filters_hits_by_scope(monkeypatch) -> None: assert body.get("data") == [{"asset_id": 88, "score": 0.9}] +@pytest.mark.asyncio +async def test_vector_similar_route_filters_by_kind_and_min_score(monkeypatch) -> None: + class _DB: + async def aquery(self, sql: str, params=()): + if "SELECT LOWER(COALESCE(kind, '')) AS kind FROM assets WHERE id = ?" in sql: + return Result.Ok([{"kind": "image"}]) + if "SELECT id AS asset_id FROM assets WHERE id IN" in sql and "LOWER(COALESCE(kind, '')) = ?" in sql: + return Result.Ok([{"asset_id": 77}, {"asset_id": 88}]) + return Result.Ok([]) + + class _Searcher: + async def find_similar(self, asset_id: int, *, top_k: int = 20): + assert asset_id == 42 + assert top_k > 10 + return Result.Ok( + [ + {"asset_id": 77, "score": 0.91}, + {"asset_id": 88, "score": 0.55}, + {"asset_id": 99, "score": 0.89}, + ] + ) + + async def _require_services(): + return {"vector_searcher": _Searcher(), "db": _DB()}, None + + async def _hydrate_passthrough(_services, result): + return Result.Ok(result.data or []) + + monkeypatch.setattr(vector_search, "_require_services", _require_services) + monkeypatch.setattr(vector_search, "_hydrate_vector_results", _hydrate_passthrough) + monkeypatch.setattr(vector_search, "is_vector_search_enabled", lambda: True) + monkeypatch.setattr(vector_search, "_check_rate_limit", lambda *_args, **_kwargs: (True, None)) + monkeypatch.setattr(vector_search, "VECTOR_SIMILAR_MIN_SCORE", 0.6) + + app = _build_vector_app() + req = make_mocked_request("GET", "/mjr/am/vector/similar/42?top_k=10&scope=all", app=app) + match = await app.router.resolve(req) + req._match_info = match + resp = await match.handler(req) + body = json.loads(resp.text) + + assert body.get("ok") is True + assert body.get("data") == [{"asset_id": 77, "score": 0.91}] + + @pytest.mark.asyncio async def test_vector_enhance_prompt_alias_route_success(monkeypatch) -> None: class _DB: diff --git a/tests/features/test_vector_service_video_xclip.py b/tests/features/test_vector_service_video_xclip.py index 95fdb0e2..b049122f 100644 --- a/tests/features/test_vector_service_video_xclip.py +++ b/tests/features/test_vector_service_video_xclip.py @@ -99,21 +99,22 @@ class _FakeFeatures: class _FakeXclipModel: - def get_image_features(self, *, pixel_values=None): + def get_video_features(self, *, pixel_values=None): assert pixel_values is not None return _FakeFeatures() +class _FakeProcessorMapping(dict): + """Fake processor output that acts as a mapping with .get().""" + pass + + class _ProcessorWithFallbackSignature: def __call__(self, *args, **kwargs): _ = args - # Simulate a non-mapping return for the primary images signature. - # Older code indexed directly with ["pixel_values"] and crashed. if "images" in kwargs: - return PILImage.new("RGB", (2, 2), color=(0, 0, 0)) - if "videos" in kwargs: - return {"pixel_values": [[[[1.0]]]]} - return {"pixel_values": [[[[1.0]]]]} + return _FakeProcessorMapping({"pixel_values": [[[[1.0]]]]}) + return _FakeProcessorMapping({"pixel_values": [[[[1.0]]]]}) class _BatchFailsOnMultiFrameModel: @@ -162,6 +163,9 @@ async def test_video_embedding_falls_back_to_single_frame_when_batch_fails(monke async def _fake_xclip_fail(_path): return m.Result.Err("METADATA_FAILED", "xclip unavailable") + # Force non-native path so the legacy SentenceTransformer fallback is tested + monkeypatch.setattr(vs, "_use_native_siglip", lambda: False) + async def _fake_ensure_model(): return _BatchFailsOnMultiFrameModel() diff --git a/user_guide.html b/user_guide.html new file mode 100644 index 00000000..a45a16d5 --- /dev/null +++ b/user_guide.html @@ -0,0 +1,795 @@ + + + + + + Majoor Assets Manager - User Guide + + + +
+

๐ŸŽจ Majoor Assets Manager

+

Comprehensive User Guide for ComfyUI

+

Version 2.4.0 | Last Updated: March 5, 2026

+
+ + + +
+ +
+

๐Ÿš€ Quick Start

+ +
+ ๐Ÿ’ก New to Majoor Assets Manager? Start with the installation section, then jump to Basic Usage to get up and running quickly! +
+
+ + +
+

๐Ÿ“ฆ Installation

+ +

Method A: ComfyUI Manager (Recommended)

+
    +
  1. Open ComfyUI Manager in your browser
  2. +
  3. Search for "Majoor Assets Manager"
  4. +
  5. Click Install, then restart ComfyUI completely
  6. +
  7. The extension appears in the sidebar as a folder icon ๐Ÿ“
  8. +
+ +

Method B: Manual Installation

+
# Navigate to ComfyUI custom_nodes directory
+cd ComfyUI/custom_nodes
+
+# Clone the repository
+git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager ComfyUI-Majoor-AssetsManager
+
+# Install Python dependencies
+cd ComfyUI-Majoor-AssetsManager
+pip install -r requirements.txt
+
+# Restart ComfyUI
+ +

Optional Dependencies (Recommended)

+

For full metadata extraction, install these tools:

+ +

Windows (PowerShell)

+
# Using Scoop
+scoop install ffmpeg exiftool
+
+# OR using Chocolatey
+choco install -y ffmpeg exiftool
+
+# OR using WinGet
+winget install -e --id Gyan.FFmpeg
+winget install -e --id PhilHarvey.ExifTool
+ +

macOS

+
brew install ffmpeg exiftool
+ +

Linux

+
# Ubuntu/Debian
+sudo apt update
+sudo apt install -y ffmpeg libimage-exiftool-perl
+
+# Fedora/RHEL
+sudo dnf install -y ffmpeg perl-Image-ExifTool
+
+ + +
+

๐Ÿ“– Basic Usage

+ +

First Steps

+
    +
  1. Open Assets Manager: Click the folder icon ๐Ÿ“ in ComfyUI sidebar
  2. +
  3. Choose Scope: Select Outputs, Inputs, Custom, or Collections
  4. +
  5. Browse Assets: Scroll through the grid view with virtual scrolling
  6. +
  7. Search: Type in the search bar for full-text search
  8. +
  9. Filter: Use dropdowns for kind, rating, workflow, date filters
  10. +
+ +

Asset Operations (Right-Click Menu)

+
+
    +
  • Open in Viewer: Double-click or right-click โ†’ Open in Viewer
  • +
  • Rate: Assign 0-5 stars
  • +
  • Edit Tags: Add/remove custom tags
  • +
  • Add to Collection: Save to existing or new collection
  • +
  • Rename: Change filename
  • +
  • Delete: Remove asset (with confirmation)
  • +
  • Open in Folder: Show in file explorer
  • +
  • Copy Path: Copy full file path to clipboard
  • +
  • Stage to Input: Copy to ComfyUI input directory
  • +
+
+ +

Collections

+
    +
  1. Select assets (Ctrl/Cmd+click for multiple)
  2. +
  3. Right-click โ†’ Add to Collection
  4. +
  5. Create new or add to existing collection
  6. +
  7. Access via Collections scope
  8. +
+
+ + +
+

๐Ÿค– AI Features NEW!

+ +
+ ๐Ÿ“š Complete Guide: See docs/AI_FEATURES.md for comprehensive AI documentation. +
+ +

What AI Features Are Available

+ +
+

๐Ÿ”ฎ AI Semantic Search

+

Search using natural language instead of keywords. Example: "sunset over mountains with orange sky"

+
+ +
+

๐Ÿ” Find Similar

+

Select an asset and discover visually similar images based on content, not just tags.

+
+ +
+

๐Ÿท๏ธ AI Auto-Tags

+

Automatic tag suggestions based on image content: portrait, landscape, cyberpunk, anime, etc.

+
+ +
+

๐Ÿ“ Enhanced Captions (Florence-2)

+

Generate detailed AI-powered image descriptions for better searchability.

+
+ +
+

๐Ÿ“Š Prompt Alignment Score

+

Measure how well your generated image matches the original prompt (0-100%).

+
+ +
+

๐Ÿ“ Smart Collections

+

Auto-create themed collections based on visual similarity clustering.

+
+ +
+

๐Ÿ”ฌ Discover Groups

+

Cluster your entire library by visual themes to discover patterns.

+
+ +

AI Models Used

+ + + + + + + + + + + + + + + + + + + + + +
ModelPurposeSize
SigLIP2 SO400MImage & text embeddings~1.2 GB
X-CLIP BaseVideo embeddings~600 MB
Florence-2 BaseImage captioning~800 MB
+ +
+ โš ๏ธ First-Time Setup: Models are downloaded automatically on first use. Initial download may take several minutes depending on your connection. +
+ +

How to Enable AI Features

+
    +
  1. Open Settings in Assets Manager
  2. +
  3. Ensure Enable AI semantic search is toggled ON (default: ON)
  4. +
  5. Trigger initial index scan (Ctrl+S)
  6. +
  7. Click Backfill vectors in Index Status section
  8. +
  9. Wait for embeddings to be computed (5-30 min for 1000 assets)
  10. +
+ +

Using AI Features

+ +

Semantic Search

+
    +
  1. Click the sparkles icon (๐Ÿ”ฎ) in the search bar
  2. +
  3. Type natural language: "cyberpunk city at night with neon lights"
  4. +
  5. Press Enter to see results ranked by relevance
  6. +
+ +

Find Similar

+
    +
  1. Select an asset in the grid
  2. +
  3. Right-click โ†’ Find Similar
  4. +
  5. Browse visually similar assets
  6. +
+ +

Generate Caption

+
    +
  1. Open an asset in the Viewer
  2. +
  3. In sidebar, find Image Description section
  4. +
  5. Click Generate
  6. +
  7. Wait for AI caption (5-30 seconds)
  8. +
+ +

Per-Asset Operations (Quick Actions)

+

You can trigger scan and backfill vectors for individual assets directly from the grid:

+ +

Via Card Status Dot

+
    +
  1. Click the status dot on an asset card (corner indicator)
  2. +
  3. Select from the menu: +
      +
    • "Index Asset" โ€” Trigger metadata scan for this asset
    • +
    • "Generate Vector" โ€” Compute AI embedding
    • +
    • "Re-index" โ€” Force re-scan and re-compute vectors
    • +
    +
  4. +
+ +

Via Sparkles Icon (๐Ÿ”ฎ) on Card

+
    +
  1. Hover over an asset card to reveal action buttons
  2. +
  3. Click the sparkles icon (๐Ÿ”ฎ) if visible
  4. +
  5. Choose AI actions: +
      +
    • Generate Caption โ€” Run Florence-2 captioning
    • +
    • Compute Alignment โ€” Calculate prompt alignment score
    • +
    • Suggest Tags โ€” Generate AI auto-tags
    • +
    • Find Similar โ€” Show visually similar assets
    • +
    +
  6. +
+ +

Visual Status Indicators

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IndicatorMeaning
๐ŸŸข Green dotAsset fully indexed with vectors
๐ŸŸก Yellow dotAsset indexed, vectors pending
๐Ÿ”ด Red dotIndexing failed or vectors unavailable
๐Ÿ”ฎ Sparkles visibleAI features available for this asset
โณ Spinning iconProcessing in progress
+ +
+ ๐Ÿ’ก Tip: For best results, run a full backfill first, then use per-asset operations to update individual items as needed. +
+ +

Performance Expectations

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Library SizeBackfill Time (CPU)Backfill Time (GPU)
100 assets2-5 min30-60 sec
500 assets10-25 min2-5 min
1000 assets20-50 min5-10 min
5000 assets2-4 hours25-50 min
+ +
+ ๐Ÿ’ก GPU Acceleration: For large libraries, GPU acceleration (NVIDIA RTX 3060 or better) significantly speeds up embedding computation. +
+ +

Configuration

+
# Enable/disable AI features (default: ON)
+export MJR_AM_ENABLE_VECTOR_SEARCH=1
+
+# Adjust auto-tag sensitivity (0.0-1.0, default: 0.25)
+export MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.25
+
+# Verbose AI logging for debugging
+export MJR_AM_AI_VERBOSE_LOGS=1
+
+ + +
+

๐Ÿ–ผ๏ธ Advanced Viewer

+ +

Opening the Viewer

+
    +
  • Double-click any asset in the grid
  • +
  • Or right-click โ†’ Open in Viewer
  • +
  • Or press Enter on selected asset
  • +
+ +

Viewer Features

+
+

Image Enhancement

+
    +
  • Exposure (EV) adjustment
  • +
  • Gamma correction
  • +
  • Channel isolation (R/G/B)
  • +
+
+ +
+

Analysis Tools

+
    +
  • False color visualization
  • +
  • Zebra patterns for exposure
  • +
  • Histogram display
  • +
  • Waveform monitor
  • +
  • Vectorscope
  • +
+
+ +
+

Visual Overlays

+
    +
  • Grid overlays (rule of thirds)
  • +
  • Pixel probe for color sampling
  • +
  • Loupe magnification
  • +
+
+ +

Navigation

+
    +
  • Mouse Wheel: Zoom in/out
  • +
  • Click + Drag: Pan zoomed image
  • +
  • Left/Right Arrow: Previous/Next asset
  • +
  • Esc: Close viewer
  • +
  • F: Toggle fullscreen
  • +
+
+ + +
+

๐Ÿ“‹ Organization: Ratings, Tags & Collections

+ +

Ratings System

+
    +
  • 0-5 Stars: Rate assets by quality or preference
  • +
  • Windows Sync: Ratings sync with Windows Explorer (RatingPercent)
  • +
  • Filter by Rating: Show only 4+ star assets, etc.
  • +
  • Keyboard: Press 0-5 to rate selected asset
  • +
+ +

Tags System

+
    +
  • Custom Tags: Add any tags you want
  • +
  • Hierarchical: Use colons for hierarchy (style:anime:fantasy)
  • +
  • Autocomplete: Tags suggest as you type
  • +
  • Bulk Operations: Apply tags to multiple assets
  • +
+ +

Collections

+
    +
  • Save Selections: Group assets across scopes
  • +
  • Smart Collections: AI-suggested groupings
  • +
  • Discover Groups: Auto-cluster by visual similarity
  • +
  • Persistent: Collections saved to disk
  • +
+
+ + +
+

โŒจ๏ธ Keyboard Shortcuts

+ +

Global / Panel

+ + + + + + +
ShortcutAction
Ctrl+S / Cmd+STrigger index scan
Ctrl+F / Ctrl+KFocus search input
Ctrl+HClear search input
DToggle sidebar (details)
+ +

Grid View

+ + + + + + + + + + + +
ShortcutAction
Arrow KeysNavigate selection
Enter / SpaceOpen Viewer
Ctrl+ASelect all
Ctrl+DDeselect all
0-5Set rating (0-5 stars)
TEdit tags
BAdd to collection
F2Rename file
DeleteDelete file
+ +

Viewer

+ + + + + + + + + + + + +
ShortcutAction
EscClose viewer
FToggle fullscreen
DToggle info panel
SpacePlay/pause video
Left/RightPrevious/next asset
IToggle pixel probe
CCopy probed color
LToggle loupe
ZToggle zebra patterns
GCycle grid overlays
+
+ + +
+

๐Ÿ”ง Troubleshooting

+ +

Extension Not Appearing

+
    +
  • Verify installation in custom_nodes/ComfyUI-Majoor-AssetsManager
  • +
  • Check ComfyUI console for import errors
  • +
  • Ensure all dependencies: pip install -r requirements.txt
  • +
  • Restart ComfyUI completely
  • +
+ +

Database Corruption

+
    +
  1. Open Assets Manager โ†’ Index Status section
  2. +
  3. Click Delete DB button
  4. +
  5. Confirm the warning dialog
  6. +
  7. Database rebuilds automatically
  8. +
+ +

External Tools Not Found

+
# Verify installation
+exiftool -ver
+ffprobe -version
+
+# If not found, install:
+# Windows: scoop install ffmpeg exiftool
+# macOS: brew install ffmpeg exiftool
+# Linux: sudo apt install ffmpeg libimage-exiftool-perl
+ +

AI Features Not Working

+
    +
  • Check if AI is enabled in Settings
  • +
  • Verify models downloaded (check console logs)
  • +
  • Run Backfill vectors in Index Status
  • +
  • Ensure sufficient RAM (8GB minimum, 16GB recommended)
  • +
+ +

Slow Performance

+
    +
  • Reduce page size in settings (default: 50-100)
  • +
  • Exclude very large directories from custom roots
  • +
  • Ensure SSD storage for database
  • +
  • For AI: enable GPU acceleration if available
  • +
+
+ + +
+

๐Ÿ“š Additional Resources

+ +

Documentation

+ + +

Community

+ +
+
+ + + +