Introduction
Tip: edit docs/DOCUMENTATION_INDEX.md to change the curated order, then regenerate with python scripts/build_user_guide_html.py.
DOCUMENTATION_INDEX.md
Majoor Assets Manager - Documentation Index
Welcome to the documentation hub for Majoor Assets Manager for ComfyUI.
Current Version: 2.4.5 Last Updated: April 14, 2026
Quick Start
New users
- Installation Guide - Install and set up the extension
- User Guide (HTML) - Visual walkthrough with screenshots
- Hotkeys & Shortcuts - Essential keyboard shortcuts
- Basic Search - Find assets quickly
Returning users
- AI Features Guide - AI-assisted search and enrichment features
- MFV Guide - Dedicated Majoor Floating Viewer walkthrough
- Viewer Feature Tutorial - Floating viewer and analysis tools
- Changelog - Recent releases and fixes
- Architecture Map - Backend boundaries and refactor guide
Privacy And Offline Use
- PRIVACY_OFFLINE.md - Dedicated privacy, offline, and token explanation page
- AI_FEATURES.md - Local AI processing, model downloads, offline behavior, and token clarification
- SECURITY_ENV_VARS.md - Remote access security, API token behavior, and safe defaults
- SETTINGS_CONFIGURATION.md - UI security settings and token storage behavior
Documentation Categories
Getting Started
| Document | Description |
|---|---|
| INSTALLATION.md | Detailed installation and network-drive guidance |
| user_guide.html | Full visual user guide |
| HOTKEYS_SHORTCUTS.md | Keyboard shortcuts |
| SHORTCUTS.md | Extra gestures and shortcuts |
Core Features
| Document | Description |
|---|---|
| SEARCH_FILTERING.md | Full-text search, filters, and sorting |
| MFV_GUIDE.md | Dedicated Majoor Floating Viewer guide |
| VIEWER_FEATURE_TUTORIAL.md | Viewer, MFV, and analysis tools |
| GRAPH_MAP.md | Graph Map workflow navigation and node detail |
| FLOATING_VIEWER_WORKFLOW_SIDEBAR.md | Node Parameters sidebar in Floating Viewer |
| RATINGS_TAGS_COLLECTIONS.md | Ratings, tags, and collections |
| DRAG_DROP.md | Drag and drop behavior |
| AI_FEATURES.md | Semantic search, auto-tags, and enrichment |
| CUSTOM_NODES.md | MajoorSaveImage & MajoorSaveVideo node reference |
Plugin System
| Document | Description |
|---|---|
| PLUGIN_QUICK_REFERENCE.md | Quick start for plugin development |
| PLUGIN_SYSTEM_DESIGN.md | Full plugin architecture design |
| PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md | Implementation status and details |
Configuration And Security
| Document | Description |
|---|---|
| PRIVACY_OFFLINE.md | Dedicated privacy, offline behavior, and token clarification guide |
| SETTINGS_CONFIGURATION.md | UI and runtime settings, index DB path, env vars |
| SECURITY_ENV_VARS.md | Environment variables and security model |
| THREAT_MODEL.md | Threats, mitigations, and residual risk |
| ARCHITECTURE_MAP.md | Package responsibilities and internal boundaries |
Maintenance And Development
| Document | Description |
|---|---|
| DB_MAINTENANCE.md | Database maintenance, recovery, and configurable index directory |
| TESTING.md | Tests, reports, and quality gate |
| API_REFERENCE.md | Backend endpoint reference |
| CONTRIBUTING.md | Developer onboarding and contribution guidelines |
| adr/ | Architecture Decision Records |
Frontend Architecture
| Document | Description |
|---|---|
| FRONTEND_IMPERATIVE_DESIGN.md | Design rationale for imperative modules |
| FRONTEND_LIFECYCLE_CONVENTIONS.md | Component lifecycle conventions |
| VUE_MIGRATION_PLAN.md | Vue 3 migration plan (archival) |
| node-stream-reactivation.md | Node Stream and MFV stream boundaries |
Historical Documents
| Document | Description |
|---|---|
| PLAN_REFACTO_COMPLET_MAJOOR_ASSETS_MANAGER.md | V1 refactoring completion plan (archival) |
| PLAN_REFACTO_V2_LONG_TERME.md | V2 long-term refactoring plan (archival) |
Quality And Verification
Use the canonical gate from the repo root:
python scripts/run_quality_gate.py
Useful variants:
python scripts/run_quality_gate.py --python-only --skip-tests
tox -e quality
npm run test:js
The quality gate enforces:
- UTF-8 text files without BOM
rufflinting on changed Python files during the migration windowmypybanditpip-audit- xenon/radon complexity checks
- backend tests
- frontend tests
npm audit
Security Note
The backend keeps a compatibility-first local security model:
- loopback writes remain allowed by default
- remote writes are denied unless explicitly authorized
MAJOOR_REQUIRE_AUTH=1switches to strict token-auth for local writes too
Current releases also support a Settings-first remote setup flow:
Recommended Remote LAN Setupis the preferred one-click LAN path- the current browser session exposes its state through the runtime
Write auth:indicator inside Assets Manager
That nuance is intentional and is the documented target behavior for current releases.
docs/DOCUMENTATION_INDEX.md
AI_FEATURES.md
Majoor Assets Manager โ AI Features Guide
Comprehensive guide to AI-powered features in Majoor Assets Manager
Version: 2.4.5 Last Updated: April 15, 2026
Table of Contents
- Overview
- AI Models & Technologies
- Enabling AI Features
- Core AI Features
- Semantic Search
- Find Similar
- AI Auto-Tags
- Enhanced Captions (Florence-2)
- Prompt Alignment Score
- Smart Collections
- Discover Groups
- How It Works
- Configuration & Tuning
- Performance Considerations
- Troubleshooting
- API Reference
- 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 | |
| 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)
- Open Assets Manager panel in ComfyUI
- Click Settings (gear icon)
- Find AI Features section
- Toggle Enable AI semantic search to ON
- Wait for initial model download (progress shown in console)
- Click Save Settings
Method 2: Via Environment Variable
Add to your environment before starting ComfyUI:
# 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:
- 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)
- 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
- Locate the status indicator on an asset card (small dot in corner)
- Click the status dot to open the asset actions menu
- 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)
- Hover over an asset card to reveal action buttons
- Click the sparkles icon (๐ฎ) if visible on the card
- 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 |
<div class="tip"> <strong>๐ก Tip:</strong> For best results, run a full backfill first, then use per-asset operations to update individual items as needed. </div>
Core AI Features
1. Semantic Search
Search your asset library using natural language queries instead of keywords.
How to Use
- Open Assets Manager panel
- Click the sparkles icon (๐ฎ) in the search bar to enable semantic mode
- Type a natural language query:
- "sunset over mountains with orange sky"
- "cyberpunk city at night"
- "portrait of a woman with blue hair"
- 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
- Your query is converted to a text embedding using SigLIP2
- The embedding is compared against all indexed image embeddings
- Results are ranked by cosine similarity (closest matches first)
- 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
- Select an asset in the grid view (click once)
- Right-click โ Find Similar (or click the Clone icon)
- View visually similar assets ranked by similarity score
- Adjust
top_kparameter 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
- Image embedding is compared against predefined tag vocabulary
- Tags with similarity above threshold are suggested
- Auto-tags are stored separately from user tags
- 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
- Open an asset in the Viewer
- Look at the Sidebar โ AI Tags section
- See suggested tags with confidence scores
How to Apply Auto-Tags
- In Viewer sidebar, click Apply All to add all AI tags
- Or click individual tags to add selectively
- Tags are merged with your existing tags
Configuration
Adjust sensitivity via environment variable:
# 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
- Open an asset in the Viewer
- In the sidebar, find Image Description section
- Click Generate button
- Wait for caption generation (5-30 seconds)
- 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:
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_captionfield - 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:
- Multi-segment imageโtext score (60% weight)
- Prompt split into segments
- Each segment scored against image
- Length-weighted average with best-segment bonus
- Captionโprompt text similarity (20% weight)
- Florence-2 caption compared to prompt
- Text-to-text coherence measure
- 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
- Open asset in Viewer
- Look for Prompt Alignment section in sidebar
- 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
- Open Collections panel
- Click Smart Suggestions button
- Review AI-suggested collections
- 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
- Asset embeddings are clustered using k-means
- Each cluster is analyzed for common themes
- Representative tags and descriptions generated
- Suggestions presented for approval
Configuration
Adjust number of suggestions:
{
"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
- Open Collections panel
- Click Discover Groups button
- Wait for clustering to complete
- Browse discovered groups
- 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
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)
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)
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
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
- Enable GPU acceleration (CUDA):
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
- Adjust batch size for your hardware:
- Low RAM:
MJR_AM_VECTOR_BATCH_SIZE=8 - High RAM:
MJR_AM_VECTOR_BATCH_SIZE=64
- Use CPU-only mode if GPU memory is limited:
export CUDA_VISIBLE_DEVICES=""
- Limit index size for large libraries:
- Vector searcher caps at 100,000 assets
- Most recent assets prioritized
- 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=1in environment - Restart ComfyUI after enabling
- Check console for model loading errors
2. Model Download Fails
Symptoms: Error downloading models, timeout
Solutions:
# 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
# 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:
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
GET /mjr/am/vector/search?q={query}&top_k={count}&scope={scope}
Parameters:
q(required): Natural language querytop_k(optional): Max results (default: 20, max: 200)scope(optional):output,input,custom,all
Example:
curl "http://localhost:8188/mjr/am/vector/search?q=sunset%20beach&top_k=10"
Response:
{
"ok": true,
"data": [
{
"id": 12345,
"asset_id": 12345,
"filepath": "/path/to/image.png",
"filename": "image.png",
"kind": "image",
"_vectorScore": 0.8923
}
]
}
Find Similar
GET /mjr/am/vector/similar/{asset_id}?top_k={count}
Parameters:
asset_id(required): Reference asset IDtop_k(optional): Max results (default: 20)
Example:
curl "http://localhost:8188/mjr/am/vector/similar/12345?top_k=10"
Prompt Alignment
GET /mjr/am/vector/alignment/{asset_id}
Response:
{
"ok": true,
"data": 0.7523
}
Generate Caption
POST /mjr/am/vector/caption/{asset_id}
Response:
{
"ok": true,
"data": "An anime-style illustration of..."
}
Auto-Tags
GET /mjr/am/vector/auto-tags/{asset_id}
Response:
{
"ok": true,
"data": ["anime", "portrait", "fantasy"]
}
Suggest Collections
POST /mjr/am/vector/suggest-collections
Content-Type: application/json
{"k": 8}
Response:
{
"ok": true,
"data": {
"suggestions": [
{
"name": "Cyberpunk Collection",
"query": "cyberpunk neon city night",
"estimated_size": 45,
"tags": ["cyberpunk", "sci-fi", "night"]
}
]
}
}
Vector Stats
GET /mjr/am/vector/stats
Response:
{
"ok": true,
"data": {
"total": 1234,
"avg_score": 0.6523,
"dim": 1152,
"enabled": true
}
}
Re-index Asset
POST /mjr/am/vector/index/{asset_id}
Force re-computation of embedding for a single asset.
Privacy & Security
For a dedicated user-facing explanation focused on privacy, offline behavior, and token meaning, see PRIVACY_OFFLINE.md.
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
In practice, semantic search, find-similar, prompt alignment, caption generation, and auto-tagging run against models loaded inside your local ComfyUI / Majoor process after those models are available locally.
Network Access
Initial model download requires internet access:
- Downloads from HuggingFace CDN
- One-time download per model
- Subsequent uses work offline
- Optional HuggingFace token only affects HuggingFace Hub downloads and rate limits
Token Clarification
Majoor exposes two different token concepts that are easy to confuse:
- HuggingFace token: optional, used for downloading model files from HuggingFace Hub with better rate limits. It is not a hosted AI inference key.
- Majoor API token: used to secure remote write access to the local Majoor backend when ComfyUI is reachable over LAN, reverse proxy, or tunnel. It is not used to send prompts or images to an external AI service.
Offline Use
Offline use is supported once the required models are already cached locally.
- If AI models are already present in the HuggingFace cache, AI features can run without internet access.
- If models are not cached yet, the first model bootstrap requires network access.
- Non-AI Majoor features do not depend on HuggingFace model downloads.
Security Considerations
- Model Integrity: Models downloaded from official HuggingFace repos
- Model Code Surface: AI inference is local, but some model loading still depends on upstream HuggingFace/Transformers model packages and compatibility loaders
- Sandboxing: Models run in same process as ComfyUI
- Resource Limits: Built-in rate limiting prevents abuse
Environment Isolation
For enhanced security, run in isolated environment:
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt -r requirements-vector.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
Community
Model Documentation
This documentation is part of the Majoor Assets Manager project. Copyright ยฉ 2026 Ewald ALOEBOETOE (MajoorWaldi)
docs/AI_FEATURES.md
API_REFERENCE.md
Majoor API Reference
Base Path: /mjr/am Version: 2.4.5 Last Updated: April 7, 2026
Compatibility
Versioning
- Current API:
/mjr/am/*(unversioned, stable) - Version Alias:
/mjr/am/v1/โ redirects to/mjr/am/(308 Permanent Redirect) - Legacy Alias:
/majoor/versionโ redirects to/mjr/am/version
Authentication
Most endpoints require authentication when ComfyUI auth is enabled:
- Read operations: May work without auth (depends on configuration)
- Write operations: Require authentication or API token
- CSRF Protection: All state-changing endpoints require
X-Requested-With: XMLHttpRequestorX-CSRF-Token
API Token
For remote access or when MAJOOR_API_TOKEN is configured:
X-MJR-Token: <your-token>
# OR
Authorization: Bearer <your-token>
Table of Contents
- Health & Diagnostics
- Settings & Configuration
- Search & Discovery
- Asset Operations
- Indexing & Scanning
- Collections & Custom Roots
- Viewer & Metadata
- Download & Export
- Database Maintenance
- Utilities
Health & Diagnostics
Health Summary
GET /mjr/am/health
Response: Extension health summary including version, status, and basic metrics.
{
"ok": true,
"data": {
"version": "2.4.5",
"status": "healthy",
"indexed_count": 1234,
"scopes_available": ["output", "input", "custom", "collections"]
}
}
Health Counters
GET /mjr/am/health/counters
Response: Indexed counters for all scopes.
{
"ok": true,
"data": {
"output": { "count": 1000, "last_scan": "2026-02-28T10:00:00Z" },
"input": { "count": 200, "last_scan": "2026-02-28T10:00:00Z" },
"custom": { "count": 34, "last_scan": "2026-02-28T10:00:00Z" },
"collections": { "count": 5 }
}
}
Database Health
GET /mjr/am/health/db
Response: Database diagnostics including schema version, integrity status, and statistics.
{
"ok": true,
"data": {
"schema_version": 7,
"integrity_check": "ok",
"page_count": 1024,
"freelist_count": 128,
"cache_size": 2000
}
}
Runtime Configuration
GET /mjr/am/config
Response: Current runtime configuration snapshot.
{
"ok": true,
"data": {
"output_directory": "/path/to/output",
"index_directory": "/path/to/_mjr_index",
"media_probe_backend": "auto",
"db_timeout": 30.0,
"max_connections": 8,
"safe_mode": true,
"allow_symlinks": false
}
}
Version Information
GET /mjr/am/version
Response: Extension version and build information.
{
"ok": true,
"data": {
"version": "2.4.5",
"branch": "main",
"build_date": "2026-03-29",
"python_version": "3.11.0",
"comfyui_version": "0.13.0"
}
}
Tool Status
GET /mjr/am/tools/status
Response: Availability status of external tools (ExifTool, FFprobe).
{
"ok": true,
"data": {
"exiftool": { "available": true, "version": "12.40" },
"ffprobe": { "available": true, "version": "6.0" },
"backend": "both"
}
}
Settings & Configuration
Probe Backend Settings
GET /mjr/am/settings/probe-backend
POST /mjr/am/settings/probe-backend
GET: Read current probe mode. POST: Set probe mode.
Request Body (POST):
{
"mode": "auto" // "auto", "exiftool", "ffprobe", "both"
}
Response:
{
"ok": true,
"data": {
"mode": "auto",
"available": ["exiftool", "ffprobe"]
}
}
Output Directory Override
GET /mjr/am/settings/output-directory
POST /mjr/am/settings/output-directory
GET: Read output directory override. POST: Set output directory override.
Request Body (POST):
{
"path": "/path/to/custom/output"
}
Response:
{
"ok": true,
"data": {
"path": "/path/to/custom/output",
"exists": true,
"writable": true
}
}
Index Directory Override
GET /mjr/am/settings/index-directory
POST /mjr/am/settings/index-directory
GET: Read the current index directory path.
Response:
{
"ok": true,
"data": {
"index_directory": "/path/to/_mjr_index"
}
}
POST: Set a new index directory path.
Requires write access (loopback or valid X-MJR-Token).
Request Body (POST):
{
"index_directory": "/path/to/local/mjr_index"
}
Send an empty string to clear the override and revert to the default (<output>/_mjr_index/):
{
"index_directory": ""
}
Response (POST):
{
"ok": true,
"data": {
"index_directory": "/path/to/local/mjr_index",
"requires_restart": true
}
}
The new path is persisted to the .mjr_index_directory_override sidecar file and the in-process environment variables. The change takes effect after ComfyUI is restarted.
Error codes:
| Code | Meaning |
|---|---|
INVALID_INPUT | Path exists but is a file, not a directory |
INVALID_INPUT | Path was given as a non-empty string but the parent directory does not exist |
DB_ERROR | Sidecar file could not be written |
Metadata Fallback Toggles
GET /mjr/am/settings/metadata-fallback
POST /mjr/am/settings/metadata-fallback
GET: Read fallback toggles for metadata extraction. POST: Set fallback toggles.
Request Body (POST):
{
"image": true, // Use internal parser if ExifTool fails
"media": true // Use hachoir if ffprobe unavailable
}
Response:
{
"ok": true,
"data": {
"image": true,
"media": true
}
}
Search & Discovery
Search with Filters
GET /mjr/am/search
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
q | string | Search query (full-text) |
scope | string | Scope: output, input, custom, collections |
root_id | string | Custom root ID (for custom scope) |
collection_id | string | Collection ID (for collections scope) |
kind | string | File kind: image, video, audio, model3d, all |
min_rating | integer | Minimum rating (0-5) |
workflow | boolean | Filter by workflow presence |
date_from | string | Start date (ISO 8601) |
date_to | string | End date (ISO 8601) |
size_from | integer | Minimum file size (bytes) |
size_to | integer | Maximum file size (bytes) |
width_from | integer | Minimum image width |
width_to | integer | Maximum image width |
height_from | integer | Minimum image height |
height_to | integer | Maximum image height |
sort | string | Sort field: relevance, name, date, size, rating |
order | string | Sort order: asc, desc |
page | integer | Page number (1-based) |
page_size | integer | Items per page (default: 50) |
hide_png_siblings | boolean | Hide PNGs when video exists |
Example:
GET /mjr/am/search?q=fantasy&scope=output&kind=image&min_rating=4&sort=date&order=desc&page=1&page_size=50
Response:
{
"ok": true,
"data": {
"total": 150,
"page": 1,
"page_size": 50,
"total_pages": 3,
"items": [
{
"id": "asset_123",
"filename": "fantasy_character.png",
"path": "/path/to/output/fantasy_character.png",
"type": "image",
"size": 2048576,
"width": 1024,
"height": 1536,
"rating": 5,
"tags": ["fantasy", "character"],
"created_at": "2026-02-28T10:00:00Z",
"metadata": {
"prompt": "A fantasy character...",
"model": "SDXL",
"steps": 30
}
}
]
}
}
List Assets (Paginated)
GET /mjr/am/list
Query Parameters: Same as search, but without q parameter.
Purpose: List assets without full-text search (faster for browsing).
Asset Details
GET /mjr/am/asset/{asset_id}
Response: Full asset details including metadata.
{
"ok": true,
"data": {
"id": "asset_123",
"filename": "fantasy_character.png",
"path": "/path/to/output/fantasy_character.png",
"type": "image",
"extension": "png",
"size": 2048576,
"width": 1024,
"height": 1536,
"rating": 5,
"tags": ["fantasy", "character"],
"created_at": "2026-02-28T10:00:00Z",
"modified_at": "2026-02-28T10:00:00Z",
"metadata": {
"prompt": "A fantasy character...",
"negative_prompt": "ugly, blurry",
"model": "SDXL",
"sampler": "DPM++ 2M Karras",
"steps": 30,
"cfg_scale": 7.0,
"seed": 123456789,
"workflow": { ... }
},
"thumbnails": {
"small": "/mjr/am/thumbnail/asset_123?size=small",
"medium": "/mjr/am/thumbnail/asset_123?size=medium",
"large": "/mjr/am/thumbnail/asset_123?size=large"
}
}
}
Metadata Lookup
GET /mjr/am/metadata
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
type | string | Scope type: output, input, custom |
filename | string | Filename |
subfolder | string | Subfolder path (optional) |
root_id | string | Custom root ID (for custom scope) |
Example:
GET /mjr/am/metadata?type=output&filename=image.png&subfolder=2026-02-28
Response:
{
"ok": true,
"data": {
"filename": "image.png",
"path": "/path/to/output/2026-02-28/image.png",
"metadata": { ... }
}
}
Asset Operations
Update Rating
POST /mjr/am/asset/rating
Request Body:
{
"asset_id": "asset_123",
"rating": 5 // 0-5
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"rating": 5,
"synced_to_file": true
}
}
Update Tags
POST /mjr/am/asset/tags
Request Body:
{
"asset_id": "asset_123",
"tags": ["fantasy", "character", "sdxl"]
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"tags": ["fantasy", "character", "sdxl"],
"synced_to_file": true
}
}
Rename Asset
POST /mjr/am/asset/rename
Request Body:
{
"asset_id": "asset_123",
"new_filename": "new_name.png"
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"old_path": "/path/to/old_name.png",
"new_path": "/path/to/new_name.png"
}
}
Note: Requires MAJOOR_ALLOW_RENAME=1 if Safe Mode is enabled.
Delete Single Asset
POST /mjr/am/asset/delete
Request Body:
{
"asset_id": "asset_123"
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"deleted": true,
"path": "/path/to/deleted_file.png"
}
}
Note: Requires MAJOOR_ALLOW_DELETE=1 if Safe Mode is enabled.
Bulk Delete Assets
POST /mjr/am/assets/delete
Request Body:
{
"asset_ids": ["asset_123", "asset_124", "asset_125"]
}
Response:
{
"ok": true,
"data": {
"deleted_count": 3,
"deleted_ids": ["asset_123", "asset_124", "asset_125"],
"failed": []
}
}
Note: Requires MAJOOR_ALLOW_DELETE=1 if Safe Mode is enabled.
Open in Folder
POST /mjr/am/open-in-folder
Request Body:
{
"asset_id": "asset_123"
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"path": "/path/to/output",
"opened": true
}
}
Note: Requires MAJOOR_ALLOW_OPEN_IN_FOLDER=1 if Safe Mode is enabled.
Stage to Input
POST /mjr/am/stage-to-input
Request Body:
{
"asset_id": "asset_123"
}
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"staged_path": "/path/to/input/staged_file.png",
"staged": true
}
}
Purpose: Copy/link asset to ComfyUI input directory for workflow use.
Indexing & Scanning
Scan Scope/Root
POST /mjr/am/scan
Request Body:
{
"scope": "output", // "output", "input", "custom"
"root_id": null, // Custom root ID (for custom scope)
"incremental": true, // Incremental scan (default)
"force": false // Force full rescan
}
Response:
{
"ok": true,
"data": {
"scan_id": "scan_123",
"scope": "output",
"status": "started",
"estimated_duration_ms": 5000
}
}
Note: Scan runs in background. Status available via health endpoints.
Index Explicit Files
POST /mjr/am/index-files
Request Body:
{
"files": [
{
"filename": "image.png",
"subfolder": "2026-02-28",
"type": "output"
}
],
"origin": "generation" // "generation", "manual", "import"
}
Response:
{
"ok": true,
"data": {
"indexed_count": 1,
"failed": []
}
}
Purpose: Index specific files (used for real-time generation tracking).
Reset Index
POST /mjr/am/index/reset
Request Body:
{
"scope": "output", // Scope to reset
"clear_assets": true, // Clear asset records
"clear_metadata": true, // Clear metadata cache
"clear_fts": true, // Clear full-text search index
"clear_journal": true // Clear scan journal
}
Response:
{
"ok": true,
"data": {
"scope": "output",
"reset": true,
"rescan_triggered": true
}
}
Collections & Custom Roots
List Collections
GET /mjr/am/collections
Response:
{
"ok": true,
"data": {
"collections": [
{
"id": "collection_123",
"name": "Best Characters",
"item_count": 25,
"created_at": "2026-02-28T10:00:00Z",
"modified_at": "2026-02-28T12:00:00Z"
}
]
}
}
Create/Update Collection
POST /mjr/am/collections/create
POST /mjr/am/collections/update
Request Body (Create):
{
"name": "Best Characters",
"items": [
{
"asset_id": "asset_123",
"scope": "output"
}
]
}
Request Body (Update):
{
"collection_id": "collection_123",
"name": "Updated Name",
"items": [...],
"add_items": [...], // Items to add
"remove_items": [...] // Items to remove
}
Response:
{
"ok": true,
"data": {
"collection_id": "collection_123",
"name": "Updated Name",
"item_count": 30
}
}
Delete Collection
POST /mjr/am/collections/delete
Request Body:
{
"collection_id": "collection_123"
}
Response:
{
"ok": true,
"data": {
"collection_id": "collection_123",
"deleted": true
}
}
Note: Does not delete assets, only the collection definition.
List Custom Roots
GET /mjr/am/custom-roots
Response:
{
"ok": true,
"data": {
"roots": [
{
"id": "root_123",
"path": "/path/to/custom/dir",
"name": "Custom Directory",
"added_at": "2026-02-28T10:00:00Z"
}
]
}
}
Add Custom Root
POST /mjr/am/custom-roots/add
Request Body:
{
"path": "/path/to/custom/dir",
"name": "Custom Directory"
}
Response:
{
"ok": true,
"data": {
"root_id": "root_123",
"path": "/path/to/custom/dir",
"added": true
}
}
Remove Custom Root
POST /mjr/am/custom-roots/remove
Request Body:
{
"root_id": "root_123"
}
Response:
{
"ok": true,
"data": {
"root_id": "root_123",
"removed": true
}
}
Viewer & Metadata
Viewer Info
GET /mjr/am/viewer/info
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
asset_id | string | Asset ID |
Example:
GET /mjr/am/viewer/info?asset_id=asset_123
Response:
{
"ok": true,
"data": {
"asset_id": "asset_123",
"filename": "fantasy_character.png",
"type": "image",
"width": 1024,
"height": 1536,
"metadata": {
"prompt": "A fantasy character...",
"model": "SDXL",
"workflow_minimap": { ... }
}
}
}
Download & Export
Download Asset
GET /mjr/am/download
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
asset_id | string | Asset ID |
filename | string | Filename (for direct download) |
type | string | Scope type |
subfolder | string | Subfolder path |
Example:
GET /mjr/am/download?asset_id=asset_123
Response: File stream with appropriate Content-Type and Content-Disposition headers.
Create Batch ZIP
POST /mjr/am/batch-zip
Request Body:
{
"asset_ids": ["asset_123", "asset_124", "asset_125"]
}
Response:
{
"ok": true,
"data": {
"token": "zip_token_abc123",
"expires_at": "2026-02-28T11:00:00Z",
"download_url": "/mjr/am/batch-zip/zip_token_abc123"
}
}
Download Batch ZIP
GET /mjr/am/batch-zip/{token}
Response: ZIP file stream containing all requested assets.
Database Maintenance
Optimize Database
POST /mjr/am/db/optimize
Purpose: Run PRAGMA optimize and ANALYZE for performance.
Response:
{
"ok": true,
"data": {
"optimized": true,
"duration_ms": 150
}
}
Cleanup Case Duplicates
POST /mjr/am/db/cleanup-case-duplicates
Purpose: Remove historical path-case duplicates (Windows-specific).
Response:
{
"ok": true,
"data": {
"duplicates_removed": 5,
"kept_canonical": 100
}
}
Force-Delete Database
POST /mjr/am/db/force-delete
Purpose: Emergency recovery for corrupted databases.
Response:
{
"ok": true,
"data": {
"deleted": true,
"reinitialized": true,
"rescan_triggered": true
}
}
Note: This endpoint bypasses normal DB-dependent security checks for emergency recovery.
Utilities
Date Histogram
GET /mjr/am/date-histogram
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
month | string | Month in YYYY-MM format |
scope | string | Scope type |
Example:
GET /mjr/am/date-histogram?month=2026-02&scope=output
Response:
{
"ok": true,
"data": {
"month": "2026-02",
"days": [
{ "date": "2026-02-01", "count": 50 },
{ "date": "2026-02-02", "count": 75 },
...
]
}
}
Purpose: Calendar histogram for date-based filtering UI.
Duplicate Alerts
GET /mjr/am/duplicates/alerts
Response:
{
"ok": true,
"data": {
"duplicate_groups": [
{
"filename": "image.png",
"paths": [
"/path/to/output/image.png",
"/path/to/output/subfolder/image.png"
],
"count": 2
}
],
"total_groups": 5,
"total_duplicates": 10
}
}
Releases Information
GET /mjr/am/releases
Response:
{
"ok": true,
"data": {
"current_version": "2.4.5",
"branch": "main",
"latest_release": {
"version": "2.4.5",
"date": "2026-04-10",
"download_url": "https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/releases/tag/v2.4.5"
},
"branches_available": ["main", "dev"],
"tags_available": ["v2.4.5", "v2.4.4", "v2.4.3", "v2.4.2"]
}
}
Security Notes
CSRF Protection
All state-changing endpoints (POST, PUT, PATCH, DELETE) require:
X-Requested-With: XMLHttpRequestheader, ORX-CSRF-Tokenheader with valid token
Authentication
When ComfyUI auth is enabled:
- Write operations require authenticated user
- API token can be used for remote access
- Token sent via
X-MJR-TokenorAuthorization: Bearer
Safe Mode
When Safe Mode is enabled (default):
- Delete operations require
MAJOOR_ALLOW_DELETE=1 - Rename operations require
MAJOOR_ALLOW_RENAME=1 - Open in folder requires
MAJOOR_ALLOW_OPEN_IN_FOLDER=1
Rate Limiting
Expensive endpoints are rate-limited:
- Search: 10 requests/minute per client
- Scan: 5 requests/minute per client
- Batch ZIP: 3 requests/minute per client
- Metadata: 20 requests/minute per client
Client identity based on IP address (or X-Forwarded-For from trusted proxies).
Error Responses
Standard Error Format
{
"ok": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": { ... } // Optional additional context
}
}
Common Error Codes
| Code | HTTP Status | Description |
|---|---|---|
AUTH_REQUIRED | 401 | Authentication required |
FORBIDDEN | 403 | Operation not allowed (Safe Mode) |
NOT_FOUND | 404 | Resource not found |
INVALID_REQUEST | 400 | Invalid request body/parameters |
RATE_LIMITED | 429 | Too many requests |
DB_ERROR | 500 | Database error |
FILE_SYSTEM_ERROR | 500 | File system error |
TOOL_UNAVAILABLE | 503 | External tool not available |
WebSocket Events
Real-Time Updates
The extension emits ComfyUI API events for real-time updates:
mjr-asset-added: New asset indexedmjr-asset-updated: Asset metadata updatedmjr-scan-complete: Scan operation completedmjr-enrichment-status: Metadata enrichment statusmjr-db-restore-status: Database restore status
Example (Frontend):
api.addEventListener("mjr-asset-added", (event) => {
const asset = event.detail;
// Update UI with new asset
});
API Reference Version: 2.0 Last Updated: April 5, 2026 Compatible with Majoor Assets Manager v2.4.4+
docs/API_REFERENCE.md
ARCHITECTURE_MAP.md
Architecture Map
Version: 2.4.5 Last Updated: April 10, 2026
This map is the working reference for backend refactors. It is intentionally short and operational.
Package Responsibilities
mjr_am_backend/routes- HTTP surface only.
- Parse request data, apply auth/CSRF/rate-limit guards, call services, map
Resultpayloads. route_catalog.pyโ declarative route registry (RouteRegistrationdataclass, CORE + OPTIONAL catalogs).mjr_am_backend/routes/core- Shared route helpers for security, path rules, JSON parsing, responses, and service resolution.
mjr_am_backend/routes/assets- Thin compatibility facades re-exporting from
features/assets/. mjr_am_backend/features- Business logic and orchestration by domain: index, metadata, collections, browser, health, tags, viewer.
mjr_am_backend/features/assets- Asset domain sub-services:
lookup_serviceโ asset lookups and queriesdownload_serviceโ download/streaming logicrating_tags_serviceโ ratings, tags, collectionsrequest_context_serviceโ request parsing and context extractionpath_resolution_serviceโ root/subfolder/path resolutiondelete_serviceโ asset deletionrename_serviceโ asset renamingfilename_validatorโ filename validation rulesmodelsโ shared data modelsserviceโ thin compatibility facademjr_am_backend/adapters- Boundary code for SQLite, filesystem watchers, external tools, and other integration points.
mjr_am_backend/adapters/dbsqlite_facade.pyโ main entry point for DB access (~1900 L)sqlite_connections.pyโ connection pool, pragmas, acquire/releasesqlite_execution.pyโ query execution, timeout, retry, batchsqlite_lifecycle.pyโ transactions, reset, WAL, Windows file-handle managementsqlite_recovery.pyโ malformed recovery, FTS rebuild, schema repairschema.py(331 L) +schema_sql.py,schema_fts.py,schema_vec.pyโ DDL and migrationconnection_pool.py,db_recovery.py,transaction_manager.pyโ lower-level helpersmjr_am_shared- Shared result types, error definitions, logging, time helpers, and cross-cutting utilities.
Frontend
- Vue 3 + Pinia for all major UI surfaces (grid, sidebar, feed, context menus, settings).
js/vue/โ Vue components and stores.js/features/โ feature-scoped JS modules.js/stores/โ Pinia stores.- Imperative runtime services remain for viewer, DnD, and ComfyUI integration (by design).
Request Flow
UI action -> route handler -> route/core helper -> feature service -> adapter -> DB/filesystem/tool -> Result response -> UI mapping
Rules:
- Routes do not decide filesystem or security policy locally.
- Services own business rules and orchestration.
- Adapters isolate external side effects.
security.pyis the single policy entry point for auth, write access, safe mode, trusted proxies, and CSRF.
Watcher And Index Flow
Filesystem event -> watcher scope/runtime -> index orchestration -> DB update -> UI refresh event
Hot zones:
- watcher lifecycle and scope resolution
- reset/rebuild orchestration
- metadata probing and vector backfill
- live viewer synchronization
These zones should be refactored behind stable service facades before adding new behavior.
Security Rules
- Public API base stays
/mjr/am/*. - Backend returns structured
Resultpayloads for expected errors. - Safe mode is enabled by default.
- Remote write remains denied without a valid token.
- Loopback write is allowed by default in the current compatibility model unless
MAJOOR_REQUIRE_AUTH=1is set.
Naming And Internal API Conventions
- Public internal APIs should be thin, stable facades under
features/orroutes/core/. - Route modules may import facades and helpers, but should not directly orchestrate storage internals across multiple subsystems.
- New security-sensitive mutations must call shared security and path helpers instead of duplicating checks.
docs/ARCHITECTURE_MAP.md
AUDIT_GRID_REACTIVITY_APR26.md
Audit โ Rรฉactivitรฉ grille, chargement, cache & premier lancement
Date : 2026-04-22
Pรฉrimรจtre
Audit ciblรฉ sur le pipeline de rendu de la grille d'assets : ouverture du panneau, premier chargement, switch de scope, cache snapshot, รฉvรฉnements temps rรฉel et pagination. Objectif : suppression des doubles chargements, des sรฉrialisations bloquantes et des relances concurrentes pour obtenir un comportement strictement live.
Fichiers principaux auditรฉs :
- js/vue/composables/useGridLoader.js
- js/vue/composables/useVirtualGrid.js
- js/features/grid/gridApi.js
- js/features/grid/StackGroupCards.js
- js/features/panel/panelRuntime.js
- js/features/panel/panelGridEventBindings.js
- js/features/panel/panelSettingsSync.js
- js/features/panel/controllers/scopeController.js
- js/features/panel/controllers/gridController.js
- js/features/runtime/registerRealtimeListeners.js
- js/features/runtime/entryUiRegistration.js
- js/vue/components/grid/VirtualAssetGridHost.vue
1. Constats
1.1 [HIGH] Double hydratation snapshot lors du switch de scope (corrigรฉ)
Symptรดme : Pour chaque switch de scope avec query == "*", la mรชme hydratation depuis GRID_SNAPSHOT_CACHE รฉtait jouรฉe deux fois consรฉcutivement (appendAssets + setItems + finalizeLoad), provoquant un re-render perceptible et une charge CPU inutile.
Chaรฎne d'appel :
scopeController.setScopeโonBeforeReloadโprepareGridForScopeSwitch()- Le contrรดleur enchaรฎne
await reloadGrid()โloadAssets(). loadAssets()dรฉtectedeferVisualResetUntilNextPage = true(assets visibles)
appelle void hydrateFromSnapshot(...) (synchrone interne, peuple state.assets).
et entre dans le fast-path snapshot qui appelle hydrateFromSnapshot() une seconde fois sur la mรชme clรฉ.
Correctif : Le hook hydrateFromSnapshot marque maintenant la clรฉ hydratรฉe sur l'รฉtat (state._mjrLastHydrateKey / _mjrLastHydrateAt). Le fast-path de loadAssets() court-circuite l'appel si la derniรจre hydratation porte sur la mรชme clรฉ et date de moins de 1500 ms ; il se contente de planifier un prefetch de la page suivante.
Fichier : useGridLoader.js
1.2 [MED] Persistance sessionStorage synchrone ร chaque page (corrigรฉ)
Symptรดme : rememberSnapshot() est appelรฉ ร chaque page chargรฉe (loadNextPage) et ร chaque rรฉussite d'hydratation. Chaque appel chaรฎne pruneGridSnapshotCache() puis JSON.stringify de jusqu'ร 8 snapshots ร 800 assets โ plusieurs Mo de payload, sur le main thread, et un storage.setItem. Pendant un scroll soutenu sur 7000+ assets, ce coรปt se rรฉpรจte ร chaque page, dรฉgradant la fluiditรฉ de la pagination.
Correctif : persistGridSnapshotsToStorage() devient un debounce 500 ms au-dessus de persistGridSnapshotsToStorageNow(). dispose() flush explicitement avant unload pour ne perdre aucun snapshot.
Fichier : useGridLoader.js
1.3 [MED] Premier lancement : cache affichรฉ sans rafraรฎchissement de fond (corrigรฉ)
Symptรดme : Lors d'une ouverture du panneau avec snapshot disponible (didHydrateFromSnapshot == true), panelRuntime.js court-circuite reloadGrid() et n'รฉmet aucune requรชte API. L'utilisateur voit donc des cartes potentiellement vieilles de 30 minutes (TTL snapshot) jusqu'au prochain รฉvรฉnement temps rรฉel ou interaction. Le comportement attendu est live.
Correctif : panelRuntime.js planifie maintenant un reloadGrid() silencieux 200 ms aprรจs l'ouverture lorsque l'hydrate snapshot a rรฉussi. Les cartes en cache restent visibles (preserve-visible-until-ready) jusqu'ร l'arrivรฉe de la premiรจre page fraรฎche.
Fichier : panelRuntime.js
1.4 [LOW] Multiples upserts par asset gรฉnรฉrรฉ
Constat : Pour un mรชme asset issu d'une gรฉnรฉration, jusqu'ร trois รฉvรฉnements temps rรฉel arrivent en cascade :
| Event | Source | Effet |
|---|---|---|
NEW_GENERATION_OUTPUT | exรฉcution Comfy | placeholder upsert (immรฉdiat, force) |
mjr-asset-added | watcher backend | upsert (immรฉdiat) |
ASSET_INDEXED | indexation | upsert (immรฉdiat, force) |
Chaque upsert passe par upsertAssetNow() qui coalesce via queueMicrotask, mais comme les WebSocket arrivent dans des macrotรขches distinctes, on obtient typiquement 3 vg.setItems(state.assets) par asset.
Statut : Comportement actuellement acceptable. La coalescence microtรขche gรจre les bursts simultanรฉs (une seule gรฉnรฉration de 5 cartes โ 1 setItems). Les 3 รฉvรฉnements espacรฉs mettent ร jour des champs successifs (placeholder โ mรฉtadonnรฉes โ enrichissement). Aucune action immรฉdiate.
Recommandation future : ajouter un debounce 50 ms par assetId cรดtรฉ upsertLiveAssetIntoGrid si une rรฉgression de fluiditรฉ est constatรฉe.
Fichier : registerRealtimeListeners.js
1.5 [LOW] MIN_LOADING_SKELETON_MS artificiellement bloque les chargements rapides
Constat : loadNextPage() impose un await waitMs(50 - elapsed) quand la page revient en moins de 50 ms (cas snapshot ou cache HTTP chaud). Ajoute jusqu'ร 50 ms de latence perรงue.
Statut : Conservรฉ volontairement pour รฉviter le clignotement du skeleton. Pas de modification.
1.6 [INFO] Sync dataset redondant entre onBeforeReload et runReloadOnce
Constat : panelRuntime.onBeforeReload รฉcrit gridContainer.dataset.mjrScope/Subfolder/..., puis quelques ยตs plus tard gridController.runReloadOnce rรฉ-รฉcrit le mรชme set. Pas de risque, lรฉgรจre duplication. ร fusionner lors d'un refactor mais sans urgence.
1.7 [INFO] Nettoyage cache stack/dup au switch de scope
Vรฉrifiรฉ : prepareGridForScopeSwitch() appelle bien disposeStackGroupCards() (corrige BUG #1 mรฉmoire). Les รฉtats state.stemMap / state.filenameCounts sont quant ร eux rรฉinitialisรฉs via resetAssetCollectionsState() au moment oรน la premiรจre page de la nouvelle scope arrive (cf. loadNextPage if (deferVisualResetUntilNextPage)). OK.
1.8 [INFO] Early-fetch output:*:mtime_desc
Vรฉrifiรฉ : startEarlyFetch() est idempotent (clรฉ + TTL 15 s + AbortController unique). consumeEarlyFetch() purge l'entrรฉe ร la consommation. La promesse est rejetรฉe proprement sur pagehide. Comportement correct.
1.9 [INFO] VirtualAssetGridHost โ gating premiรจre mesure
Vรฉrifiรฉ : hasMeasuredHostWidthOnce empรชche le premier paint avant que ResizeObserver ait livrรฉ la largeur de l'hรดte (corrige le double-render historique). OK.
2. Synthรจse des corrections appliquรฉes
| # | Fichier | Type | Effet |
|---|---|---|---|
| 1 | useGridLoader.js | Dรฉdoublonnage hydrate scope-switch | -1 cycle hydrate complet par switch (~10โ40 ms perรงus) |
| 2 | useGridLoader.js | Debounce persist sessionStorage (500 ms) | Suppression des stringify multi-Mo en boucle pendant scroll |
| 3 | useGridLoader.js | Marqueur _mjrLastHydrateKey | Sรฉcurise le guard #1 |
| 4 | useGridLoader.js | Flush persist sur dispose | Pas de perte de snapshot |
| 5 | panelRuntime.js | Refresh silencieux aprรจs hydrate au premier lancement | Garantit un grid ร jour โค 200 ms aprรจs l'ouverture |
3. Pistes restantes (non bloquantes)
- Coalescer les 3 รฉvรฉnements
NEW_GEN_OUTPUT/ASSET_ADDED/ASSET_INDEXED - *Mutualiser la mise ร jour des
dataset.mjr** entreonBeforeReloadet - Diagnostic cache : exposer
window.__MJR_GRID_CACHE_STATS__(size, - Snapshot par scope plus fin : actuellement la persistance รฉcrit toutes
par assetId sur 50 ms cรดtรฉ registerRealtimeListeners pour rรฉduire le nombre de setItems lors d'une rafale de gรฉnรฉrations.
runReloadOnce dans un helper unique.
hits, misses) en mode debug pour profiler le ratio cache/fetch.
les snapshots ; possible d'รฉcrire uniquement la derniรจre modifiรฉe (delta write) si la taille devient gรชnante.
4. Validation
get_errorssur les fichiers modifiรฉs : aucune erreur.- Aucun changement d'API publique :
loadAssets/reloadGrid/ - Tests ร exรฉcuter :
vitestsurjs/tests/grid_loader_*.vitest.mjs.
prepareGridForScopeSwitch conservent leur signature.
docs/AUDIT_GRID_REACTIVITY_APR26.md
CONTRIBUTING.md
Contributing to Majoor Assets Manager
Thank you for your interest in contributing to ComfyUI-Majoor-AssetsManager! This guide will help you set up your development environment, understand our coding standards, and submit quality contributions.
Table of Contents
- Development Setup
- Project Structure
- Coding Standards
- Testing
- Submitting Contributions
- Git Workflow
- Code Quality Gate
- Documentation
- Getting Help
Development Setup
Prerequisites
- Python: 3.10 - 3.13 (3.14 not yet supported)
- Node.js: 18+ (LTS recommended)
- Git: 2.30+
- ComfyUI: >= 0.13.0 (running instance for testing)
Quick Start
# 1. Clone the repository
git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager.git
cd ComfyUI-Majoor-AssetsManager
# 2. Install Python dependencies
pip install -r requirements.txt
# For AI/vector features (optional):
pip install -r requirements.txt -r requirements-vector.txt
# For development tooling (optional but recommended):
pip install -r requirements-dev.txt
# 3. Install Node dependencies
npm install
# 4. Install Git hooks (pre-commit, pre-push quality gates)
npm run hooks:install
# 5. Build frontend (if making JS/Vue changes)
npm run build
Development Workflow
# Watch mode for frontend development (auto-rebuild)
npm run build:watch
# Run Python tests
pytest
# Run frontend tests
npm run test:js
# Run quality checks (linters, type checkers, security scanners)
npm run quality
# Python-only quality check (faster)
npm run quality:py
# Fix linting issues
npm run lint:py:fix # Python
npm run lint:js:fix # JavaScript
npm run format # Prettier
Project Structure
majoor-assetsmanager/
โโโ __init__.py # ComfyUI extension entrypoint
โโโ mjr_am_backend/ # Python backend (aiohttp routes + features)
โ โโโ routes/ # HTTP route handlers
โ โโโ features/ # Business logic (search, index, AI, etc.)
โ โโโ adapters/ # External system adapters (DB, FS, tools)
โโโ mjr_am_shared/ # Shared utilities (types, errors, logging)
โโโ js/ # Frontend source (Vue 3 + vanilla JS)
โ โโโ vue/ # Vue 3 components
โ โโโ features/ # Feature modules (viewer, grid, filters)
โ โโโ components/ # Reusable UI components
โ โโโ stores/ # Pinia state management
โ โโโ api/ # Backend HTTP client
โ โโโ app/ # Application bootstrap
โโโ js_dist/ # Built frontend (Vite output)
โโโ tests/ # Python tests (pytest)
โ โโโ backend/ # Backend unit/integration tests
โ โโโ features/ # Feature tests
โ โโโ security/ # Security tests
โโโ docs/ # Documentation
โ โโโ adr/ # Architecture Decision Records
โโโ scripts/ # Development/CI scripts
โโโ plugins/ # Plugin system examples
Key Architecture Principles
- Backend: Routes โ Features โ Adapters (clean separation of concerns)
- Frontend: Vue 3 owns major UI surfaces; imperative runtime bridges are explicit and limited
- State: Pinia stores own UI state; localStorage for persistence
- Security: Safe mode by default; write/delete operations require explicit opt-in
- Error Handling:
Result[T]pattern everywhere โ no exceptions bubble to UI
See <code>docs/ARCHITECTURE_MAP.md</code> and <code>docs/adr/</code> for detailed design decisions.
Coding Standards
Python
- Style: Follow Black formatting (line length: 100)
- Linting: Ruff with rules in
pyproject.toml - Type Checking: MyPy + Pyright (standard mode)
- Imports: Organized by Ruff
Irule (isort-compatible) - Error Handling: Use
Result[T]frommjr_am_shared/result.pyโ never raise to UI - Security: Never import
ComfyUI/server.py(see ADR-002)
Example:
from mjr_am_shared.result import Result, Ok, Err
def process_asset(asset_id: str) -> Result[dict]:
try:
# ... processing logic
return Ok({"status": "success", "id": asset_id})
except Exception as e:
return Err(f"Failed to process asset {asset_id}: {e}")
JavaScript/Vue
- Style: ESLint + Prettier (config in
eslint.config.mjs,.prettierrc) - Framework: Vue 3 Composition API with
<script setup> - State: Pinia stores for reactive state; no global mutable objects
- Testing: Vitest with Happy-DOM for component tests
- Naming: camelCase for variables/functions, PascalCase for Vue components
Example:
<script setup>
import { ref, computed } from 'vue'
import { usePanelStore } from '../../stores/usePanelStore.js'
const props = defineProps({
asset: { type: Object, required: true }
})
const panelStore = usePanelStore()
const isSelected = computed(() =>
panelStore.selectedIds.includes(props.asset.id)
)
</script>
Commits
- Use Conventional Commits format:
feat: add multi-pin support in Floating Viewer
fix: prevent timeout leak in Card.js
docs: update API reference with vector endpoints
- Keep commits focused and atomic (one logical change per commit)
- Write clear, descriptive messages (explain "why", not "what")
Testing
Backend (Python)
# Run all tests
pytest
# Run with coverage
pytest --cov=mjr_am_backend --cov-report=html
# Run specific test file
pytest tests/features/test_sampler_tracer_extra.py
# Run with timeout
pytest --timeout=30
Test organization:
tests/backend/โ Backend unit/integration teststests/features/โ Feature-specific tests (geninfo parser, metadata extraction)tests/security/โ Security tests (rate limiting, auth)tests/database/โ Database schema and migration tests
Frontend (JavaScript/Vue)
# Run all tests
npm run test:js
# Run with coverage
npm run test:js:coverage
# Watch mode
npm run test:js:watch
Test organization:
js/tests/โ Vitest test files- Test files named
*.vitest.mjs - Cover: composables, utilities, security-critical modules
Coverage Thresholds
| Metric | Python | JavaScript |
|---|---|---|
| Lines | 68% | 30% |
| Branches | Tracked (--cov-branch) | 20% |
| Functions | 70% | 30% |
Note: These are minimum thresholds. Security-critical code should have 100% coverage.
Submitting Contributions
Before You Start
- Check existing issues: See if someone else is working on your idea
- Open an issue: For features, bugs, or significant changes
- Discuss approach: For complex changes, propose design in issue comments
Pull Request Process
- Fork the repository and create your branch from
main - Make your changes following coding standards
- Add tests for new features or bug fixes
- Run quality gate:
npm run quality(must pass) - Update documentation if behavior changes
- Submit PR with clear description:
- What changed and why
- How to test the changes
- Any breaking changes or migration steps
PR Checklist
- [ ] Code follows project style guide
- [ ] Self-review completed
- [ ] Tests added/updated
- [ ] Quality gate passes (
npm run quality) - [ ] Documentation updated
- [ ] Commit messages are clear and conventional
- [ ] No merge conflicts with target branch
Review Process
Mainters will review your PR and may:
- Request changes (address feedback before merging)
- Approve and merge
- Ask for clarification on implementation choices
Git Workflow
Branch Strategy
main: Stable release branch (always deployable)- Feature branches:
feat/feature-nameorfix/bug-name - Release tags:
v2.4.5,v2.5.0, etc. (semantic versioning)
Typical Workflow
# 1. Create feature branch
git checkout main
git pull origin main
git checkout -b feat/add-new-filter
# 2. Make changes and commit
git add .
git commit -m "feat: add resolution filter to asset queries"
# 3. Push and create PR
git push origin feat/add-new-filter
Pre-Push Quality Gate
The pre-push hook automatically runs:
python scripts/run_changed_quality_gate.py
This checks changed files only (faster than full quality gate).
Code Quality Gate
The quality gate enforces:
Python Checks
- โ UTF-8 text files without BOM
- โ Ruff linting (changed files during migration window)
- โ MyPy type checking
- โ Bandit security linting
- โ pip-audit dependency auditing
- โ Xenon/Radon complexity checks
- โ Backend tests pass
Frontend Checks
- โ ESLint passes
- โ Prettier formatting
- โ Vitest tests pass
- โ npm audit passes
Running the Gate
# Full quality gate
npm run quality
# Python only (faster)
npm run quality:py
# Skip tests (for quick checks)
python scripts/run_quality_gate.py --skip-tests
# Tox environment
tox -e quality
Important: The quality gate must pass before PRs are merged.
Documentation
When to Update Docs
Update documentation when:
- Adding new features or changing behavior
- Modifying API endpoints or configuration
- Introducing breaking changes
- Fixing bugs that affect user-facing behavior
Documentation Standards
- Language: English (all user-facing docs)
- Format: Markdown (
.mdfiles) - Location:
docs/directory - Index: Update <code>docs/DOCUMENTATION_INDEX.md</code> for new docs
Types of Documentation
| Document Type | Purpose | Location |
|---|---|---|
| User Guides | How to use features | docs/*.md |
| API Reference | Backend endpoints | docs/API_REFERENCE.md |
| Architecture | Design decisions | docs/adr/ |
| Configuration | Settings & env vars | docs/SETTINGS_CONFIGURATION.md |
| Security | Threat model, hardening | docs/SECURITY_ENV_VARS.md, docs/THREAT_MODEL.md |
Architecture Decision Records (ADRs)
For significant architectural decisions:
- Create
docs/adr/NNNN-short-title.md - Follow ADR template (context, decision, consequences)
- Reference in
docs/adr/README.md
See existing ADRs for examples.
Getting Help
Resources
- Documentation: <code>docs/DOCUMENTATION_INDEX.md</code>
- API Reference: <code>docs/API_REFERENCE.md</code>
- Architecture: <code>docs/ARCHITECTURE_MAP.md</code>
- Changelog: <code>CHANGELOG.md</code>
Contact
- GitHub Issues: Report bugs or request features
- GitHub Discussions: Ask questions or share ideas
Common Questions
Q: How do I test changes locally?
# Rebuild frontend
npm run build
# Restart ComfyUI (extension loads from js_dist/)
# Test in browser, iterate quickly
Q: Can I add a new dependency?
- Runtime: Must be justified and approved (keep minimal)
- Dev: Easier to approve (testing, linting tools)
- Optional: AI/vector deps in
requirements-vector.txt
Q: What if the quality gate fails on CI but passes locally?
- Check Python/Node version differences
- Ensure all files are committed (CI checks entire repo)
- Run
npm run qualityfrom clean state
Recognition
Contributors are recognized in:
- GitHub contributors page
- Release notes (for significant contributions)
- This file (optional, add your name below)
Contributors
- MajoorWaldi โ Original creator and lead maintainer
- [Your name here โ submit a PR to add yourself!]
License
By contributing, you agree that your contributions will be licensed under the project's LICENSE.
Thank you for helping make Majoor Assets Manager better! ๐
docs/CONTRIBUTING.md
CUSTOM_NODES.md
Custom Nodes Reference
Majoor Assets Manager provides two ComfyUI nodes that embed generation timing metadata directly inside saved files. The asset manager's indexing pipeline then reads this metadata automatically.
Source: <code>nodes.py</code>
Majoor Save Image ๐พ
Node name: MajoorSaveImage Category: Majoor Display name: Majoor Save Image ๐พ
Drop-in replacement for ComfyUI's built-in SaveImage node. Saves PNG files to the standard output directory with generation_time_ms persisted in the PNG text chunks.
Inputs
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
images | IMAGE | โ | โ | The image batch to save |
filename_prefix | STRING | โ | Majoor | Filename prefix. Supports ComfyUI formatting placeholders (%date%, %batch_num%, etc.) |
generation_time_ms | INT | โ | -1 | Generation time in milliseconds. Set to -1 for automatic detection from the prompt lifecycle |
Hidden Inputs
| Input | Type | Description |
|---|---|---|
prompt | PROMPT | Full ComfyUI prompt graph (auto-provided) |
extra_pnginfo | EXTRA_PNGINFO | Additional PNG metadata (workflow, etc.) |
Metadata Written
Each saved PNG contains the following text chunks:
| Key | Content |
|---|---|
prompt | Full prompt graph as JSON |
workflow | Full workflow as JSON (via extra_pnginfo) |
generation_time_ms | Elapsed time since prompt start, in milliseconds |
CreationTime | ISO 8601 timestamp (YYYY-MM-DD HH:MM:SS) |
Output
Returns a UI result with the list of saved images (filename, subfolder, type) for ComfyUI's preview system.
Example Usage
- Connect any image output to the
imagesinput - Optionally set a custom
filename_prefix - Leave
generation_time_msat-1for automatic timing - The node saves PNGs to
ComfyUI/output/with full metadata
Majoor Save Video ๐ฌ
Node name: MajoorSaveVideo Category: Majoor Display name: Majoor Save Video ๐ฌ
Saves a VIDEO input or a batch of IMAGE frames as a video file. Uses PyAV for MP4 encoding (same approach as ComfyUI's native SaveVideo node) and Pillow for GIF/WebP.
Inputs
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
filename_prefix | STRING | โ | MajoorVideo | Filename prefix |
format | COMBO | โ | mp4 (h264) | Output format: mp4 (h264), gif, webp |
images | IMAGE | โ | โ | Batch of frames to encode as video |
video | VIDEO | โ | โ | A VIDEO input (from LoadVideo, CreateVideo, etc.) |
frame_rate | FLOAT | โ | 24.0 | Frames per second (1โ120). Ignored when video input carries its own frame rate |
loop_count | INT | โ | 0 | Loop count for GIF/WebP. 0 = infinite loop |
generation_time_ms | INT | โ | -1 | Generation time in ms. -1 = auto-detect |
audio | AUDIO | โ | โ | Audio track to mux into the MP4 container |
crf | INT | โ | 19 | Constant Rate Factor (0โ63). Lower = higher quality, larger file |
save_first_frame | BOOLEAN | โ | true | Save a PNG sidecar of the first frame with full metadata |
Hidden Inputs
| Input | Type | Description |
|---|---|---|
prompt | PROMPT | Full ComfyUI prompt graph |
extra_pnginfo | EXTRA_PNGINFO | Additional metadata (workflow, etc.) |
Input Resolution
At least one of images or video must be connected:
videoinput (priority): frame tensor, frame rate, and audio are extracted viavideo.get_components(). Theframe_ratewidget is ignored.imagesinput (fallback): frames are taken from the IMAGE batch, andframe_rate/audiowidgets are used.- Neither connected: the node produces no output.
Metadata Written
MP4 (h264)
Metadata is embedded directly in the MP4 container using PyAV with movflags=use_metadata_tags:
| Key | Content |
|---|---|
prompt | Full prompt graph as JSON |
workflow | Full workflow as JSON |
generation_time_ms | Elapsed time since prompt start, in milliseconds |
CreationTime | ISO 8601 timestamp |
These tags are readable by FFProbe (ffprobe -show_format_tags) and ExifTool.
GIF / WebP
Animated GIF and WebP formats do not support arbitrary metadata. When save_first_frame is enabled (default), a PNG sidecar is saved alongside the animation with full metadata in its text chunks. The asset manager indexes this sidecar automatically.
Video Encoding Details
- Codec: libx264, pixel format yuv420p
- Quality: controlled by
crf(default 19) - Audio: AAC codec, supports mono/stereo/5.1 layouts
- Frame rate: stored as an exact fraction for precision
Output
Returns a UI result with the saved video file(s) for ComfyUI's preview system.
Example Usage
From IMAGE batch:
- Connect a batch of images to
images - Set
formattomp4 (h264) - Adjust
frame_rateandcrfas needed - The node saves an MP4 to
ComfyUI/output/
From VIDEO input:
- Connect a VIDEO output to
video - The node uses the video's native frame rate and audio
- Set
formatandcrfto control encoding
Auto-Detection of generation_time_ms
Both nodes support automatic generation time measurement. When generation_time_ms is set to -1 (default), the node computes the elapsed time by reading the prompt start time from Majoor's runtime_activity module.
How It Works
- When ComfyUI starts executing a prompt,
runtime_activityrecordstime.monotonic()aslast_started_at - When the save node runs, it computes
(time.monotonic() - last_started_at) * 1000to get milliseconds - This value is written into the file metadata
Manual Override
Connect an INT value to generation_time_ms (any value โฅ 0) to override automatic detection. This is useful for:
- Measuring only part of a workflow
- Importing timing from external sources
- Testing and debugging
Extraction Pipeline
When assets are indexed by Majoor Assets Manager, generation_time_ms is extracted through the following chain:
PNG Files
read_png_text_chunks()reads thegeneration_time_mstext chunk โPNG:Generation_time_ms_merge_png_exif()merges it into the EXIF data dict_apply_rating_tags_and_generation_time()extracts the integer value โmetadata["generation_time_ms"]_best_effort_generation_time_ms()finds it at the top level and returns it for DB storage
MP4 Files
- FFProbe reads the container format tags (including
generation_time_ms) apply_video_ffprobe_fields()extracts it fromformat.tags.generation_time_ms_best_effort_generation_time_ms()finds it and returns it for DB storage
Fallback Behavior
When the Majoor save nodes are not used (e.g., standard SaveImage or third-party nodes), the asset manager falls back to:
- EXIF
DateTimeOriginal/CreateDateforgeneration_time(date-based, not duration) - Prompt graph analysis for workflow metadata
- No
generation_time_msis stored (column remains NULL)
docs/CUSTOM_NODES.md
DB_MAINTENANCE.md
Database Maintenance
Version: 2.4.5 Last Updated: April 7, 2026
Majoor Assets Manager stores its index in an SQLite database. By default this is at <output>/_mjr_index/assets.sqlite, but the index directory can be relocated to any local path โ useful when your output folder is on a network share. This document covers the maintenance tools available in the UI, the recovery procedures for corruption scenarios, and how to configure the index directory.
Recent highlights: Improved corruption detection, automatic health monitoring, and safer rebuild flows.
Configuring the Index Directory
By default the index lives next to your assets at <output>/_mjr_index/. You can move it to any local path without touching your asset files.
Why you might want to relocate
- Network drives (NAS/SMB/CIFS): SQLite relies on OS-level file locking. Many NAS/SMB implementations do not support these locks reliably, which can cause "database is locked" errors under concurrent access. Moving the index to a fast local SSD while keeping assets on the NAS eliminates this class of error entirely.
- Separate disk for performance: Keep assets on a large slow HDD and the index/DB on a fast SSD.
- Shared ComfyUI install, separate indexes: Multiple users on the same machine can each point to their own index directory.
How to configure
Option 1 โ UI (recommended, survives reinstalls):
- Open Settings โ Majoor Assets Manager โ Advanced and search for
pathif needed. - Locate Index Database Directory.
- Enter the full path to the desired directory (it will be created if it does not exist).
- Save. A toast confirms the change and reminds you to restart ComfyUI.
- Restart ComfyUI. A fresh scan runs on startup.
[Index directory override in Majoor settings]
The same section also exposes Generation Output Directory when you need to override the ComfyUI output folder independently from the database location.
The UI persists the path in a sidecar file .mjr_index_directory_override next to the extension root. This file takes precedence over the default on every startup.
Option 2 โ Environment variable:
Set either MJR_AM_INDEX_DIRECTORY or MAJOOR_INDEX_DIRECTORY in the environment that launches ComfyUI:
:: Windows โ batch launcher
set MJR_AM_INDEX_DIRECTORY=C:\mjr_index
python main.py
# Linux/macOS โ shell launcher
export MJR_AM_INDEX_DIRECTORY=/var/local/mjr_index
python main.py
Env var overrides the sidecar file and the default. Restart ComfyUI after changing it.
Priority order (highest wins):
MJR_AM_INDEX_DIRECTORYorMAJOOR_INDEX_DIRECTORYenvironment variable.mjr_index_directory_overridesidecar file (written by the UI)- Default:
<output_directory>/_mjr_index/
What the index contains
The index directory holds:
| File | Contents |
|---|---|
assets.sqlite | Main index: metadata, ratings, tags, FTS search index, scan journal |
assets.sqlite-wal / -shm | SQLite WAL and shared memory (transient, recreated automatically) |
vectors.sqlite | Optional AI embeddings (vector search) |
collections/ | Collection JSON files (preserved across Delete DB) |
custom_roots.json | Custom roots configuration (preserved across Delete DB) |
After changing the index directory
- Restart ComfyUI to apply the new path.
- A fresh scan starts automatically.
- Ratings, tags, and AI vectors from the old database are not automatically migrated.
- To migrate: stop ComfyUI, copy the
.sqlitefiles to the new directory, then restart. - The old
_mjr_indexdirectory is not deleted automatically.
Buttons in the Status Panel
Open the Assets Manager panel and expand the Index Status section. Two action buttons appear at the bottom:
| Button | Purpose |
|---|---|
| Reset index | Clears cached data inside the existing database and triggers a background rescan. Requires the database to be readable. |
| Delete DB | Force-deletes the database files from disk (bypassing all DB-dependent checks) and rebuilds from scratch. Works even when the database is corrupted. |
Reset Index
The Reset index button calls POST /mjr/am/scan/reset with flags to clear assets, metadata, FTS index, and scan journal, then triggers a full rescan.
If the database is corrupted, the reset will fail because security-preference queries cannot execute on a malformed database. When this happens:
- The status dot turns red.
- A toast appears: "Reset failed -- database is corrupted. Use the Delete DB button to force-delete and rebuild."
Delete DB (Emergency Recovery)
The Delete DB button calls POST /mjr/am/db/force-delete. It is designed to work even when the database is completely unreadable.
How it works
- CSRF check only -- no database-dependent security queries are run.
- Adapter reset (fast path) -- tries
db.areset()first. If this succeeds the database is wiped through the adapter and a rescan starts immediately. - Manual file deletion (fallback) -- if the adapter reset fails (typical for severe corruption):
- Calls
db.close()to release connections. - Runs
gc.collect()twice with a short delay to release Windows file handles. - Deletes
assets.sqlite,assets.sqlite-wal,assets.sqlite-shm, andassets.sqlite-journalwith up to 6 retries per file. - Re-initialization -- creates a fresh database with all tables, indexes, and triggers.
- Background rescan -- triggers a full non-incremental scan of the output directory.
Confirmation dialog
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
- Star ratings
- Custom tags
- Cached metadata (prompts, models, generation parameters)
- Scan journal / history
What is preserved
- Original image/video/audio files (never touched)
- Collections (stored as separate JSON files in
_mjr_index/collections/) - Custom roots configuration (
_mjr_index/custom_roots.json) - All ComfyUI settings (stored in
localStorage)
Automatic Corruption Detection
The status polling loop (every few seconds) checks health counters via GET /mjr/am/health/counters. If the response error contains keywords like "malformed", "corrupt", or "disk image":
- The status dot turns red.
- The status text shows "Database is corrupted" with a hint to use the Delete DB button.
- A one-time toast notification appears with the same guidance.
The toast fires only once per session to avoid spamming. It resets after a successful Delete DB operation.
Database Optimize
An additional endpoint POST /mjr/am/db/optimize runs PRAGMA optimize and ANALYZE on the database. This is useful after large scans or bulk deletes to keep query performance optimal. It is best-effort and never throws errors to the UI.
Manual Recovery
If the Delete DB button reports that files could not be deleted (another process holds a lock):
- Stop ComfyUI completely.
- Navigate to
<output>/_mjr_index/. - Delete
assets.sqliteand any sibling files (-wal,-shm,-journal). - Restart ComfyUI.
- The database will be recreated automatically on startup and a scan will populate it.
Related Files
| File | Role |
|---|---|
mjr_am_backend/routes/handlers/db_maintenance.py | /db/optimize and /db/force-delete endpoints |
mjr_am_backend/adapters/db/sqlite.py | DB adapter with malformed detection and online recovery |
js/features/status/StatusDot.js | Frontend status polling, corruption detection, Reset/Delete buttons |
js/api/client.js | forceDeleteDb() API call |
Environment Variables
| Variable | Default | Description |
|---|---|---|
MJR_AM_INDEX_DIRECTORY / MAJOOR_INDEX_DIRECTORY | <output>/_mjr_index/ | Override the index database directory |
MAJOOR_DB_TIMEOUT | 30.0 | SQLite busy timeout (seconds) |
MAJOOR_DB_MAX_CONNECTIONS | 8 | Maximum concurrent DB connections |
MAJOOR_DB_QUERY_TIMEOUT | 60.0 | Per-query timeout (seconds) |
docs/DB_MAINTENANCE.md
DEPENDENCY_POLICY.md
Dependency Policy
Date: 2026-04-09
Goal
Keep one explicit dependency contract per use case and avoid drift between packaging metadata, manual installs, and CI tooling.
Source Of Truth
requirements.txtis the primary dependency source of truth for the project.requirements.txtis also the default runtime install contract for the extension.requirements-vector.txtextends runtime with optional AI/vector dependencies.requirements-dev.txtis the contributor tooling layer for tests, linting, typing, and security checks.pyproject.tomlmirrors published metadata and optional dependency groups for packaging, but does not replacerequirements.txtas the source of truth used by users and CI.
Dependency Roles
Runtime
Put a dependency in requirements.txt only if the extension needs it for normal backend operation.
Examples:
aiohttpaiosqlitepillowsend2trash
Optional AI / Vector
Put a dependency in requirements-vector.txt only if it is required for optional AI, vector search, captioning, clustering, or related enrichment features.
This file must include -r requirements.txt so it always layers on top of the runtime baseline.
Dev / Contributor Tooling
Put a dependency in requirements-dev.txt only if it is used to develop, test, lint, type-check, or audit the project locally or in CI.
Examples:
pytestruffmypybanditpip-audit
Dev tools must not be added to requirements.txt just because CI uses them.
Update Rules
When adding or changing a dependency:
- Decide whether it is
runtime,vector, ordev. - Update
requirements.txtfirst when the dependency affects the main project baseline. - Update the matching extension file when the dependency is optional or contributor-only.
- Mirror the change in
pyproject.tomlwhen it affects published metadata. - Update docs if the install story or contributor workflow changed.
- Run the relevant tests or quality checks for the impacted area.
CI And Quality Gate Expectations
- Runtime security auditing targets
requirements.txt. - Contributor tooling is installed from
requirements-dev.txtfor local development and quality workflows. - Optional AI/vector installs remain opt-in and must not become implicit in the default setup.
Non-Goals
- No hidden dependency source in ad hoc scripts.
- No โinstall everything alwaysโ default for end users.
- No divergence between README instructions and the actual files used by installs.
Quick Commands
# Runtime only
pip install -r requirements.txt
# Runtime + optional AI/vector features
pip install -r requirements.txt -r requirements-vector.txt
# Contributor tooling
pip install -r requirements-dev.txtdocs/DEPENDENCY_POLICY.md
DRAG_DROP.md
Majoor Assets Manager - Drag & Drop Guide
Version: 2.4.5 Last Updated: April 5, 2026
Overview
The Majoor Assets Manager provides seamless drag and drop functionality that integrates with both ComfyUI and your operating system. This guide covers all aspects of drag and drop operations within the extension.
Recent highlights: Audio file staging support, improved multi-select ZIP creation, and better node compatibility detection.
Drag to ComfyUI Canvas
Basic Drag Operation
- Select one or more assets in the Assets Manager
- Click and hold on an asset card
- Drag the asset onto the ComfyUI canvas
- Release the mouse button to drop the asset
Single Asset Drag
- Drag any single asset to the canvas
- The asset path is automatically injected into compatible nodes
- Compatible nodes include LoadImage, LoadLatent, and similar input nodes
- The asset is staged for immediate use in your workflow
Multiple Asset Drag
- Select multiple assets using Ctrl/Cmd+click or Shift+click
- Drag any selected asset to the canvas
- Multiple file paths are handled appropriately
- Some nodes support multiple inputs, others will use the first path
Node Compatibility
The system intelligently identifies compatible nodes:
Image Input Nodes
- LoadImage: Accepts image file paths
- LoadImageMask: For mask images
- PreviewImage: For direct preview
- LoadLatent: For latent files
Video Input Nodes
- LoadVideo: For video file paths
- VideoLoad: For various video formats
Workflow Input Nodes
- LoadWorkflow: For workflow files
- ImportWorkflow: For external workflow files
Staging Mechanism
- Assets are staged temporarily when dragged to canvas
- Paths are injected into appropriate input fields
- No permanent changes to workflow until execution
- Allows for experimentation without committing to changes
Drag to Operating System
Single File Drag
- Select an asset in the Assets Manager
- Click and drag the asset outside the browser window
- Drop onto your file explorer, desktop, or another application
- The original file is transferred to the destination
Multiple File Drag
- Select multiple assets using Ctrl/Cmd+click or Shift+click
- Drag any selected asset outside the browser window
- A ZIP file is automatically created containing all selected assets
- Drop the ZIP file onto your destination
ZIP Creation Process
- ZIP files are created on-demand when dragging multiple items
- Files are streamed directly from disk (no temporary copies)
- ZIPs are flat (no directory structure maintained)
- ZIP files are automatically cleaned up after transfer
Protocol Support
The system supports multiple drag protocols:
DownloadURL Protocol
- Used for single file transfers
- Direct file download to destination
- Maintains original filename and extension
text/uri-list Protocol
- Used for multiple file transfers
- Properly formatted URI list for operating system
- Compatible with most file managers and applications
Drag Operations
Starting a Drag
- Click and hold on any asset card for a moment
- A visual indicator appears showing the drag is active
- The cursor changes to indicate drag operation
- Selection remains active during drag
Drag Indicators
- Visual Outline: Selected assets show a highlighted border
- Cursor Change: Cursor changes to indicate drag capability
- Preview: Small preview of dragged asset(s) follows cursor
- Count Display: For multiple selections, shows number of items
Drag Targets
The system recognizes different drag targets:
Valid Targets
- ComfyUI canvas area
- Compatible input nodes
- Browser address bar (downloads)
- File explorer windows
- Desktop
- Email attachments
- Other applications accepting files
Invalid Targets
- Text input fields (unless specifically designed for file drops)
- Non-compatible areas of the interface
- Applications that don't accept the file type
File Transfer Mechanisms
Direct File Transfer
- Single files are transferred directly
- Maintains original file properties
- Preserves embedded metadata
- No quality loss during transfer
Batch ZIP Transfer
- Multiple files are packaged into a ZIP archive
- ZIP created dynamically during drag operation
- No temporary files written to disk
- Automatic cleanup after transfer completion
Streaming Transfer
- Files are streamed directly from disk
- No intermediate copies created
- Memory efficient for large files
- Maintains file integrity during transfer
Filename Collision Indicator
Understanding the Indicator
- When multiple assets share the same filename, the extension badge shows
EXT+ - For example:
image.pngbecomesPNG+when duplicates exist - Helps identify potential conflicts during drag operations
Handling Collisions
- Collisions don't prevent drag operations
- ZIP files handle duplicate names automatically
- Individual file drops may require manual renaming
- Consider renaming files to avoid confusion
Drag Performance
Optimizations
- Lightweight preview during drag
- Asynchronous file operations
- Memory-efficient streaming
- Responsive interface during drag operations
Performance Considerations
- Large files may take time to initiate transfer
- Multiple large files in ZIP may take longer to create
- Network drives may affect performance
- System resources impact drag responsiveness
Advanced Drag Features
Drag Preview
- Live preview of dragged assets
- Shows thumbnail of the primary dragged item
- Displays count for multiple selections
- Updates in real-time during drag
Drag Validation
- Validates target compatibility before allowing drop
- Provides visual feedback for valid/invalid targets
- Prevents invalid operations
- Shows appropriate cursors for different states
Drag Cancellation
- Cancel drag by releasing over invalid target
- Escape key cancels active drag operation
- Clicking elsewhere cancels drag
- Drag automatically cancels if window loses focus
Integration with Other Features
Selection Integration
- Drag operations work with current selection
- Multiple selections enable batch operations
- Selection filters apply to drag operations
- Search results can be dragged directly
Collection Integration
- Assets from collections can be dragged normally
- Entire collections can be exported via drag
- Collection organization preserved in transfers
- Cross-collection drags work seamlessly
Rating/Tag Integration
- Rated and tagged assets maintain metadata during drag
- Tags and ratings preserved in file transfers (when supported)
- Filtered views can be dragged directly
- Quality indicators visible during drag
Troubleshooting
Common Drag Issues
Drag Not Initiating
- Ensure you're clicking and holding long enough
- Check that the asset is properly selected
- Verify no JavaScript errors in console
- Try refreshing the Assets Manager
Drag Target Not Recognized
- Ensure target application accepts file drops
- Check if target area is actually droppable
- Verify file type compatibility with target
- Try dragging to a different application
ZIP Creation Failure
- Check available disk space
- Verify write permissions to temporary directory
- Ensure no antivirus interference
- Try dragging fewer files at once
File Transfer Problems
- Large files may timeout during transfer
- Network drives may cause delays
- Antivirus software may interfere
- Check file permissions at destination
Browser-Specific Issues
Chrome/Chromium
- May require specific security settings for file downloads
- Extensions might interfere with drag operations
- Incognito mode may behave differently
Firefox
- Security settings may restrict file operations
- Enhanced Tracking Protection might interfere
- Check privacy settings for file handling
Safari
- May require additional permissions for file access
- Security restrictions could limit functionality
- Check website permissions for file handling
Operating System Issues
Windows
- UAC (User Account Control) may interfere
- Antivirus software may block operations
- File permissions may restrict transfers
macOS
- Gatekeeper may restrict file operations
- Privacy settings may limit access
- Sandboxing could affect functionality
Linux
- File permissions are strictly enforced
- Desktop environment may affect drag behavior
- Security modules might restrict operations
Security Considerations
File Access Security
- Drag operations respect file system permissions
- No files can be accessed that the user can't read
- Temporary files are created with restricted permissions
- ZIP files are cleaned up automatically
Cross-Origin Security
- Drag operations confined to same origin
- No cross-site data leakage possible
- File system access limited to allowed directories
- Path validation prevents directory traversal
Malicious File Protection
- No automatic execution of dragged files
- Files transferred in their original form
- No modification during drag operations
- Integrity preserved throughout transfer
Performance Optimization
Large File Handling
- Stream large files instead of loading entirely
- Progress indicators for long operations
- Asynchronous operations to maintain interface responsiveness
- Memory management for multiple large files
Multiple File Optimization
- Efficient ZIP creation algorithms
- Parallel processing where possible
- Memory buffering for optimal performance
- Cleanup operations scheduled appropriately
Interface Responsiveness
- Drag operations don't block UI thread
- Smooth animations during drag
- Immediate feedback for user actions
- Graceful degradation for slower systems
Best Practices
Efficient Drag Operations
- Use multiple selection for batch operations
- Organize assets in collections for easier access
- Use search and filters to find assets quickly
- Verify target compatibility before initiating drag
File Organization
- Maintain consistent naming conventions
- Use collections to group related assets
- Apply tags for easy retrieval
- Regular cleanup of unused assets
Workflow Integration
- Use drag operations to accelerate workflow creation
- Stage frequently used assets in collections
- Leverage drag to canvas for rapid prototyping
- Combine with other Assets Manager features
Performance Tips
- Close unnecessary browser tabs during large operations
- Ensure sufficient system resources
- Use wired connections for network drives
- Regular maintenance of index database
Advanced Usage
Scripted Operations
- Combine drag operations with automation scripts
- Use collections for automated workflows
- Integrate with external tools via file operations
- Batch processing using drag interfaces
Power User Techniques
- Master keyboard shortcuts with drag operations
- Use multiple monitors for efficient transfers
- Leverage search and filters for precise selection
- Combine collections with drag for complex operations
Drag & Drop Guide Version: 1.0 Last Updated: April 5, 2026
docs/DRAG_DROP.md
FLOATING_VIEWER_WORKFLOW_SIDEBAR.md
Floating Viewer โ Workflow Sidebar & Run Implementation
Purpose: The Floating Viewer (MFV) includes a lightweight sidebar panel that allows
modifying widgets of selected nodes on the canvas, plus a Run button,
without duplicating or competing with ComfyUI's native "Workflow Overview" panel.
1. Design Principles โ "Delegate, Don't Duplicate"
| Rule | Detail |
|---|---|
| Read-only data access | Reads app.graph, app.canvas.selected_nodes โ no parallel model invented. |
| Write via native widget | Writes via widget.value = x ; widget.callback?.(x) โ ComfyUI engine handles propagation. |
| Execution via endpoint | Run calls POST /prompt with payload from app.graphToPrompt() โ queue is not reimplemented. |
| No own persistence | No store, no cache โ each opening reads live graph state. |
2. Module Architecture
js/features/viewer/
โโโ FloatingViewer.js โ existing (modified _buildToolbar)
โโโ floatingViewerManager.js โ existing (added selection bindings)
โ
โโโ workflowSidebar/ โ NEW submodule
โ โโโ WorkflowSidebar.js โ Main component (DOM panel)
โ โโโ NodeWidgetRenderer.js โ Renders node widgets
โ โโโ widgetAdapters.js โ Type โ HTML input adapters
โ โโโ sidebarRunButton.js โ Run button (queue prompt)
2.1 Dependencies
FloatingViewer
โโโบ WorkflowSidebar (created in _buildToolbar, injected into MFV DOM)
โโโบ NodeWidgetRenderer (instantiated 1ร per displayed node)
โ โโโบ widgetAdapters (pure functions, no state)
โโโบ sidebarRunButton (autonomous button, calls ComfyUI API)
None of these files import from comfyui-frontend directly. Everything goes through the existing bridge: js/app/comfyApiBridge.js.
3. Module by Module
3.1 WorkflowSidebar.js โ Sidebar Panel
Role: Sliding container (slide-in) that opens on the right side of the Floating Viewer.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Majoor Viewer Lite [โ] [โถ Run] [โ] โ โ header + toolbar
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ Workflow Sidebar โ
โ Viewer Content โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ (image/video) โ โ KSampler [๐] โ โ
โ โ โ seed [156680208700286] โ โ
โ โ โ steps [20] โ โ
โ โ โ cfg [8.0] โ โ
โ โ โ sampler [euler โพ] โ โ
โ โ โ scheduler [normal โพ] โ โ
โ โ โ denoise [1.00] โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ โ CLIP Text Encode [๐] โ โ
โ โ โ text [beautiful sce...] โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Public API
class WorkflowSidebar {
constructor({ hostEl, onClose })
show() // slide-in, reads current selection
hide() // slide-out
toggle()
refresh() // re-reads app.canvas.selected_nodes and re-renders
get isVisible()
destroy()
}
How to read selected nodes
The pattern already exists in NodeStreamController.js:
import { getComfyApp } from "../../../app/comfyApiBridge.js";
function getSelectedNodes() {
const app = getComfyApp();
const selected = app?.canvas?.selected_nodes
?? app?.canvas?.selectedNodes
?? null;
if (!selected) return [];
if (Array.isArray(selected)) return selected.filter(Boolean);
if (selected instanceof Map) return Array.from(selected.values());
if (typeof selected === "object") return Object.values(selected);
return [];
}
How to listen to selection changes
The pattern exists in LiveStreamTracker.js:
const canvas = app.canvas;
const origSelected = canvas.onNodeSelected;
const origSelChange = canvas.onSelectionChange;
canvas.onNodeSelected = function (node) {
origSelected?.call(this, node);
sidebar.refresh(); // โ our hook
};
canvas.onSelectionChange = function (selectedNodes) {
origSelChange?.call(this, selectedNodes);
sidebar.refresh(); // โ our hook
};
Important: These hooks must be properly attached/detached when opening/closing
the sidebar to avoid leaks. We use the same chained pattern as LiveStreamTracker.
3.2 NodeWidgetRenderer.js โ Node Rendering
Role: For a given LGraphNode, iterates over node.widgets and generates a corresponding HTML form.
Information available on a ComfyUI widget
node.widgets.forEach(widget => {
widget.name // "seed", "steps", "cfg", "sampler_name" โฆ
widget.type // "number", "combo", "text", "toggle", "IMAGEUPLOAD" โฆ
widget.value // current value
widget.options // { min, max, step, values: [...] } (for combo/number)
widget.callback // function(value) โ to call after write
});
Generated DOM structure
<section class="mjr-ws-node" data-node-id="3">
<div class="mjr-ws-node-header">
<span class="mjr-ws-node-title">KSampler</span>
<button class="mjr-icon-btn mjr-ws-locate" title="Locate on canvas">
<i class="pi pi-map-marker"></i>
</button>
</div>
<div class="mjr-ws-node-body">
<!-- widgets rendered by widgetAdapters -->
</div>
</section>
Locating a node on the canvas
function locateNode(node) {
const app = getComfyApp();
const canvas = app?.canvas;
if (!canvas || !node) return;
// Center view on node
canvas.centerOnNode?.(node);
// Fallback LiteGraph
if (!canvas.centerOnNode && canvas.ds) {
canvas.ds.offset[0] = -node.pos[0] - node.size[0] / 2 + canvas.canvas.width / 2;
canvas.ds.offset[1] = -node.pos[1] - node.size[1] / 2 + canvas.canvas.height / 2;
canvas.setDirty(true, true);
}
}
3.3 widgetAdapters.js โ Widget โ HTML Input Conversion
File of pure functions, no state. Each adapter returns an HTMLElement.
widget.type | HTML Input | Notes |
|---|---|---|
"number" | <input type="number" min max step> | Uses widget.options.{min,max,step} |
"combo" | <select> with <option> | Uses widget.options.values |
"text" | <textarea> | Auto-resize |
"toggle" | <input type="checkbox"> | |
"IMAGEUPLOAD" | (ignored) | Too complex, not handled |
| other / unknown | <input type="text" readonly> | Display only |
Writing to a ComfyUI widget
function writeWidgetValue(widget, newValue) {
if (widget.type === "number") {
const n = Number(newValue);
if (Number.isNaN(n)) return false;
const { min = -Infinity, max = Infinity } = widget.options ?? {};
widget.value = Math.min(max, Math.max(min, n));
} else {
widget.value = newValue;
}
// Notify ComfyUI that widget changed
widget.callback?.(widget.value);
// Mark canvas dirty for visual refresh
const app = getComfyApp();
app?.canvas?.setDirty?.(true, true);
return true;
}
This pattern is identical to the one already used in DragDrop.js for dropping
media on nodes โ nothing is reinvented.
3.4 sidebarRunButton.js โ Run Button
Role: Single button that triggers POST /prompt using ComfyUI API.
Option A โ Via app object (recommended)
import { getComfyApp } from "../../../app/comfyApiBridge.js";
async function queueCurrentPrompt() {
const app = getComfyApp();
if (!app) return { ok: false, error: "ComfyUI app not ready" };
// graphToPrompt() serializes current workflow into executable payload
const promptData = await app.graphToPrompt();
if (!promptData?.output) return { ok: false, error: "Empty prompt" };
// Trigger execution
// Method 1: via app.queuePrompt (if available)
if (typeof app.queuePrompt === "function") {
const res = await app.queuePrompt(0); // 0 = insert at end of queue
return { ok: true, data: res };
}
// Method 2: via api.queuePrompt
const api = app.api;
if (api && typeof api.queuePrompt === "function") {
const res = await api.queuePrompt(0, promptData);
return { ok: true, data: res };
}
// Method 3: direct HTTP fallback
const resp = await fetch("/prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: promptData.output,
extra_data: { extra_pnginfo: { workflow: promptData.workflow } },
}),
});
return { ok: resp.ok, data: await resp.json() };
}
Rendering
<!-- In toolbar, next to โ settings button -->
<button class="mjr-icon-btn mjr-mfv-run-btn" title="Queue Prompt (Run)">
<i class="pi pi-play"></i>
</button>
Visual state:
- Idle โ green
pi-playicon - Running โ
pi-spin pi-spinnericon + disabled - Error โ red flash 1s then back to idle
4. Integration in FloatingViewer
4.1 Modification of _buildToolbar()
Add 2 buttons to existing toolbar:
// After existing capture/download button:
// --- Separator ---
const sep2 = document.createElement("div");
sep2.className = "mjr-mfv-toolbar-sep";
bar.appendChild(sep2);
// --- Settings Button (opens/closes sidebar) ---
const settingsBtn = document.createElement("button");
settingsBtn.type = "button";
settingsBtn.className = "mjr-icon-btn mjr-mfv-settings-btn";
settingsBtn.title = "Node Parameters";
const settingsIcon = document.createElement("i");
settingsIcon.className = "pi pi-cog"; // โ PrimeIcons icon
settingsBtn.appendChild(settingsIcon);
settingsBtn.addEventListener("click", () => this._sidebar?.toggle());
bar.appendChild(settingsBtn);
// --- Run Button ---
const runBtn = createRunButton(); // from sidebarRunButton.js
bar.appendChild(runBtn);
4.2 Modification of render()
render() {
const el = this._el;
el.appendChild(this._buildHeader());
el.appendChild(this._buildToolbar());
el.appendChild(this._contentEl);
// NEW: sidebar, mounted once, hidden by default
this._sidebar = new WorkflowSidebar({
hostEl: el,
onClose: () => this._updateSettingsBtnState(false),
});
el.appendChild(this._sidebar.el);
return el;
}
4.3 Selection Hook in floatingViewerManager.js
// In open():
_bindNodeSelectionListener();
// In close():
_unbindNodeSelectionListener();
5. CSS โ Added Classes
/* โโ Sidebar container โโ */
.mjr-ws-sidebar {
position: absolute;
top: 0;
right: 0;
width: 280px;
height: 100%;
background: var(--comfy-menu-bg, #1a1a1a);
border-left: 1px solid var(--border-color, #333);
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.2s ease;
z-index: 1;
}
.mjr-ws-sidebar.open {
transform: translateX(0);
}
/* โโ Node sections โโ */
.mjr-ws-node { padding: 8px 12px; border-bottom: 1px solid var(--border-color, #333); }
.mjr-ws-node-header { display: flex; align-items: center; justify-content: space-between; }
.mjr-ws-node-title { font-weight: 600; font-size: 13px; color: var(--input-text, #ddd); }
.mjr-ws-node-body { display: flex; flex-direction: column; gap: 6px; padding-top: 6px; }
/* โโ Widget rows โโ */
.mjr-ws-widget-row { display: flex; align-items: center; gap: 8px; }
.mjr-ws-widget-label { flex: 0 0 90px; font-size: 12px; color: var(--descrip-text, #999); }
.mjr-ws-widget-input { flex: 1; }
.mjr-ws-widget-input input,
.mjr-ws-widget-input select,
.mjr-ws-widget-input textarea {
width: 100%;
background: var(--comfy-input-bg, #222);
color: var(--input-text, #ddd);
border: 1px solid var(--border-color, #444);
border-radius: 4px;
padding: 4px 6px;
font-size: 12px;
}
/* โโ Run button โโ */
.mjr-mfv-run-btn i { color: #4caf50; }
.mjr-mfv-run-btn.running i { color: var(--descrip-text, #999); }
.mjr-mfv-run-btn.error i { color: #f44336; }
/* โโ Settings button active state โโ */
.mjr-mfv-settings-btn.active i { color: var(--p-primary-color, #4fc3f7); }
6. Events and Data Flow
โโโโโโโโโโโโโโโ click โ โโโโโโโโโโโโโโโโโโโโโ
โ Toolbar โ โโโโโโโโโโโโโโโโบ โ WorkflowSidebar โ
โ settingsBtn โ โ .toggle() โ
โโโโโโโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโโโ
โ show()
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ getSelectedNodes() โ
โ (comfyApiBridge) โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโโโโผโโโโโโโโโโโโ
โ NodeWidgetRenderer โ
โ for each node โ
โ โ widgetAdapters โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ user edits input
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ writeWidgetValue() โ
โ widget.value = x โ
โ widget.callback(x) โ
โ canvas.setDirty() โ
โโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโ click โถ โโโโโโโโโโโโโโโโโโโโโ
โ Toolbar โ โโโโโโโโโโโโโโโบโ queueCurrentPromptโ
โ runBtn โ โ app.graphToPrompt()โ
โ โ โ POST /prompt โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
7. What We Do NOT Do
| Prohibited | Reason |
|---|---|
| Recreate a full node editor | We're not an IDE โ just a quick adjustment panel |
| Handle node add/delete | ComfyUI canvas manages that |
| Duplicate Workflow Overview | Our sidebar is a contextual shortcut (selected nodes only) |
| Intercept execution queue | We POST and that's it โ no own progress tracking |
| Store widget state | We read/write live graph โ no local copy |
| Add ComfyUI frontend dependencies | Everything goes through comfyApiBridge.js |
8. Implementation Checklist
- [x] Create
js/features/viewer/workflowSidebar/widgetAdapters.js - [x] Create
js/features/viewer/workflowSidebar/NodeWidgetRenderer.js - [x] Create
js/features/viewer/workflowSidebar/WorkflowSidebar.js - [x] Create
js/features/viewer/workflowSidebar/sidebarRunButton.js - [x] Modify
FloatingViewer.jsโ add โ + โถ buttons in_buildToolbar() - [x] Modify
FloatingViewer.jsโ instantiateWorkflowSidebarinrender() - [x] Modify
floatingViewerManager.jsโonNodeSelected/onSelectionChangehooks - [x] Add CSS classes in
theme-comfy.css - [x] Tests:
workflowSidebar.vitest.mjs(DOM/adapters),sidebarRunButton.vitest.mjs(mock fetch)
Status: โ Complete โ All items implemented and tested.
9. Final MFV Toolbar Visual Summary
[ Mode ][ Pin โพ]โ[ Live ][ Preview ][ NodeStream ]โ[ GenInfo ][ PopOut ][ Download ]โ[ โ Settings ][ โถ Run ]โ[ โ ]
โโโ NEW โโโ
10. Sidebar Position Setting
Users can customize sidebar placement via Settings:
- Right (default) โ sidebar slides in from right
- Left โ sidebar slides in from left
- Bottom โ sidebar slides up from bottom
The setting applies immediately without reload and persists across sessions.
docs/FLOATING_VIEWER_WORKFLOW_SIDEBAR.md
FRONTEND_IMPERATIVE_DESIGN.md
Frontend: Imperative vs. Vue ownership
Last updated: April 10, 2026
Summary
The frontend uses Vue 3 + Pinia for all major UI surfaces. Certain runtime modules remain imperative by design because they orchestrate cross-cutting behavior that predates or spans multiple Vue component trees.
Imperative by design
These modules are intentionally kept outside Vue's reactivity model:
| Module | Reason |
|---|---|
js/app/bootstrap.js | App init sequence, ComfyUI lifecycle hooks |
js/app/comfyApiBridge.js | Bridge to ComfyUI's API (external dependency, not ours) |
js/app/events.js | Central event bus for cross-feature communication |
js/app/metrics.js | Performance telemetry (no UI surface) |
js/features/panel/panelRuntime.js | Panel lifecycle orchestration: init, teardown, events |
js/features/viewer/* | Viewer runtime: canvas, zoom, playback โ too stateful and performance-sensitive for Vue reactivity |
js/features/dnd/* | Drag-and-drop runtime: low-level DOM events, interop with ComfyUI DnD |
js/features/runtime/* | ComfyUI runtime integration (generation flow, progress, queue) |
Provisionally imperative (may migrate later)
| Module | Notes |
|---|---|
js/stores/panelStateBridge.js | Bridge between Vue panel store and legacy non-Vue consumers. Remove when all consumers are Vue. |
js/app/dialogs.js, js/app/toast.js | UI utilities currently used by both Vue and imperative code. Can move to Vue composables once all callers migrate. |
Fully Vue-owned
All surfaces under js/vue/components/ and js/vue/composables/ are fully Vue-owned:
- Grid, sidebar, feed, settings panels, context menus
- All Pinia stores under
js/stores/
Decision criteria
A module should stay imperative when:
- It manages a lifecycle spanning multiple Vue app mounts/unmounts
- It bridges to external APIs not under our control (ComfyUI)
- It requires low-level DOM/canvas control where Vue overhead is measurable
- Its mutation patterns don't map cleanly to reactive state (e.g. streaming events, Web Workers)
docs/FRONTEND_IMPERATIVE_DESIGN.md
FRONTEND_LIFECYCLE_CONVENTIONS.md
Frontend Lifecycle Conventions
Last updated: April 10, 2026
Vue component lifecycle
- Vue apps are created via
js/vue/createVueApp.jswith Pinia store injection. - Components use
onMounted/onUnmountedfor DOM setup/teardown. - Event listeners added in
onMountedmust be removed inonUnmounted. - Pinia stores are the single source of truth for UI state.
Imperative runtime lifecycle
panelRuntime.jsmanages the top-level panel init/destroy cycle.- Feature modules (viewer, DnD, grid) expose
init()/destroy()orstart()/stop(). - The bootstrap sequence is:
bootstrap.jsinitializes the app contextpanelRuntime.jssets up the panel shell- Vue apps mount into panel slots
- Feature runtimes activate via events or direct calls
Teardown rules
- Every
addEventListenermust have a matchingremoveEventListeneron destroy. - Every
setInterval/setTimeoutmust be cleared on destroy. - Pinia store subscriptions are auto-cleaned when the Vue app unmounts.
- Custom event buses (
events.js) listeners must be unsubscribed explicitly.
Bridge conventions
comfyApiBridge.js: wraps ComfyUI API calls. Never call ComfyUI API directly from Vue components.panelStateBridge.js: syncs Pinia panel store with legacy imperative consumers. Will be removed when all consumers migrate to Vue.- New bridges should be avoided. Prefer Pinia stores or composables for new features.
docs/FRONTEND_LIFECYCLE_CONVENTIONS.md
GRAPH_MAP.md
Graph Map Guide
Last Updated: May 10, 2026
Overview
Graph Map is the workflow-aware navigation view inside the Majoor Floating Viewer.
It is designed for one fast job: keep the saved workflow readable while you inspect the asset that came out of it.
Graph Map combines three things in one place:
- a large workflow map with readable node labels
- a selected-node detail panel with copy/import actions
- a small asset preview that keeps the current media visible while the panel refreshes
Why It Exists
When a workflow gets large, the usual minimap is useful for shape but not for understanding.
Graph Map adds the missing context:
- real node labels instead of anonymous boxes
- subgraph names that stay human-readable instead of raw UUID or hash identifiers
- a direct bridge between the saved workflow and the asset currently shown in the viewer
This is especially useful for video workflows, larger prompt-routing graphs, and pipelines that rely on nested subgraphs.
How To Open It
- Open an asset in the Majoor Floating Viewer.
- Switch the viewer to Graph Map mode.
- If you also open the Node Parameters sidebar, use the Graph Map tab to keep the small preview and node details visible together.
Graph Map only appears when the selected asset contains readable workflow data.
What You See
1. Workflow overview
The large map shows the saved workflow structure with node labels, links, and the current selection highlight.
- loader, sampler, and output nodes remain easy to spot
- subgraphs are labeled with their real names when that metadata is available
- the selected node is outlined so you can keep your position while panning or zooming
2. Selected node detail panel
When you click a node in Graph Map, the detail panel shows the node title, its type, and the most useful simple parameters.
Available actions include:
- Copy node
- Import node
- Import workflow
- Transfer params to selected canvas node
You can also click individual parameter rows to copy a single value.
3. Asset preview
The small preview keeps the active asset visible while you browse the workflow.
For video assets, the preview is meant to stay stable while Graph Map live refreshes, so it does not constantly restart or flicker during normal sidebar updates.
Navigation And Interaction
Canvas controls
- Click a node to select it
- Mouse wheel to zoom in or out
- Click-drag to pan around the map
Node detail workflow
- select the node that generated or refined the output you care about
- inspect the visible parameters
- copy a single value or the full node JSON
- import the node or the whole workflow back into the current canvas when needed
Subgraph Labels
Graph Map now prefers readable subgraph names over opaque identifiers.
In practice that means:
- named subgraphs are shown with their visible workflow name
- raw UUID or long hash node types are no longer the main label when a better name exists
- the same readable naming is reused in the map and the node detail panel
This matters most in larger reusable workflow blocks such as detailers, post-process passes, regional prompt groups, or custom routed subgraphs.
Best Use Cases
- understand which block produced the current result
- jump quickly between sampler, prompt, and save/output nodes
- inspect a subgraph without opening the full ComfyUI canvas layout
- compare outputs while keeping the originating node parameters close by
- reuse values from a saved asset back into the current canvas node
Limitations
- Graph Map depends on workflow metadata being present in the asset.
- If an asset has no embedded workflow, the panel cannot reconstruct the graph.
- The detail panel intentionally focuses on simple, copyable values; not every complex widget or linked input is shown as a plain field.
Related Docs
docs/GRAPH_MAP.md
GRID_OPTIMIZATION_CHECKLIST.md
Majoor Assets Manager - Grid Optimization Checklist
Objectif: rendre le panel grid stable, previsible et rapide, surtout pendant la pagination, les updates backend, les changements de scope et la selection.
1. Stabiliser le contrat de chargement
Etat: fait pour le contrat critique.
- [x]
appendNextPagene doit pas vider la grille pendant la pagination. - [x] La pagination adaptative doit continuer a charger quand une page recue ajoute 0 carte visible a cause du dedupe/filtrage.
- [x] Les updates backend ne doivent pas forcer un reload si la grille affiche deja des cartes visibles.
- [x] Separer explicitement les chemins
reload,appendNextPage,refreshHead,upsertRealtime. - [x] Garantir par tests que
appendNextPagene remplace pas toute la grille. - [x] Garantir que
appendNextPagene touche jamais au scroll. - [x] Garantir que
appendNextPagene purge jamais la selection. - [x] Documenter les invariants de chargement dans le code.
2. Unifier l'etat grid
Etat: fait pour les nouveaux chemins, legacy garde en compatibilite.
- [x] Definir une source canonique lisible pour
assets. - [x] Definir une source canonique lisible pour
pagination. - [x] Definir une source canonique lisible pour
selection. - [x] Definir une source canonique lisible pour
viewport. - [x] Definir une source canonique lisible pour
context. - [x] Reduire completement le role des
datasetDOM a une couche de compatibilite legacy. - [x] Encapsuler les methodes
_mjr*du container derriere une API unique (_mjrGridApi). - [x] Ajouter des tests qui verifient l'etat canonique expose par le loader.
3. Proteger scroll et selection
Etat: fait pour pagination et reload partiel.
- [x] Eviter les reloads automatiques qui cassent scroll/selection quand la grille a deja des cartes visibles.
- [x] Sauver/restaurer un anchor visuel, pas seulement
scrollTop. - [x] Ne jamais appeler
scrollToSelectionapres pagination normale. - [x] Garder la selection meme si l'asset selectionne n'est pas encore charge dans la page courante.
- [x] Distinguer "asset pas encore charge" de "resultat complet": prune seulement quand le resultat est complet.
- [x] Ajouter tests scroll + selection apres append.
4. Remplacer les reloads automatiques par des upserts
Etat: fait pour les chemins critiques.
- [x] Bloquer le reload automatique sur update index quand des cartes sont visibles.
- [x] Upsert en tete pour nouveaux assets compatibles avec le contexte courant.
- [x] Patch cible pour metadata existante.
- [x] Remove cible pour suppression.
- [x] Reload complet uniquement pour scope, filtre, tri, recherche ou collection.
- [x] Ajouter une API de decisions
reload/appendNextPage/refreshHead/upsertRealtime.
5. Rendre la pagination plus previsible
Etat: fait pour les metriques de base.
- [x] Continuer avec une taille de page plus grande si une page ajoute 0 carte visible.
- [x] Mesurer pages demandees.
- [x] Mesurer assets recus.
- [x] Mesurer assets visibles ajoutes.
- [x] Mesurer assets masques/dedupliques.
- [x] Mesurer temps API.
- [x] Mesurer temps render.
- [x] Ajuster la taille de page selon le "visible yield" minimal: page vide visible => page suivante plus large.
6. Durcir la virtual grid
Etat: partiel.
- [x] Stabiliser les hauteurs de lignes via estimation unique par largeur/details.
- [x] Limiter les appels a
measure()par coalescingrequestAnimationFrame. - [x] Adapter l'overscan selon vitesse de scroll.
- [x] Garder l'infinite scroll base sur distance au bas + prefetch controle.
- [x] Verifier que le sentinel ne declenche pas de reload: le chemin Vue utilise
loadNextPage.
7. Ameliorer le cache snapshot
Etat: partiel, cache disque borne et memoire etendue.
- [x] Garder un cache disque borne pour eviter les gros
JSON.stringify. - [x] Garder un cache memoire complet des assets charges par contexte.
- [x] Snapshot par contexte exact: scope, sort, filtres, query, root, subfolder.
- [x] Hydratation instantanee puis refresh silencieux sans flash via preserveVisibleUntilReady.
- [x] Interdire le remplacement brutal de la grille apres hydratation snapshot.
8. Ajouter observabilite dev
Etat: fait pour le snapshot debug programmatique.
- [x] Ajouter un snapshot debug grid programmatique.
- [x] Exposer offset, total, loaded, visible.
- [x] Exposer selected, active, loading, done.
- [x] Exposer le dernier
reload reason. - [x] Exposer le dernier
append reason. - [x] Compter les resets.
- [x] Compter les restaurations de scroll.
9. Reduire la dependance au DOM legacy
Etat: fait pour les nouveaux chemins grid.
- [x] Faire du DOM une sortie d'affichage, pas une source d'etat.
- [x] Regrouper les API legacy
_mjr*derriere_mjrGridApipour les nouveaux appels. - [x] Deplacer les lectures directes du DOM vers des selectors/bridges testes.
- [x] Supprimer progressivement les chemins dupliques Vue/legacy.
10. Renforcer les tests de stabilite
Etat: fait pour la stabilite critique.
- [x] Test: pagination adaptative quand plusieurs pages ajoutent 0 carte visible.
- [x] Test: pas de reload sur update backend quand la grille a deja des cartes visibles.
- [x] Test: selection state restore.
- [x] Test: pagination sans perte de scroll.
- [x] Test: reload avec anchor restore.
- [x] Test: selection persistante apres append.
- [x] Test: hidden/duplicate assets qui forcent adaptive paging.
- [x] Test: seul un scope/filter/sort/search/collection switch peut reset.
- [x] Test: snapshot hydrate puis refresh sans flash.
Regle directrice
- [x] Faire evoluer le grid vers: append/patch first, reload last.
docs/GRID_OPTIMIZATION_CHECKLIST.md
GRID_REFACTOR_ROADMAP.md
Grid Refactor Roadmap
Current Problem
The current asset grid mixes too many responsibilities across a few large modules:
VirtualAssetGridHost.vuehandles virtualization, selection, stacks, duplicates, drag/drop, stats, skeletons, resize, keyboard behavior, and DOM bridge compatibility.useGridLoader.jshandles pagination, localStorage snapshots, reloads, context detection, search, selection preservation, and realtime upserts.useVirtualGrid.jsmixes scroll root detection, sentinel setup,IntersectionObserver, manual scroll fallback, page fetching, dedupe, and realtime insertion.
The target architecture is a single clear pipeline:
GridQuery
|
v
PagedAssetStore
|
v
VirtualRows
|
v
AssetCard
The grid should have one authority for query state, one authority for paged loading, and one authority for virtual rendering.
Target Module Layout
js/vue/grid/
useGridQuery.js // Reads and normalizes filters, scope, sort, search
usePagedAssets.js // Fetch, reset, append, cursor/offset state
useInfiniteTrigger.js // Observes the bottom sentinel and calls loadMore
useGridVirtualRows.js // TanStack virtual rows only
useAssetCollection.js // Dedupe, upsert, remove, item indexes
useGridSnapshotCache.js // Optional cache/hydrate layer, separate from loading
Progress Tracker
- [x] Phase 0 started: pagination freeze regression coverage added for consumed pages that add no visible cards.
- [x] Phase 0 started: infinite loading now has a sentinel trigger so pagination does not depend only on scroll events.
- [x] Phase 0 advanced:
usePagedAssetsnow has synthetic 8000 asset pagination coverage. - [x] Phase 1 complete for loader/page fetch:
useGridQuery.jsadded with immutable query helpers and tests. - [x] Phase 1 complete for loader/page fetch: snapshot keys, early-fetch keys, default browse detection, and page URL filters now use the shared query normalizer.
- [x] Phase 2 started:
useAssetCollection.jsadded with Map-backed append/upsert/remove helpers and tests. - [x] Phase 3 started:
usePagedAssets.jsadded with a small offset state machine and tests. - [x] Phase 2 advanced: legacy removal path now rebuilds
assetIdSet/seenKeysthroughuseAssetCollectionhelpers. - [x] Phase 2 advanced: realtime upsert dedupe now rebuilds legacy indexes through
useAssetCollectionhelpers. - [x] Phase 3 advanced: legacy page-advance helper now delegates to
usePagedAssets.resolvePageAdvance. - [x] Phase 3 advanced: cursor state is supported by
usePagedAssetsand the legacy loader. - [x] Phase 3 advanced: adaptive empty-page pagination now runs through
usePagedAssets.loadPagesUntilVisibleinstead of living directly inuseGridLoader. - [x] Phase 3 advanced:
useGridLoadersyncs pagination throughusePagedAssets.getPageState/setPageStateinstead of mutating the composable state fields directly. - [x] Phase 3 advanced: pagination fetch/metrics wrapping is configured on
usePagedAssets;loadNextPagenow delegates visible-page loading throughpagedAssets.loadUntilVisible. - [x] Phase 3 complete for current loader: active
offset/cursor/total/donewrites are centralized through theusePagedAssetsbridge. - [x] Phase 4 started:
useInfiniteTrigger.jsadded with a sentinel-based trigger and tests. - [x] Phase 5 started:
useGridVirtualRows.jsadded with row slicing helpers and tests. - [x] Phase 5 started:
VirtualAssetGridHost.vuenow uses shared row slicing helpers. - [x] Phase 7 started:
useGridSnapshotCache.jsextracted anduseGridLoader.jsnow uses the cache API. - [x] Phase 4 complete for production component:
VirtualAssetGridHost.vueusesuseInfiniteTrigger; scroll listener now only tracks velocity. - [x] Phase 5 complete for production component: TanStack virtualizer ownership moved into
useGridVirtualRows.js. - [x] Phase 0 complete: add broad 8000+ asset scroll coverage.
- [x] Phase 1 complete: remaining panel/controller query dataset writes are moved behind a single bridge.
- [x] Phase 2 complete: asset dedupe/upsert/remove ownership moved to
useAssetCollection.js. - [x] Phase 3 complete: page loading ownership moved to
usePagedAssets.js. - [x] Phase 4 complete: only one infinite-load trigger remains in production grid flow.
- [x] Phase 5 complete: TanStack virtualization itself is owned by
useGridVirtualRows.js. - [x] Phase 6 complete:
VirtualAssetGridHost.vueis mostly composition and rendering. - [x] Phase 7 complete for current behavior: snapshot cache isolated behind
useGridSnapshotCache.js. - [x] Phase 8 complete: output browse backend cursor pagination is available with offset fallback.
The main component should eventually become composition and rendering only:
const query = useGridQuery();
const assets = usePagedAssets(query);
const virtualRows = useGridVirtualRows(assets.items);
useInfiniteTrigger({
canLoad: assets.canLoadMore,
loadMore: assets.loadMore,
});
Phase 0: Stabilize Before Refactor
Before moving code, keep the current pagination/virtual-scroll fixes and add non-regression coverage.
Recommended tests:
- 8000+ assets can scroll through multiple pages.
- A consumed backend page that adds zero visible cards does not freeze pagination.
- Scope/sort/filter changes during an in-flight request do not corrupt the grid.
- The debug snapshot exposes
query,offset,total,done,loading,items.length, visible rows, and last load reason.
Phase 1: Create useGridQuery.js
Goal: stop reading gridContainer.dataset throughout pagination and fetch code.
Responsibilities:
- Read scope, sort, filters, search, collection, custom root, and grouping state.
- Normalize values.
- Produce an immutable query object.
- Produce a stable
queryKey.
Target API:
const query = useGridQuery({
gridContainerRef,
panelStore,
});
Example immutable query:
export function createGridQuery({
scope = "output",
q = "*",
subfolder = "",
customRootId = "",
kind = "",
minRating = 0,
workflowOnly = false,
sort = "mtime_desc",
groupStacks = false,
} = {}) {
return Object.freeze({
scope,
q: String(q || "*").trim() || "*",
subfolder,
customRootId,
kind,
minRating,
workflowOnly,
sort,
groupStacks,
});
}
During migration, useGridQuery may still read DOM dataset values. That DOM dependency should live only in this module.
Phase 2: Create useAssetCollection.js
Goal: move dedupe, upsert, and remove behavior out of the loader and component.
Responsibilities:
- Own
items. - Maintain
byIdandbyKeyindexes. - Append pages.
- Upsert realtime assets.
- Remove assets.
- Reset state.
- Gradually absorb duplicate/stack representative logic.
Target API:
const collection = useAssetCollection({
assetKey,
sortKey,
groupStacks,
});
Example internal model:
const items = shallowRef([]);
const byId = new Map();
const byKey = new Map();
function assetKey(asset) {
return [
asset.source || asset.type || "output",
asset.root_id || "",
asset.subfolder || "",
asset.filename || asset.filepath || "",
].join("|").toLowerCase();
}
function appendAssets(newAssets) {
const next = items.value.slice();
for (const asset of newAssets || []) {
const id = String(asset.id || "");
const key = assetKey(asset);
if (id && byId.has(id)) continue;
if (key && byKey.has(key)) continue;
next.push(asset);
if (id) byId.set(id, asset);
if (key) byKey.set(key, asset);
}
items.value = next;
}
function upsertAsset(asset) {
const id = String(asset.id || "");
const key = assetKey(asset);
const existing = (id && byId.get(id)) || (key && byKey.get(key));
if (existing) {
Object.assign(existing, asset);
items.value = items.value.slice();
return;
}
appendAssets([asset]);
}
Keep legacy AssetCardRenderer.appendAssets as a temporary adapter until Vue state is the sole source of truth.
Phase 3: Create usePagedAssets.js
Goal: replace useGridLoader.js with a smaller state machine.
Minimal state:
const state = reactive({
items: [],
offset: 0,
total: null,
loading: false,
error: null,
done: false,
requestId: 0,
});
Target API:
const assets = usePagedAssets({
query,
collection,
pageSize,
fetchPage,
});
Core behavior:
async function reset(nextQuery) {
state.requestId += 1;
collection.reset();
state.offset = 0;
state.total = null;
state.done = false;
state.error = null;
query.value = nextQuery;
await loadMore();
}
async function loadMore() {
if (state.loading || state.done) return;
const requestId = state.requestId;
state.loading = true;
try {
const page = await fetchAssetsPage({
query: query.value,
offset: state.offset,
limit: pageSize.value,
});
if (requestId !== state.requestId) return;
appendPage(page);
} catch (err) {
if (requestId !== state.requestId) return;
state.error = err;
} finally {
if (requestId === state.requestId) {
state.loading = false;
}
}
}
function appendPage(page) {
const assets = Array.isArray(page.assets) ? page.assets : [];
collection.append(assets);
state.offset += assets.length;
if (page.total != null) {
state.total = page.total;
}
if (!assets.length) {
state.done = true;
}
if (state.total != null && state.offset >= state.total) {
state.done = true;
}
}
Keep resolvePageAdvanceCount() while the backend uses offset pagination and can return pages that are consumed but mostly hidden or deduped.
Phase 4: Create useInfiniteTrigger.js
Goal: keep one mechanism for deciding when to load more.
Prefer a single sentinel-based IntersectionObserver.
export function useInfiniteTrigger({
rootRef,
enabled,
canLoad,
loadMore,
rootMargin = "900px",
}) {
let observer = null;
const sentinelRef = ref(null);
onMounted(() => {
observer = new IntersectionObserver(
async ([entry]) => {
if (!entry?.isIntersecting) return;
if (!enabled.value || !canLoad.value) return;
await loadMore();
},
{
root: rootRef.value,
rootMargin,
threshold: 0.01,
},
);
if (sentinelRef.value) observer.observe(sentinelRef.value);
});
onBeforeUnmount(() => {
observer?.disconnect();
observer = null;
});
return { sentinelRef };
}
Remove progressively:
- manual scroll listener fallback;
- dynamic scroll root detection from pagination code;
userScrolled;allowUntilFilled;ignoreNextScroll;- sentinel ownership from
useVirtualGrid.js.
Phase 5: Create useGridVirtualRows.js
Goal: isolate TanStack virtualization.
export function useGridVirtualRows({
scrollRef,
items,
columnCount,
estimateRowHeight,
}) {
const rowCount = computed(() =>
Math.ceil(items.value.length / columnCount.value),
);
const virtualizer = useVirtualizer(
computed(() => ({
count: rowCount.value,
getScrollElement: () => scrollRef.value,
estimateSize: () => estimateRowHeight.value,
overscan: 8,
})),
);
const virtualRows = computed(() =>
virtualizer.value.getVirtualItems().map((row) => {
const start = row.index * columnCount.value;
return {
virtual: row,
items: items.value.slice(start, start + columnCount.value),
};
}),
);
return {
virtualizer,
virtualRows,
totalSize: computed(() => virtualizer.value.getTotalSize()),
};
}
VirtualAssetGridHost.vue should no longer compute virtual rows directly once this exists.
Phase 6: Reduce VirtualAssetGridHost.vue
Target responsibilities:
- Compose the grid composables.
- Render rows and cards.
- Expose temporary legacy APIs.
Move out:
- page loading;
- dedupe and upsert;
- snapshot/cache logic;
- stack/duplicate collection bookkeeping;
- infinite-scroll decision logic;
- query parsing;
- DOM dataset reads except for transitional bridge code.
The DOM bridge should remain only as compatibility for older imperative callers.
Phase 7: Extract Snapshot Cache
Goal: localStorage cache becomes an optional hydration layer, not part of pagination.
Create:
js/vue/grid/useGridSnapshotCache.js
Responsibilities:
- Load snapshot by
queryKey. - Save snapshot debounced.
- Enforce TTL.
- Enforce storage size limits.
- Never fetch network pages.
Target flow:
const snapshot = useGridSnapshotCache(query);
assets.hydrate(snapshot.items);
assets.refresh();
The network store remains the source of truth.
Phase 8: Add Cursor Pagination Backend
Long-term performance target: replace deep offset paging.
Current:
/mjr/am/list?limit=80&offset=8000
Target:
/mjr/am/list?limit=80&cursor=mtime:1730000000,id:4567
Response:
{
"assets": [],
"next_cursor": "mtime:1730000000,id:4567",
"has_more": true
}
Frontend state becomes:
const state = reactive({
items: [],
cursor: null,
hasMore: true,
loading: false,
});
Cursor migration plan:
- Keep offset pagination compatible.
- Add optional
cursorparameter. - For
mtime_desc, cursor is based on(mtime, id). - For
name_ascandname_desc, cursor is based on(filename, id). - Frontend uses cursor when the backend provides
next_cursor, otherwise falls back to offset.
Recommended Execution Order
- Add tests and debug state for the current behavior.
- Create
useGridQuery.js. - Create
useAssetCollection.js. - Create
usePagedAssets.js. - Create
useInfiniteTrigger.js. - Create
useGridVirtualRows.js. - Reduce
VirtualAssetGridHost.vue. - Extract
useGridSnapshotCache.js. - Add backend cursor pagination.
Do not start with backend cursor pagination. First make the frontend have a single loading authority. Then offset-to-cursor becomes a much smaller change.
docs/GRID_REFACTOR_ROADMAP.md
HOTKEYS_SHORTCUTS.md
Majoor Assets Manager - Hotkeys & Keyboard Shortcuts Guide
Version: 2.4.5 Last Updated: May 11, 2026
Overview
This guide covers all active keyboard shortcuts for the Majoor Assets Manager, including the new Majoor Floating Viewer (MFV).
Table of Contents
- Global / Panel Hotkeys
- Grid View Hotkeys
- Standard Viewer Hotkeys
- Majoor Floating Viewer (MFV) Hotkeys
- Video Playback Hotkeys
- Mouse Shortcuts
Global / Panel Hotkeys
These shortcuts work globally in the Assets Manager panel.
| Shortcut | Action | Scope |
|---|---|---|
| Ctrl+S / Cmd+S | Trigger Index Scan | Global (Panel Focused) |
| Ctrl+F / Ctrl+K | Focus Search Input | Global |
| Ctrl+H | Clear Search Input | Global |
| D | Toggle Sidebar (Details) | Grid / Panel |
| V | Toggle Floating Viewer | Grid / Panel |
Grid View Hotkeys
These shortcuts apply when the asset grid has focus.
Navigation & Selection
| Shortcut | Action |
|---|---|
| Arrow Keys (โโโโ) | Navigate selection |
| Enter / Space | Open Viewer |
| Ctrl+A / Cmd+A | Select All |
| Ctrl+D / Cmd+D | Deselect All |
| Ctrl+Click / Cmd+Click | Toggle Selection |
| Shift+Click | Range Selection |
| Home | Go to first asset |
| End | Go to last asset |
| Page Up | Scroll up one page |
| Page Down | Scroll down one page |
Organization & Rating
| Shortcut | Action |
|---|---|
| 0 | Reset Rating (0 stars) |
| 1 - 5 | Set Rating (1-5 stars) |
| T | Edit Tags |
| B | Add to Collection (Bookmark) |
| Shift+B | Remove from Collection |
File Operations
| Shortcut | Action |
|---|---|
| F2 | Rename File |
| Delete | Delete File (with confirmation) |
| Ctrl+Shift+C / Cmd+Shift+C | Copy File Path |
| Ctrl+Shift+E / Cmd+Shift+E | Open in Explorer/Finder |
Standard Viewer Hotkeys
These shortcuts apply when the Standard Viewer overlay is open (double-click on asset).
General
| Shortcut | Action |
|---|---|
| Esc | Close Viewer |
| F | Toggle Fullscreen |
| D | Toggle Info Panel (Generation Data) |
| Space | Play/Pause Video |
Navigation
| Shortcut | Action | Notes |
|---|---|---|
| Left Arrow | Previous Asset | Default behavior |
| Right Arrow | Next Asset | Default behavior |
| Left Arrow | Step Frame (-1) | Only when Video Player bar is focused |
| Right Arrow | Step Frame (+1) | Only when Video Player bar is focused |
| Mouse Wheel | Zoom In/Out | |
| Click+Drag | Pan Image | When zoomed in |
Tools & Analysis
| Shortcut | Action | Notes |
|---|---|---|
| I | Set In Point (video) / Toggle Pixel Probe (image) | Context-sensitive |
| O | Set Out Point | Video only |
| C | Copy Probed Color (Hex) | |
| L | Toggle Loupe | |
| Z | Toggle Zebra (Exposure) | |
| G | Cycle Grid Overlays | |
| Alt+1 | Toggle 1:1 Pixel View | |
| + / - | Zoom In / Out |
Enhancement Tools
| Shortcut | Action |
|---|---|
| E | Toggle Exposure (EV) Control |
| M | Toggle Gamma Correction |
| 1 / 2 / 3 | Isolate R/G/B Channel |
| 0 | Reset to RGB (all channels) |
| A | Toggle Alpha Channel View |
| Y | Toggle Luma (Y) View |
Analysis Overlays
| Shortcut | Action |
|---|---|
| Shift+Z | Toggle False Color |
| Shift+H | Toggle Histogram |
| Shift+W | Toggle Waveform |
| Shift+V | Toggle Vectorscope |
Majoor Floating Viewer (MFV) Hotkeys
These shortcuts apply when the Majoor Floating Viewer panel is open.
General Controls
| Shortcut | Action |
|---|---|
| Esc | Close Floating Viewer |
| V | Open or close Floating Viewer |
| C | Cycle compare modes: A/B, Side-by-side, Off (Simple mode) |
| K | Toggle KSampler denoising preview on or off |
| L | Toggle Live Stream final-output following on or off |
| N | Toggle selected-node Node Stream on or off |
Panel And Media Interaction
| Shortcut | Action |
|---|---|
| Mouse Wheel | Zoom In/Out |
| Click+Drag | Pan Image (when zoomed) |
| Drag Header | Move the floating panel |
| Drag Edges | Resize the floating panel |
Node Stream follows the currently selected compatible node when enabled. Grid compare and Graph Map are available from the MFV toolbar or mode menu rather than dedicated keyboard shortcuts.
Video Controls (MFV)
| Shortcut | Action |
|---|---|
| Space | Play/Pause the focused MFV player |
| Left Arrow | Step backward one frame |
| Right Arrow | Step forward one frame |
Click once on the inline player surface to give it focus before using the playback shortcuts above.
Video Playback Hotkeys
These shortcuts apply when playing video content in the viewer.
Playback Control
| Shortcut | Action |
|---|---|
| Space | Play/Pause |
| Click on video | Play/Pause |
Frame Navigation
| Shortcut | Action | Notes |
|---|---|---|
| Left Arrow | Previous Frame | When player bar is focused |
| Right Arrow | Next Frame | When player bar is focused |
| Home | Go to In Point | |
| End | Go to Out Point |
In/Out Points (Edit Marks)
| Shortcut | Action |
|---|---|
| I | Set In Point at current frame |
| O | Set Out Point at current frame |
Speed Control
| Shortcut | Action |
|---|---|
| [ | Decrease Playback Speed (-0.25x) |
| ] | Increase Playback Speed (+0.25x) |
| \\ | Reset Speed to Normal (1x) |
Audio
Video audio is automatically unmuted after first user interaction (play, seek, or frame step).
Mouse Shortcuts
Grid View
| Action | Result |
|---|---|
| Click | Select asset |
| Double-Click | Open in Viewer |
| Ctrl+Click / Cmd+Click | Toggle selection |
| Shift+Click | Range selection |
| Right-Click | Open context menu |
| Drag | Initiate drag & drop |
Viewer
| Action | Result |
|---|---|
| Mouse Wheel | Zoom in/out |
| Click+Drag (zoomed) | Pan image |
| Double-Click | Toggle 1:1 zoom |
| Right-Click | Open context menu |
| Middle-Click | Reset zoom and pan |
Floating Viewer
| Action | Result |
|---|---|
| Drag Header | Move panel |
| Drag Edges | Resize panel |
| Click Outside | Close open MFV popovers |
| Mouse Wheel | Zoom in/out |
| Click+Drag (zoomed) | Pan image |
Quick Reference Card
Most Used Shortcuts
Grid Navigation: Arrow keys
Open Viewer: Enter / Double-click
Rate: 0-5 keys
Search: Ctrl+F / Ctrl+K
Scan: Ctrl+S
Tags: T
Collection: B
Close Viewer: Esc
Zoom: Mouse wheel
Pan: Click+drag
Viewer Essentials
Fullscreen: F
Info Panel: D
Pixel Probe: I
Loupe: L
Zebra: Z
Grid: G
1:1 Zoom: Alt+1
MFV Essentials
Toggle MFV: V
Compare Mode: C
Live Stream: L
KSampler Preview: K
Node Stream: N
Play/Pause: Space
Customization
Remapping Shortcuts
Currently, shortcuts are hardcoded. Future versions may support customization via:
- Browser extensions
- ComfyUI keymap settings
- Configuration file
Conflicts with ComfyUI
The following shortcuts may conflict with ComfyUI globals:
- Ctrl+S: Also triggers ComfyUI workflow save
- Delete: Also deletes ComfyUI nodes
- Ctrl+Z: ComfyUI undo (not overridden)
When the Assets Manager panel has focus, its shortcuts take precedence.
Accessibility
Keyboard-Only Navigation
All features are accessible via keyboard:
- Tab through UI elements
- Enter/Space to activate
- Arrow keys to navigate
- Esc to close dialogs
Screen Reader Support
- ARIA labels on interactive elements
- Status announcements for operations
- Alt text on thumbnails
_Hotkeys & Shortcuts Guide Version: 2.4.5_ _Last Updated: May 11, 2026_ _Compatible with Majoor Assets Manager v2.4.4+_
docs/HOTKEYS_SHORTCUTS.md
INSTALLATION.md
Majoor Assets Manager - Installation Guide
Version: 2.4.5 Last Updated: April 7, 2026
Overview
This guide provides detailed instructions for installing and configuring the Majoor Assets Manager for ComfyUI. Follow these steps to get the extension up and running with all its features.
Recent highlights: Improved metadata parsing, expanded floating viewer compare modes, better workflow grouping with job and stack IDs, and support for a configurable Index DB directory (useful on network drives or NAS storage).
Prerequisites
System Requirements
- ComfyUI installation (โฅ 0.13.0 recommended)
- Python 3.10, 3.11, or 3.12 (3.13 compatible)
- At least 500MB free disk space for the extension and dependencies
- Administrator privileges (for installation and optional tool installations)
Supported Platforms
- Windows: 10/11
- macOS: 10.15 or higher
- Linux: Ubuntu 22.04+, Debian 12+, Fedora, or equivalent
Quick Installation (Recommended)
Using ComfyUI Manager
- Open ComfyUI Manager in your browser
- Find "Majoor Assets Manager" in the extensions list
- Click "Install" next to the extension
- Wait for the installation to complete
- Restart ComfyUI completely
- The extension should now be available in the Assets Manager tab
Manual Installation
Step 1: Clone the Repository
Open your terminal/command prompt and navigate to your ComfyUI custom_nodes directory:
cd /path/to/your/ComfyUI/custom_nodes
Clone the repository:
git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager ComfyUI-Majoor-AssetsManager
Step 2: Install Python Dependencies
Navigate to the extension directory and install the required packages:
cd ComfyUI-Majoor-AssetsManager
pip install -r requirements.txt
If you want the optional AI/vector features as well, install the extra vector stack too:
pip install -r requirements.txt -r requirements-vector.txt
If you are contributing to the project itself, install the local dev/test tooling separately:
pip install -r requirements-dev.txt
Dependency ownership and update rules are documented in docs/DEPENDENCY_POLICY.md.
Faster installs with uv: If you have uv installed, you can use it as a drop-in replacement for significantly faster dependency resolution:
```bash
uv pip install -r requirements.txt
```
Installuvwithpip install uvor see the uv documentation for other methods.
For AI/vector features with uv:
```bash
uv pip install -r requirements.txt -r requirements-vector.txt
```
For contributor tooling with uv:
```bash
uv pip install -r requirements-dev.txt
```
Step 3: Restart ComfyUI
Stop your ComfyUI server completely and restart it to load the new extension.
Optional Dependencies (Highly Recommended)
For full functionality including metadata extraction and file tagging, install these external tools:
Windows Installation
Option A: Using Scoop Package Manager
- Install Scoop if you don't have it:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
irm get.scoop.sh | iex
- Install the required tools:
scoop install ffmpeg exiftool
Option B: Using Chocolatey Package Manager
- Install Chocolatey if you don't have it:
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
- Install the required tools:
choco install -y ffmpeg exiftool
Option C: Using WinGet
winget install -e --id Gyan.FFmpeg
winget install -e --id OliverBetz.ExifTool
Option D: Manual Installation
- FFmpeg: Download from https://www.gyan.dev/ffmpeg/builds/
- Extract to a folder (e.g.,
C:\ffmpeg) - Add
C:\ffmpeg\binto your system PATH
- ExifTool: Download from https://exiftool.org/
- Download
exiftool-#.##.zip - Extract to a folder (e.g.,
C:\exiftool) - Add
C:\exiftoolto your system PATH
macOS Installation
Using Homebrew (Recommended)
- Install Homebrew if you don't have it:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Install the required tools:
brew install ffmpeg exiftool
Linux Installation
Ubuntu/Debian
sudo apt update
sudo apt install -y ffmpeg libimage-exiftool-perl
Fedora/RHEL
sudo dnf install -y ffmpeg perl-Image-ExifTool
Arch Linux
sudo pacman -S ffmpeg exiftool
Verification Steps
After installation, verify everything is working:
Step 1: Check Extension Loading
- Start ComfyUI
- Look for the Assets Manager tab in the interface
- Check the console/logs for any error messages during startup
Step 2: Verify External Tools (if installed)
Open a terminal/command prompt and run:
exiftool -ver
ffprobe -version
Both commands should return version information without errors.
Step 3: Test Basic Functionality
- Open the Assets Manager in ComfyUI
- Switch between different scopes (Outputs, Inputs, Custom, Collections)
- Perform a simple search to verify indexing is working
- Try opening the viewer for an asset
Configuration
Environment Variables
You can configure the extension using environment variables. Add these to your shell profile or set them before starting ComfyUI:
# Override default output directory
export MAJOOR_OUTPUT_DIRECTORY="/path/to/your/output"
# Override index database directory (useful on NAS/SMB/network drives)
# By default the index lives at <output>/_mjr_index/
# Move it to a local disk if your output is on a slow or SMB-mounted drive:
export MJR_AM_INDEX_DIRECTORY="/var/local/mjr_index"
# Specify tool paths if not in system PATH
export MAJOOR_EXIFTOOL_PATH="/path/to/exiftool"
export MAJOOR_FFPROBE_PATH="/path/to/ffprobe"
# Set media probe backend (auto, exiftool, ffprobe, both)
export MAJOOR_MEDIA_PROBE_BACKEND="auto"
UI Output Directory Override
If you prefer not to change shell startup scripts, you can override the generation output folder directly from the ComfyUI settings UI:
- Open Settings โ Majoor Assets Manager โ Advanced and search for
pathif needed. - Edit Generation Output Directory.
- Save the new directory path. Leave the field empty to fall back to the current backend default.
[Output directory override in Majoor settings]
Windows Batch File Example
Create a batch file to set environment variables and start ComfyUI:
@echo off
set MAJOOR_MEDIA_PROBE_BACKEND=auto
REM Optional: move the index DB to a local SSD if output is on a network drive
set MJR_AM_INDEX_DIRECTORY=C:\mjr_index
REM Start ComfyUI with the environment variables
cd /d "C:\path\to\ComfyUI"
python main.py --auto-launch
pause
Remote Access Write Permissions
If you open ComfyUI from another machine, Majoor blocks write operations by default unless you explicitly allow them.
Recommended setup: define MAJOOR_API_TOKEN on the machine that runs ComfyUI, then send the same token from the remote client.
If no persistent token has been configured yet, Majoor can also bootstrap the first remote session token automatically for a signed-in ComfyUI user over HTTPS. Local loopback browser sessions can also recover a write session automatically after restart/new tab without a separate ComfyUI sign-in step.
Older installs that only kept a legacy token hash are recovered automatically on the first successful bootstrap: Majoor rotates to a fresh persistent token and re-authorizes the current browser session.
Recent builds also expose the main remote security toggles directly in Majoor Settings, so a signed-in ComfyUI user can usually complete the initial setup without editing shell startup scripts manually:
Require Token For All WritesRecommended Remote LAN Setupto auto-generate a token and apply the safest common LAN defaults in one clickAllow HTTP Token Transportfor trusted LAN-only HTTP setupsAllow Remote Full Accessif you explicitly want no-token remote writesAPI Tokenfor the fixed shared token value
Fastest Settings-only path for a trusted LAN
- Open Majoor Settings in ComfyUI.
- Turn on
Recommended Remote LAN Setup. - Confirm that the current browser session is authorized.
- Keep
Allow Remote Full Accessoff unless you explicitly want no-token remote writes.
That preset automatically:
- generates a strong API token if none exists yet
- enables
Require Token For All Writes - keeps
Allow Remote Full Accessdisabled - enables
Allow HTTP Token Transportautomatically when the current session is plain HTTP on a non-loopback LAN address - injects the token into the current browser session immediately so write actions work without a manual copy/paste step
Visual confirmation inside Assets Manager:
- the runtime status widget now shows a
Write auth:line such asWrite auth: active ...ABCD - if the browser session is missing authorization while the server still requires a token, the same widget reports that state directly
If you need to authorize a different browser or device, either:
- enter a fixed shared value in
Security -> Majoor: API Token, or - open that browser while signed in to ComfyUI and let Majoor bootstrap a session token there
Windows batch example
@echo off
set MAJOOR_API_TOKEN=change-this-to-a-long-random-secret
cd /d "C:\path\to\ComfyUI"
python main.py --listen 0.0.0.0 --port 8188
pause
Windows PowerShell example
$env:MAJOOR_API_TOKEN = "change-this-to-a-long-random-secret"
Set-Location "C:\path\to\ComfyUI"
python main.py --listen 0.0.0.0 --port 8188
Linux/macOS shell example
export MAJOOR_API_TOKEN="change-this-to-a-long-random-secret"
cd /path/to/ComfyUI
python main.py --listen 0.0.0.0 --port 8188
Unsafe fallback
If you really want to allow remote writes without a token, set MAJOOR_ALLOW_REMOTE_WRITE=1 before starting ComfyUI. This is less safe and should only be used on trusted networks.
export MAJOOR_ALLOW_REMOTE_WRITE=1
python main.py --listen 0.0.0.0 --port 8188
Remote client side
- In the ComfyUI UI, open the Majoor settings and set the same token under
Security -> Majoor: API Token. - For direct API calls, send either
X-MJR-Token: <token>orAuthorization: Bearer <token>. - Restart ComfyUI after changing environment variables so the new values are picked up.
- On first remote use, if ComfyUI authentication is enabled and no persistent token exists yet, Majoor can provision the initial session token automatically.
Troubleshooting
Extension Not Appearing
- Verify the folder is named correctly:
ComfyUI-Majoor-AssetsManager - Check that all files were downloaded properly
- Ensure ComfyUI was restarted after installation
- Look for error messages in the ComfyUI console
Missing Dependencies Error
If you see messages about missing dependencies:
# Navigate to the extension directory
cd ComfyUI-Majoor-AssetsManager
pip install -r requirements.txt
If the error is related to AI/vector search features, install the optional vector stack too:
pip install -r requirements.txt -r requirements-vector.txt
External Tools Not Found
If metadata extraction isn't working:
- Verify tools are installed:
exiftool -verandffprobe -version - Check if tools are in your system PATH
- Set environment variables to point to the executables directly
Permission Errors
- Ensure ComfyUI has read/write access to the output directory
- On Windows, run as administrator if needed
- On Unix systems, check file permissions with
ls -la
Slow Performance
- The first scan may take time for large directories
- Subsequent scans will be faster due to incremental indexing
- Consider excluding very large directories that don't contain relevant assets
Post-Installation Setup
First-Time Usage
- Open ComfyUI and navigate to the Assets Manager tab
- The extension will automatically begin indexing your output directory
- Wait for the initial scan to complete (progress shown in status bar)
- Use the search bar to find assets
- Right-click on assets to access context menus
Adding Custom Directories
- Right-click in the Assets Manager interface
- Select "Add Custom Root"
- Enter the path to your custom directory
- The directory will be added to the Custom scope
Creating Your First Collection
- Select one or more assets
- Right-click and choose "Add to Collection"
- Create a new collection or add to an existing one
- Access your collections from the Collections tab
Updates
Using ComfyUI Manager
Updates will be available through the ComfyUI Manager interface when released.
Manual Updates
cd ComfyUI/custom_nodes/ComfyUI-Majoor-AssetsManager
git pull origin main
pip install -r requirements.txt --upgrade
If you use AI/vector features, also upgrade the optional vector requirements:
pip install -r requirements.txt -r requirements-vector.txt --upgrade
Remember to restart ComfyUI after updating.
Uninstallation
Using ComfyUI Manager
Simply click "Uninstall" in the ComfyUI Manager interface.
Manual Removal
- Delete the
ComfyUI-Majoor-AssetsManagerfolder fromcustom_nodes - Restart ComfyUI
- The extension will be completely removed
Installation Guide Version: 1.1 Last Updated: April 5, 2026
docs/INSTALLATION.md
MFV_GUIDE.md
Majoor Floating Viewer (MFV) Guide
Last Updated: May 10, 2026
Overview
The Majoor Floating Viewer is the fast workflow cockpit inside Majoor Assets Manager.
It is built for one practical goal: keep you close to the current result while giving you the controls you need to compare, follow execution, inspect node outputs, adjust workflow parameters, and relaunch or stop a run without bouncing back and forth between panels.
This guide focuses only on MFV.
What MFV Covers
MFV is not just a floating preview anymore. It brings together:
- compare modes
- A/B/C/D pin slots
- format and guide overlays
- Live Stream
- Node Stream
- KSampler Preview
- Node Parameters
- Run and Stop actions
- pop-out / detached window workflow
Opening MFV
You can open the Majoor Floating Viewer from the Assets Manager panel or from the viewer-related UI that already targets MFV in your current setup.
Typical workflow:
- Select an asset in the grid.
- Open the Floating Viewer.
- Pick the mode or stream you want from the MFV toolbar.
Compare Modes
MFV supports several ways to compare results depending on how much context you need on screen.
Simple mode
Use this when you want the cleanest single-asset view.
- one asset visible at a time
- best for inspection and overlay controls
- good default when you are reviewing video or a single final image
A/B compare
Use A/B compare when you want to flip between two candidates quickly.
- ideal for subtle sampler, CFG, or prompt changes
- good for before/after review
- especially useful when one slot is pinned and the other stays live
Side-by-side compare
Use side-by-side when both assets must remain visible at the same time.
- stronger spatial comparison than A/B switching
- useful for composition and crop review
- good when synchronized zoom/pan matters
Grid compare
Use grid compare when you want to review several references together.
- suited to broader sweeps
- pairs naturally with A/B/C/D pins
- good for sampler or prompt variation sets
Pin Mode (A/B/C/D)
The pin system lets you lock up to four references so they stay stable while other slots keep changing.
What pins are for
- keep your best result as a baseline
- compare new generations against a fixed reference
- hold four selected results in grid compare
Practical pin patterns
- A pinned, B live: best pattern for iterative prompt or sampler work
- A/B pinned: useful for deliberate side-by-side review
- A/B/C/D pinned: useful for a final selection round in grid compare
Pinned slots are meant to remain stable while unpinned content can keep following the current workflow output.
Format And Guides
MFV can also act as a framing and presentation surface.
Format overlays
Use format controls when you want to preview how the media reads inside a target ratio or crop.
- useful for cinematic framing checks
- useful when comparing several outputs against the same intended presentation shape
Guides
Use guides when you want composition references directly in the viewer.
- rule-of-thirds style guidance
- safe-area style guidance
- fast visual help during compare sessions
These controls matter most when MFV is used as a review surface, not just as a passive preview.
Live Stream
Live Stream is for final outputs.
- follows newly completed outputs after execution
- best for monitoring what the workflow actually saves
- keeps MFV aligned with the most recent finished result
Use Live Stream when you care about the last completed output file, not the currently selected node.
Node Stream
Node Stream is for selected-node media.
- follows the node you click in ComfyUI when that node exposes frontend media
- useful for preview nodes, save nodes, loader nodes, and compatible live-preview surfaces
- good for debugging a specific point in the workflow
Use Node Stream when you want to inspect the media attached to the selected node, not the final workflow output.
KSampler Preview
KSampler Preview is for denoising-step frames during execution.
- shows the progression while the workflow is still running
- useful when you want feedback before the final file exists
- different from Live Stream and different from Node Stream
In short:
- Live Stream = final outputs
- Node Stream = selected node media
- KSampler Preview = execution preview frames
Node Parameters
Node Parameters let you inspect and edit the workflow node that produced the current result without leaving MFV.
Typical values you will use there:
- prompt text
- seed
- steps
- CFG
- sampler
- scheduler
This makes MFV practical for short iteration loops where you want to tweak one or two values and launch another run immediately.
Run And Stop
MFV is not only a viewer surface. It also exposes execution actions.
Run
Use Run when you want to queue the current workflow again directly from MFV.
Good use cases:
- prompt iteration
- seed changes
- sampler or scheduler changes
- quick compare loops with pinned references
Stop
Use Stop when you want to abort the current generation without leaving the viewer surface.
Good use cases:
- a bad direction is obvious early
- KSampler Preview already tells you the run is not worth finishing
- you want to reclaim time before launching the next variation
Pop-Out
Pop-out detaches MFV from the main ComfyUI window so it can live as a separate viewer window.
This is useful when:
- you want MFV on a second screen
- you want the canvas and the viewer visible side by side
- you want to keep MFV open as a dedicated monitoring surface while working on the graph elsewhere
Graph Map As A Natural Companion
Graph Map is the workflow-context side of MFV.
Use MFV when you want to compare outputs, follow streams, inspect node parameters, or relaunch quickly. Open Graph Map when you want to understand where the current result came from in the saved workflow.
Graph Map complements MFV by adding:
- readable node and subgraph names instead of raw opaque identifiers
- a selected-node detail panel with copy-ready parameters and actions
- a workflow overview that stays close to the current asset preview
Typical pairing:
- Review the latest result in MFV.
- Open Graph Map to locate the relevant node or subgraph.
- Inspect the selected node details.
- Return to Node Parameters or Run inside MFV for the next iteration.
For the full Graph Map walkthrough, including the selected-node detail panel, see GRAPH_MAP.md.
Recommended Workflows
Fast prompt iteration
- Pin your baseline in slot A.
- Edit prompt or seed in Node Parameters.
- Click Run.
- Let the unpinned slot follow Live Stream.
Execution monitoring
- Turn on KSampler Preview.
- Watch denoising steps during the run.
- Keep Live Stream ready for the final output.
Node-focused debugging
- Turn on Node Stream.
- Click the relevant node in ComfyUI.
- Inspect available frontend media from that node.
Two-screen review
- Pop out MFV.
- Keep ComfyUI on the main screen.
- Use the detached viewer as a dedicated compare and monitoring surface.
Related Docs
docs/MFV_GUIDE.md
PLUGIN_QUICK_REFERENCE.md
Plugin Development Quick Reference
Quick Start
1. Create Plugin File
# my_plugin.py
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self): return "my_extractor"
@property
def supported_extensions(self): return ['.png']
@property
def priority(self): return 50
async def extract(self, filepath):
data = {"key": "value"}
return self._create_success_result(data)
2. Install Plugin
# Copy to plugin directory
cp my_plugin.py ~/.comfyui/majoor_plugins/extractors/
3. Reload Plugins
# Via API
curl -X POST http://localhost:8188/mjr/am/plugins/reload
# Or restart ComfyUI
Plugin API
Required Properties
| Property | Type | Description |
|---|---|---|
name | str | Unique identifier (lowercase, underscores) |
supported_extensions | list[str] | File extensions (e.g., ['.png', '.webp']) |
priority | int | Higher = runs first (100-999 custom, 50-99 format, 1-49 generic) |
Required Methods
| Method | Signature | Description |
|---|---|---|
extract | async def extract(self, filepath: str) -> ExtractionResult | Main extraction logic |
Optional Properties
| Property | Type | Default | Description |
|---|---|---|---|
metadata | ExtractorMetadata | Auto | Plugin info (version, author, description) |
min_compatibility_version | str | "2.4.5" | Minimum Majoor version |
Optional Methods
| Method | Signature | Description |
|---|---|---|
can_extract | def can_extract(self, filepath: str) -> bool | Check if can handle file |
pre_extract | async def pre_extract(self, filepath: str) -> bool | Pre-extraction validation |
post_extract | async def post_extract(self, filepath, result) | Post-extraction enrichment |
cleanup | async def cleanup(self) | Cleanup on unload |
Helper Methods
| Method | Signature | Description |
|---|---|---|
_create_success_result | def _create_success_result(self, data, confidence=1.0) | Create success result |
_create_error_result | def _create_error_result(self, error) | Create error result |
ExtractionResult
@dataclass
class ExtractionResult:
success: bool # True if extraction succeeded
data: Dict[str, Any] # Extracted metadata
error: Optional[str] # Error message if failed
extractor_name: str # Plugin name
confidence: float # 0.0-1.0 confidence score
Standard Metadata Fields
{
# Standard ComfyUI metadata
"prompt": str, # Main prompt
"negative_prompt": str, # Negative prompt
"seed": int, # Random seed
"steps": int, # Sampling steps
"sampler": str, # Sampler name
"cfg": float, # CFG scale
"models": list, # Model names
"loras": list, # LoRA info
# Custom data
"custom_data": dict, # Node-specific data
"workflow": dict, # Workflow JSON
"file_info": dict, # File information
}
Plugin Lifecycle
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Discovery โ
โ - Plugin file found in plugin directory โ
โ - Validated by PluginValidator โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. Loading โ
โ - Module imported โ
โ - Extractor classes instantiated โ
โ - Registered in PluginLoader โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. Runtime โ
โ - can_extract() called to check compatibility โ
โ - pre_extract() called for validation โ
โ - extract() called for extraction โ
โ - post_extract() called for enrichment โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. Cleanup โ
โ - cleanup() called on unload โ
โ - Resources released โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Security
Blocked Patterns
The following patterns are blocked during validation:
# Code execution
eval()
exec()
compile()
# OS access
os.system()
os.popen()
subprocess.*
# Network access
socket.*
requests.*
urllib.*
# Unsafe deserialization
pickle.load()
marshal.load()
# Dynamic imports
__import__()
importlib.reload()
Allowed Imports
# Standard library (safe)
typing, collections, functools, itertools, pathlib
json, re, hashlib, base64, io, struct
logging, dataclasses, enum, abc, contextlib
asyncio, datetime, time, math
# Third-party (safe)
PIL (Pillow)
numpy
Testing
Unit Test Example
import pytest
from my_plugin import MyExtractor
@pytest.mark.asyncio
async def test_extraction(tmp_path):
extractor = MyExtractor()
# Create test file
test_file = tmp_path / "test.png"
test_file.write_bytes(b"fake png data")
# Test extraction
result = await extractor.extract(str(test_file))
assert result.success is True
assert "key" in result.data
assert result.confidence > 0
Debug Logging
# Enable debug logging
export MJR_PLUGIN_LOG_LEVEL=DEBUG
# In plugin code
logger.debug(f"Debug message: {data}")
logger.info(f"Info message: {data}")
logger.warning(f"Warning message: {data}")
logger.error(f"Error message: {error}")
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/mjr/am/plugins/list | GET | List all plugins |
/mjr/am/plugins/{name}/enable | POST | Enable plugin |
/mjr/am/plugins/{name}/disable | POST | Disable plugin |
/mjr/am/plugins/reload | POST | Reload all plugins |
Example Requests
# List plugins
curl http://localhost:8188/mjr/am/plugins/list
# Enable plugin
curl -X POST http://localhost:8188/mjr/am/plugins/my_extractor/enable
# Disable plugin
curl -X POST http://localhost:8188/mjr/am/plugins/my_extractor/disable
# Reload plugins
curl -X POST http://localhost:8188/mjr/am/plugins/reload
Troubleshooting
Plugin Not Loading
| Symptom | Solution |
|---|---|
| No error in logs | Check file is in correct directory |
| Syntax error | Fix Python syntax in plugin file |
| Validation failed | Remove blocked patterns |
| Import error | Check import paths are valid |
Extraction Fails
| Symptom | Solution |
|---|---|
| File not found | Verify filepath is absolute |
| Permission denied | Check file permissions |
| Timeout | Optimize extraction logic |
| Low confidence | Improve data extraction |
Performance Issues
| Symptom | Solution |
|---|---|
| Slow extraction | Add caching, optimize logic |
| High memory | Release resources in cleanup() |
| Blocking I/O | Use async operations |
Best Practices
Code Organization
# Good: Organized extraction logic
class MyExtractor(MetadataExtractorPlugin):
async def extract(self, filepath):
if filepath.endswith('.png'):
return await self._extract_png(filepath)
elif filepath.endswith('.json'):
return await self._extract_json(filepath)
return self._create_error_result("Unsupported format")
async def _extract_png(self, filepath):
# PNG-specific logic
pass
async def _extract_json(self, filepath):
# JSON-specific logic
pass
Error Handling
# Good: Comprehensive error handling
async def extract(self, filepath):
try:
# Pre-checks
if not Path(filepath).exists():
return self._create_error_result("File not found")
# Extraction
data = await self._do_extract(filepath)
return self._create_success_result(data)
except FileNotFoundError as e:
return self._create_error_result(f"File not found: {e}")
except PermissionError as e:
return self._create_error_result(f"Permission denied: {e}")
except Exception as e:
logger.exception(f"Extraction failed")
return self._create_error_result(str(e))
Logging
# Good: Appropriate logging levels
logger.debug(f"Processing file: {filepath}") # Debug info
logger.info(f"Extracted metadata from {filepath}") # Success
logger.warning(f"Missing field: {field}") # Non-critical issue
logger.error(f"Extraction failed: {error}") # Critical error
Examples
See plugins/examples/ for complete examples:
wanvideo_extractor.py- WanVideo metadata extractioncustom_node_extractor.py- Template with documentation
Resources
docs/PLUGIN_SYSTEM_DESIGN.md- Full architecturemjr_am_backend/features/metadata/plugin_system/base.py- API referenceplugins/README.md- Installation guide
docs/PLUGIN_QUICK_REFERENCE.md
PLUGIN_SYSTEM_DESIGN.md
Majoor Assets Manager - Plugin System Design Document
Document Type: Architecture Design Version: 1.0.0 Date: March 16, 2026 Author: Architecture Team Status: โ Implemented (as of April 2026, v2.4.4+)
Note: This design has been fully implemented. See <code>PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md</code> for implementation details and <code>PLUGIN_QUICK_REFERENCE.md</code> for developer quick reference.
๐ Table of Contents
- Executive Summary
- Current Architecture Analysis
- Plugin System Architecture
- Implementation Plan
- API Reference
- Security Model
- Testing Strategy
- Migration Guide
- Appendices
๐ Executive Summary
Objective
Design and implement a plugin system for the Majoor Assets Manager that allows third-party developers to create custom metadata extractors without modifying the core codebase.
Problem Statement
The current metadata extraction system is hardcoded with fixed extractors for known formats (PNG, WEBP, video, audio). This creates limitations:
- โ Cannot extract custom node-specific metadata (WanVideo, rgthree, etc.)
- โ Cannot support new file formats without code changes
- โ Cannot add custom parsing logic for proprietary schemas
- โ Requires core codebase modification for any new extractor
Solution
Implement a plugin architecture with:
- โ Well-defined plugin interface (abstract base class)
- โ Auto-discovery mechanism (scan plugin directories)
- โ Priority-based extractor selection
- โ Sandboxed execution environment
- โ Hot-reload capability for development
Benefits
| Benefit | Impact |
|---|---|
| Extensibility | Add formats without core changes |
| Community | Share extractors as separate packages |
| Isolation | Plugin bugs don't crash core system |
| Testing | Test extractors independently |
| Versioning | Independent release cycles |
๐ Current Architecture Analysis
Current Metadata Extraction Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MetadataService.extract() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ File Type Detection (image/video/audio/3d) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Hardcoded Extractor Lookup โ
โ - extract_png_metadata() โ
โ - extract_webp_metadata() โ
โ - extract_video_metadata() โ
โ - extract_audio_metadata() โ
โ - extract_3d_model_metadata() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Metadata Normalization & Storage โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Current Extractor Registry
File: mjr_am_backend/features/metadata/extractor_registry.py
# Current implementation - hardcoded extractors
EXTRACTORS = {
"png": extract_png_metadata,
"webp": extract_webp_metadata,
"video": extract_video_metadata,
"audio": extract_audio_metadata,
"model3d": extract_3d_model_metadata,
}
def get_extractor(file_type: str):
return EXTRACTORS.get(file_type)
Limitations Identified
- No Extension Points - Cannot inject custom extractors
- Tight Coupling - Extractors imported directly in service
- No Priority System - All extractors treated equally
- No Validation - No plugin safety checks
- No Discovery - Manual registration required
๐๏ธ Plugin System Architecture
High-Level Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ComfyUI Runtime โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Majoor Assets Manager โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MetadataService (Modified) โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ PluginLoader (NEW) โ โ โ
โ โ โ - discover_plugins() โ โ โ
โ โ โ - validate_plugins() โ โ โ
โ โ โ - get_extractor(filepath) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Built-in Extractors โ โ โ
โ โ โ - PNG/WEBP extractor โ โ โ
โ โ โ - Video extractor (FFprobe) โ โ โ
โ โ โ - Audio extractor โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Plugin Directory โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โ
โ โ wanvideo_plugin โ โ rgthree_plugin โ โ custom_plugin โ โ
โ โ โโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโ โ โ
โ โ priority: 100 โ โ priority: 50 โ โ priority: 10 โ โ
โ โ extensions: .png โ โ extensions: .png โ โ extensions: โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Component Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Plugin System Components โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Plugin Interface โ Abstract base class defining contract โ
โ โ (ABC) โ โ
โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Plugin Loader โ Discovery, validation, loading โ
โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Plugin Registry โ Runtime registry of loaded plugins โ
โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Plugin Manager โ Lifecycle management, hot-reload โ
โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Security Validatorโ Sandboxing, permission checks โ
โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Directory Structure
ComfyUI-Majoor-AssetsManager/
โโโ mjr_am_backend/
โ โโโ features/
โ โโโ metadata/
โ โโโ service.py # Modified to use plugins
โ โโโ extractor_registry.py # Keep built-in extractors
โ โโโ plugin_system/ # NEW: Plugin system
โ โโโ __init__.py
โ โโโ base.py # Plugin interface (ABC)
โ โโโ loader.py # Plugin discovery & loading
โ โโโ registry.py # Runtime plugin registry
โ โโโ manager.py # Lifecycle management
โ โโโ validator.py # Security validation
โ โโโ sandbox.py # Execution sandboxing
โ
โโโ plugins/ # NEW: Plugin directory
โ โโโ __init__.py
โ โโโ README.md
โ โโโ examples/
โ โโโ wanvideo_extractor.py
โ โโโ rgthree_extractor.py
โ โโโ custom_node_extractor.py
โ
โโโ tests/
โ โโโ plugins/ # NEW: Plugin tests
โ โโโ test_plugin_loader.py
โ โโโ test_plugin_validator.py
โ โโโ test_plugin_integration.py
โ
โโโ docs/
โโโ PLUGIN_DEVELOPMENT_GUIDE.md # NEW: Plugin dev guide
๐ ๏ธ Implementation Plan
Phase 1: Core Infrastructure (Week 1)
1.1 Define Plugin Interface
File: mjr_am_backend/features/metadata/plugin_system/base.py
"""
Plugin System - Base Interface
Defines the abstract base class for metadata extractor plugins.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
import logging
logger = logging.getLogger(__name__)
@dataclass
class ExtractorMetadata:
"""Metadata about an extractor plugin."""
name: str
version: str = "1.0.0"
author: str = "Unknown"
description: str = ""
homepage: str = ""
license: str = "MIT"
@dataclass
class ExtractionResult:
"""Result from metadata extraction."""
success: bool
data: Dict[str, Any] = field(default_factory=dict)
error: Optional[str] = None
extractor_name: str = ""
confidence: float = 1.0 # 0.0-1.0, for fuzzy matching
class MetadataExtractorPlugin(ABC):
"""
Abstract base class for metadata extractor plugins.
All plugins must inherit from this class and implement
the required methods.
Example:
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self): return "my_extractor"
@property
def supported_extensions(self): return ['.png', '.webp']
async def extract(self, filepath): ...
"""
# โโโ Required Properties โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@property
@abstractmethod
def name(self) -> str:
"""
Unique plugin identifier.
Must be lowercase, alphanumeric with underscores.
Example: "wanvideo_extractor", "rgthree_extractor"
"""
pass
@property
@abstractmethod
def supported_extensions(self) -> List[str]:
"""
File extensions this extractor handles.
Returns:
List of extensions including dot, e.g., ['.png', '.webp']
"""
pass
@property
@abstractmethod
def priority(self) -> int:
"""
Extraction priority (higher = runs first).
Priority levels:
- 100-999: Custom node-specific extractors
- 50-99: Format-specific extractors
- 1-49: Generic/fallback extractors
"""
pass
# โโโ Optional Properties โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@property
def metadata(self) -> ExtractorMetadata:
"""Plugin metadata (optional, provides defaults)."""
return ExtractorMetadata(name=self.name)
@property
def min_compatibility_version(self) -> str:
"""Minimum Majoor version required (default: "2.4.5")."""
return "2.4.5"
# โโโ Required Methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@abstractmethod
async def extract(self, filepath: str) -> ExtractionResult:
"""
Extract metadata from file.
Args:
filepath: Absolute path to file
Returns:
ExtractionResult with success status and data
Raises:
Any exceptions should be caught and returned as
ExtractionResult(success=False, error=str(exc))
"""
pass
# โโโ Optional Methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def can_extract(self, filepath: str) -> bool:
"""
Check if this extractor can handle the file.
Default implementation checks file extension.
Override for more complex detection logic.
"""
ext = Path(filepath).suffix.lower()
return ext in self.supported_extensions
async def pre_extract(self, filepath: str) -> bool:
"""
Pre-extraction hook (optional).
Called before extract(). Return False to skip extraction.
Useful for quick validation checks.
"""
return True
async def post_extract(
self,
filepath: str,
result: ExtractionResult
) -> ExtractionResult:
"""
Post-extraction hook (optional).
Called after extract(). Can modify result.
Useful for cleanup or data enrichment.
"""
return result
async def cleanup(self) -> None:
"""
Cleanup hook called on plugin unload.
Override to release resources, close connections, etc.
"""
pass
# โโโ Helper Methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _create_success_result(
self,
data: Dict[str, Any],
confidence: float = 1.0
) -> ExtractionResult:
"""Helper to create success result."""
return ExtractionResult(
success=True,
data=data,
extractor_name=self.name,
confidence=confidence
)
def _create_error_result(
self,
error: str
) -> ExtractionResult:
"""Helper to create error result."""
return ExtractionResult(
success=False,
error=error,
extractor_name=self.name
)
1.2 Implement Plugin Loader
File: mjr_am_backend/features/metadata/plugin_system/loader.py
"""
Plugin System - Loader
Discovers, validates, and loads plugins from configured directories.
"""
from __future__ import annotations
import importlib
import importlib.util
import pkgutil
import sys
from pathlib import Path
from typing import Dict, List, Optional, Set, Type
import logging
from .base import MetadataExtractorPlugin, ExtractorMetadata
logger = logging.getLogger(__name__)
class PluginLoadError(Exception):
"""Raised when plugin loading fails."""
pass
class PluginLoader:
"""
Discovers and loads metadata extractor plugins.
Usage:
loader = PluginLoader([Path("/plugins")])
loader.discover_plugins()
extractor = loader.get_extractor("image.png")
result = await extractor.extract("image.png")
"""
def __init__(
self,
plugin_dirs: Optional[List[Path]] = None,
auto_discover: bool = True
):
"""
Initialize plugin loader.
Args:
plugin_dirs: Directories to scan for plugins
auto_discover: Automatically discover on init
"""
self.plugin_dirs = plugin_dirs or self._default_plugin_dirs()
self._extractors: Dict[str, MetadataExtractorPlugin] = {}
self._loaded_modules: Set[str] = set()
self._load_errors: List[tuple[str, str]] = []
if auto_discover:
self.discover_plugins()
def _default_plugin_dirs(self) -> List[Path]:
"""Get default plugin directories."""
from ...config import OUTPUT_ROOT_PATH
return [
# Bundled plugins
Path(__file__).parent.parent.parent.parent / "plugins",
# User plugins (global)
Path.home() / ".comfyui" / "majoor_plugins" / "extractors",
# User plugins (local to ComfyUI)
OUTPUT_ROOT_PATH / "_mjr_plugins" / "extractors",
]
def discover_plugins(self) -> int:
"""
Scan plugin directories and load all valid extractors.
Returns:
Number of plugins successfully loaded
"""
loaded_count = 0
for plugin_dir in self.plugin_dirs:
if not plugin_dir.exists():
logger.debug(f"Plugin directory does not exist: {plugin_dir}")
continue
if not plugin_dir.is_dir():
logger.warning(f"Plugin path is not a directory: {plugin_dir}")
continue
try:
count = self._scan_directory(plugin_dir)
loaded_count += count
logger.info(f"Loaded {count} plugins from {plugin_dir}")
except Exception as e:
logger.error(f"Failed to scan {plugin_dir}: {e}")
self._load_errors.append((str(plugin_dir), str(e)))
logger.info(f"Total plugins loaded: {loaded_count}")
return loaded_count
def _scan_directory(self, directory: Path) -> int:
"""Scan a single directory for plugins."""
count = 0
# Scan .py files
for module_file in directory.glob("*.py"):
if module_file.name.startswith("_"):
continue
try:
self._load_module(module_file)
count += 1
except Exception as e:
logger.error(f"Failed to load {module_file}: {e}")
self._load_errors.append((str(module_file), str(e)))
# Scan subdirectories as packages
for subdir in directory.iterdir():
if not subdir.is_dir():
continue
if subdir.name.startswith("_"):
continue
if not (subdir / "__init__.py").exists():
continue
try:
count += self._scan_package(subdir)
except Exception as e:
logger.error(f"Failed to scan package {subdir}: {e}")
return count
def _scan_package(self, package_dir: Path) -> int:
"""Scan a package directory for plugins."""
count = 0
package_name = package_dir.name
for module_file in package_dir.glob("*.py"):
if module_file.name.startswith("_"):
continue
if module_file.name == "__init__.py":
continue
try:
self._load_module(module_file, package_name=package_name)
count += 1
except Exception as e:
logger.error(f"Failed to load {module_file}: {e}")
return count
def _load_module(
self,
module_file: Path,
package_name: Optional[str] = None
) -> None:
"""Load a single Python module and extract plugins."""
module_name = module_file.stem
full_name = f"majoor_plugins.{package_name}.{module_name}" if package_name else f"majoor_plugins.{module_name}"
# Skip already loaded modules
if full_name in self._loaded_modules:
return
# Load module
spec = importlib.util.spec_from_file_location(full_name, module_file)
if spec is None or spec.loader is None:
raise PluginLoadError(f"Cannot load spec for {module_file}")
module = importlib.util.module_from_spec(spec)
sys.modules[full_name] = module
spec.loader.exec_module(module)
self._loaded_modules.add(full_name)
# Find and instantiate all extractor classes
for attr_name in dir(module):
attr = getattr(module, attr_name)
if self._is_valid_extractor_class(attr):
try:
extractor = attr()
self.register_extractor(extractor)
except Exception as e:
logger.error(f"Failed to instantiate {attr_name}: {e}")
def _is_valid_extractor_class(self, cls: Type) -> bool:
"""Check if class is a valid extractor plugin."""
try:
return (
isinstance(cls, type) and
issubclass(cls, MetadataExtractorPlugin) and
cls is not MetadataExtractorPlugin
)
except TypeError:
return False
def register_extractor(
self,
extractor: MetadataExtractorPlugin,
force: bool = False
) -> None:
"""
Register an extractor instance.
Args:
extractor: Extractor instance to register
force: Overwrite existing extractor with same name
"""
name = extractor.name
if name in self._extractors and not force:
logger.warning(f"Extractor '{name}' already registered, skipping")
return
self._extractors[name] = extractor
logger.debug(f"Registered extractor: {name}")
def unregister_extractor(self, name: str) -> bool:
"""
Unregister an extractor by name.
Returns:
True if extractor was found and removed
"""
if name not in self._extractors:
return False
extractor = self._extractors.pop(name)
try:
# Call cleanup hook
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(extractor.cleanup())
except Exception as e:
logger.error(f"Cleanup failed for {name}: {e}")
logger.debug(f"Unregistered extractor: {name}")
return True
def get_extractor(self, filepath: str) -> Optional[MetadataExtractorPlugin]:
"""
Get the best extractor for a file (by priority).
Args:
filepath: Path to file
Returns:
Best matching extractor or None
"""
matching = [
ext for ext in self._extractors.values()
if ext.can_extract(filepath)
]
if not matching:
return None
# Return highest priority extractor
return max(matching, key=lambda e: e.priority)
def get_extractor_by_name(self, name: str) -> Optional[MetadataExtractorPlugin]:
"""Get extractor by name."""
return self._extractors.get(name)
@property
def all_extractors(self) -> List[MetadataExtractorPlugin]:
"""Get all registered extractors (sorted by priority)."""
return sorted(
self._extractors.values(),
key=lambda e: e.priority,
reverse=True
)
@property
def extractor_names(self) -> List[str]:
"""Get names of all registered extractors."""
return list(self._extractors.keys())
@property
def load_errors(self) -> List[tuple[str, str]]:
"""Get list of (path, error) tuples for failed loads."""
return self._load_errors.copy()
def get_stats(self) -> Dict[str, any]:
"""Get loader statistics."""
return {
"total_extractors": len(self._extractors),
"loaded_modules": len(self._loaded_modules),
"load_errors": len(self._load_errors),
"plugin_dirs": [str(d) for d in self.plugin_dirs],
}
1.3 Implement Plugin Registry
File: mjr_am_backend/features/metadata/plugin_system/registry.py
"""
Plugin System - Registry
Runtime registry for plugin state and configuration.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional
import logging
from .base import MetadataExtractorPlugin, ExtractorMetadata
logger = logging.getLogger(__name__)
@dataclass
class PluginState:
"""Runtime state of a plugin."""
name: str
version: str
enabled: bool = True
load_order: int = 0
error_count: int = 0
last_error: Optional[str] = None
extraction_count: int = 0
avg_confidence: float = 0.0
class PluginRegistry:
"""
Runtime registry for plugin state and configuration.
Persists plugin configuration and tracks runtime statistics.
"""
def __init__(self, config_path: Optional[Path] = None):
"""
Initialize registry.
Args:
config_path: Path to persist configuration (optional)
"""
self.config_path = config_path
self._plugin_states: Dict[str, PluginState] = {}
self._load_order_counter = 0
if config_path and config_path.exists():
self._load_config()
def register_plugin(
self,
plugin: MetadataExtractorPlugin,
enabled: bool = True
) -> None:
"""Register a plugin with state tracking."""
name = plugin.name
if name in self._plugin_states:
state = self._plugin_states[name]
state.version = plugin.metadata.version
else:
state = PluginState(
name=name,
version=plugin.metadata.version,
enabled=enabled,
load_order=self._load_order_counter
)
self._load_order_counter += 1
self._plugin_states[name] = state
def unregister_plugin(self, name: str) -> None:
"""Unregister a plugin."""
if name in self._plugin_states:
del self._plugin_states[name]
def enable_plugin(self, name: str) -> bool:
"""Enable a plugin."""
if name not in self._plugin_states:
return False
self._plugin_states[name].enabled = True
self._save_config()
return True
def disable_plugin(self, name: str) -> bool:
"""Disable a plugin."""
if name not in self._plugin_states:
return False
self._plugin_states[name].enabled = False
self._save_config()
return True
def is_enabled(self, name: str) -> bool:
"""Check if plugin is enabled."""
if name not in self._plugin_states:
return False
return self._plugin_states[name].enabled
def record_error(self, name: str, error: str) -> None:
"""Record an error for a plugin."""
if name not in self._plugin_states:
return
state = self._plugin_states[name]
state.error_count += 1
state.last_error = error
def record_extraction(
self,
name: str,
confidence: float = 1.0
) -> None:
"""Record a successful extraction."""
if name not in self._plugin_states:
return
state = self._plugin_states[name]
state.extraction_count += 1
# Running average of confidence
total = state.extraction_count
state.avg_confidence = (
(state.avg_confidence * (total - 1) + confidence) / total
)
def get_state(self, name: str) -> Optional[PluginState]:
"""Get state for a plugin."""
return self._plugin_states.get(name)
def get_all_states(self) -> List[PluginState]:
"""Get all plugin states."""
return list(self._plugin_states.values())
def get_enabled_plugins(self) -> List[PluginState]:
"""Get states for enabled plugins only."""
return [s for s in self._plugin_states.values() if s.enabled]
def _load_config(self) -> None:
"""Load configuration from disk."""
try:
data = json.loads(self.config_path.read_text())
for name, state_data in data.get("plugins", {}).items():
self._plugin_states[name] = PluginState(**state_data)
logger.info(f"Loaded plugin config from {self.config_path}")
except Exception as e:
logger.warning(f"Failed to load plugin config: {e}")
def _save_config(self) -> None:
"""Save configuration to disk."""
if not self.config_path:
return
try:
data = {
"plugins": {
name: asdict(state)
for name, state in self._plugin_states.items()
}
}
self.config_path.parent.mkdir(parents=True, exist_ok=True)
self.config_path.write_text(json.dumps(data, indent=2))
except Exception as e:
logger.warning(f"Failed to save plugin config: {e}")
def get_stats(self) -> Dict[str, Any]:
"""Get registry statistics."""
states = self.get_all_states()
return {
"total_plugins": len(states),
"enabled_plugins": len(self.get_enabled_plugins()),
"total_extractions": sum(s.extraction_count for s in states),
"total_errors": sum(s.error_count for s in states),
}
Phase 2: Integration (Week 2)
2.1 Modify MetadataService
File: mjr_am_backend/features/metadata/service.py
"""Metadata extraction service - Modified for plugin support."""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, Optional
import logging
from .plugin_system.loader import PluginLoader
from .plugin_system.registry import PluginRegistry
from .extractors import extract_png_metadata, extract_webp_metadata
from ...config import INDEX_DIR_PATH
logger = logging.getLogger(__name__)
class MetadataService:
"""
Metadata extraction service with plugin support.
Orchestrates extraction using both built-in extractors
and plugin-based extractors.
"""
def __init__(
self,
exiftool,
ffprobe,
settings,
plugin_dirs: Optional[list] = None
):
self.exiftool = exiftool
self.ffprobe = ffprobe
self.settings = settings
# Initialize plugin system
plugin_config_path = INDEX_DIR_PATH / "plugin_config.json"
self.plugin_registry = PluginRegistry(plugin_config_path)
self.plugin_loader = PluginLoader(
plugin_dirs=plugin_dirs,
auto_discover=True
)
logger.info(
f"MetadataService initialized with "
f"{len(self.plugin_loader.all_extractors)} plugin extractors"
)
async def extract_metadata(
self,
filepath: str,
file_type: str
) -> Dict[str, Any]:
"""
Extract metadata from file using plugin system.
Priority:
1. Plugin extractors (by priority order)
2. Built-in extractors (fallback)
Args:
filepath: Absolute path to file
file_type: File type (image, video, audio, model3d)
Returns:
Dict with extracted metadata
"""
filepath = str(filepath).strip()
# 1. Try plugin extractors first (by priority)
plugin_result = await self._extract_with_plugins(filepath)
if plugin_result and plugin_result.get("success"):
logger.debug(
f"Plugin extraction successful for {filepath} "
f"(extractor: {plugin_result.get('extractor')})"
)
return plugin_result.get("data", {})
# 2. Fallback to built-in extractors
logger.debug(f"Falling back to built-in extractor for {filepath}")
return await self._extract_with_builtin(filepath, file_type)
async def _extract_with_plugins(
self,
filepath: str
) -> Optional[Dict[str, Any]]:
"""Try extraction with plugin extractors."""
extractor = self.plugin_loader.get_extractor(filepath)
if not extractor:
return None
plugin_name = extractor.name
# Check if plugin is enabled
if not self.plugin_registry.is_enabled(plugin_name):
logger.debug(f"Plugin {plugin_name} is disabled, skipping")
return None
try:
# Pre-extraction hook
if not await extractor.pre_extract(filepath):
logger.debug(f"Plugin {plugin_name} pre_extract returned False")
return None
# Extract
result = await extractor.extract(filepath)
if result.success:
# Record success
self.plugin_registry.record_extraction(
plugin_name,
result.confidence
)
# Post-extraction hook
result = await extractor.post_extract(filepath, result)
return {
"success": True,
"data": result.data,
"extractor": plugin_name,
"confidence": result.confidence,
}
else:
# Record error
self.plugin_registry.record_error(
plugin_name,
result.error or "Unknown error"
)
logger.warning(
f"Plugin {plugin_name} extraction failed: {result.error}"
)
return None
except Exception as e:
logger.exception(f"Plugin {plugin_name} raised exception: {e}")
self.plugin_registry.record_error(plugin_name, str(e))
return None
async def _extract_with_builtin(
self,
filepath: str,
file_type: str
) -> Dict[str, Any]:
"""Extract using built-in extractors."""
try:
ext = Path(filepath).suffix.lower()
if ext in ['.png']:
data = await extract_png_metadata(
filepath, self.exiftool, self.ffprobe
)
elif ext in ['.webp', '.gif']:
data = await extract_webp_metadata(
filepath, self.exiftool, self.ffprobe
)
elif file_type == 'video':
data = await extract_video_metadata(
filepath, self.ffprobe
)
elif file_type == 'audio':
data = await extract_audio_metadata(
filepath, self.ffprobe
)
elif file_type == 'model3d':
data = await extract_3d_model_metadata(filepath)
else:
data = {"success": False, "error": "No extractor available"}
return data
except Exception as e:
logger.exception(f"Built-in extraction failed: {e}")
return {
"success": False,
"error": str(e),
"extractor": "builtin"
}
def get_plugin_stats(self) -> Dict[str, Any]:
"""Get plugin system statistics."""
return {
"loader": self.plugin_loader.get_stats(),
"registry": self.plugin_registry.get_stats(),
}
def enable_plugin(self, name: str) -> bool:
"""Enable a plugin by name."""
return self.plugin_registry.enable_plugin(name)
def disable_plugin(self, name: str) -> bool:
"""Disable a plugin by name."""
return self.plugin_registry.disable_plugin(name)
def list_plugins(self) -> list:
"""List all registered plugins with state."""
plugins = []
for extractor in self.plugin_loader.all_extractors:
state = self.plugin_registry.get_state(extractor.name)
plugins.append({
"name": extractor.name,
"version": extractor.metadata.version,
"enabled": state.enabled if state else True,
"priority": extractor.priority,
"extensions": extractor.supported_extensions,
"extractions": state.extraction_count if state else 0,
"errors": state.error_count if state else 0,
})
return plugins
2.2 Add Plugin Management Routes
File: mjr_am_backend/routes/handlers/plugins.py
"""Plugin management API routes."""
from aiohttp import web
from ...shared import Result, get_logger
logger = get_logger(__name__)
def register_plugin_routes(routes: web.RouteTableDef) -> None:
"""Register plugin management routes."""
@routes.get("/mjr/am/plugins/list")
async def list_plugins(request: web.Request) -> web.Response:
"""List all registered plugins."""
try:
services = request.app["services"]
metadata_service = services.get("metadata")
if not metadata_service:
return web.json_response({
"ok": False,
"error": "Metadata service not available"
})
plugins = metadata_service.list_plugins()
stats = metadata_service.get_plugin_stats()
return web.json_response({
"ok": True,
"data": {
"plugins": plugins,
"stats": stats
}
})
except Exception as e:
logger.exception("Failed to list plugins")
return web.json_response({
"ok": False,
"error": str(e)
}, status=500)
@routes.post("/mjr/am/plugins/{name}/enable")
async def enable_plugin(request: web.Request) -> web.Response:
"""Enable a plugin."""
try:
name = request.match_info["name"]
services = request.app["services"]
metadata_service = services.get("metadata")
if not metadata_service:
return web.json_response({
"ok": False,
"error": "Metadata service not available"
})
success = metadata_service.enable_plugin(name)
return web.json_response({
"ok": success,
"data": {"enabled": success}
})
except Exception as e:
logger.exception(f"Failed to enable plugin {name}")
return web.json_response({
"ok": False,
"error": str(e)
}, status=500)
@routes.post("/mjr/am/plugins/{name}/disable")
async def disable_plugin(request: web.Request) -> web.Response:
"""Disable a plugin."""
try:
name = request.match_info["name"]
services = request.app["services"]
metadata_service = services.get("metadata")
if not metadata_service:
return web.json_response({
"ok": False,
"error": "Metadata service not available"
})
success = metadata_service.disable_plugin(name)
return web.json_response({
"ok": success,
"data": {"enabled": not success}
})
except Exception as e:
logger.exception(f"Failed to disable plugin {name}")
return web.json_response({
"ok": False,
"error": str(e)
}, status=500)
@routes.post("/mjr/am/plugins/reload")
async def reload_plugins(request: web.Request) -> web.Response:
"""Reload all plugins (hot-reload)."""
try:
services = request.app["services"]
metadata_service = services.get("metadata")
if not metadata_service:
return web.json_response({
"ok": False,
"error": "Metadata service not available"
})
# Rediscover plugins
count = metadata_service.plugin_loader.discover_plugins()
return web.json_response({
"ok": True,
"data": {
"reloaded": count,
"message": f"Reloaded {count} plugins"
}
})
except Exception as e:
logger.exception("Failed to reload plugins")
return web.json_response({
"ok": False,
"error": str(e)
}, status=500)
Phase 3: Example Plugins (Week 3)
3.1 WanVideo Extractor Plugin
File: plugins/examples/wanvideo_extractor.py
"""
WanVideo Metadata Extractor Plugin
Extracts WanVideo-specific metadata from generated images.
WanVideo stores custom parameters in PNG text chunks.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
import json
import logging
# Import from plugin system
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
ExtractorMetadata,
)
logger = logging.getLogger(__name__)
class WanVideoExtractor(MetadataExtractorPlugin):
"""Extractor for WanVideo custom node metadata."""
@property
def name(self) -> str:
return "wanvideo_extractor"
@property
def supported_extensions(self) -> list[str]:
return ['.png', '.webp']
@property
def priority(self) -> int:
return 100 # High priority - runs before generic extractors
@property
def metadata(self) -> ExtractorMetadata:
return ExtractorMetadata(
name=self.name,
version="1.0.0",
author="Majoor Team",
description="Extract WanVideo metadata from PNG/WebP files",
homepage="https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager",
license="MIT"
)
async def extract(self, filepath: str) -> ExtractionResult:
"""Extract WanVideo metadata."""
try:
from PIL import Image
result_data = {
"prompt": None,
"negative_prompt": None,
"seed": None,
"steps": None,
"sampler": None,
"cfg": None,
"models": [],
"loras": [],
"custom_data": {
"wan_version": None,
"motion_bucket_id": None,
"fps": None,
"aug_level": None,
"source_type": None,
}
}
img = None
try:
img = Image.open(filepath)
# Check for WanVideo-specific PNG chunks
wan_data = None
if "wanv2" in img.info:
wan_data = json.loads(img.info["wanv2"])
elif "wanvideo" in img.info:
wan_data = json.loads(img.info["wanvideo"])
if wan_data:
result_data["custom_data"].update({
"wan_version": wan_data.get("version"),
"motion_bucket_id": wan_data.get("motion_bucket_id"),
"fps": wan_data.get("fps"),
"aug_level": wan_data.get("aug_level"),
"source_type": wan_data.get("source_type"),
})
# Also extract standard ComfyUI metadata
if "parameters" in img.info:
param_string = img.info["parameters"]
parsed = self._parse_parameters(param_string)
result_data.update(parsed)
img.close()
except Exception as e:
if img:
img.close()
raise
return self._create_success_result(result_data, confidence=0.95)
except Exception as e:
logger.exception(f"WanVideo extraction failed for {filepath}")
return self._create_error_result(str(e))
def _parse_parameters(self, param_string: str) -> Dict[str, Any]:
"""Parse ComfyUI parameter string."""
# Implementation for parsing standard ComfyUI metadata
# This is simplified - use existing parser from extractors.py
result = {}
try:
lines = param_string.split('\n')
for line in lines:
if ':' in line:
key, value = line.split(':', 1)
key = key.strip().lower()
value = value.strip()
if key == 'seed':
result['seed'] = int(value)
elif key == 'steps':
result['steps'] = int(value)
elif key == 'cfg':
result['cfg'] = float(value)
elif key == 'sampler':
result['sampler'] = value
except Exception:
pass
return result
3.2 rgthree Extractor Plugin
File: plugins/examples/rgthree_extractor.py
"""
rgthree Metadata Extractor Plugin
Extracts rgthree custom node metadata including comparison images
and advanced workflow data.
"""
from __future__ import annotations
from typing import Any, Dict
import json
import logging
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
ExtractorMetadata,
)
logger = logging.getLogger(__name__)
class RgthreeExtractor(MetadataExtractorPlugin):
"""Extractor for rgthree custom node metadata."""
@property
def name(self) -> str:
return "rgthree_extractor"
@property
def supported_extensions(self) -> list[str]:
return ['.png', '.json']
@property
def priority(self) -> int:
return 50
@property
def metadata(self) -> ExtractorMetadata:
return ExtractorMetadata(
name=self.name,
version="1.0.0",
author="Majoor Team",
description="Extract rgthree node metadata",
license="MIT"
)
async def extract(self, filepath: str) -> ExtractionResult:
"""Extract rgthree metadata."""
try:
result_data = {
"custom_data": {
"rgthree_nodes": [],
"comparison_images": [],
"context_mappings": [],
}
}
# Try to extract from PNG metadata
if filepath.lower().endswith('.png'):
result_data.update(
await self._extract_from_png(filepath)
)
# Try to extract from workflow JSON
elif filepath.lower().endswith('.json'):
result_data.update(
await self._extract_from_json(filepath)
)
return self._create_success_result(result_data, confidence=0.85)
except Exception as e:
logger.exception(f"rgthree extraction failed for {filepath}")
return self._create_error_result(str(e))
async def _extract_from_png(self, filepath: str) -> Dict[str, Any]:
"""Extract rgthree data from PNG."""
from PIL import Image
result = {"custom_data": {}}
try:
img = Image.open(filepath)
# Look for rgthree-specific metadata
if "rgthree" in img.info:
rgthree_data = json.loads(img.info["rgthree"])
result["custom_data"]["rgthree_nodes"] = rgthree_data.get("nodes", [])
img.close()
except Exception as e:
logger.debug(f"Failed to extract rgthree PNG data: {e}")
return result
async def _extract_from_json(self, filepath: str) -> Dict[str, Any]:
"""Extract rgthree data from workflow JSON."""
result = {"custom_data": {}}
try:
with open(filepath, 'r', encoding='utf-8') as f:
workflow = json.load(f)
# Look for rgthree nodes in workflow
nodes = workflow.get("nodes", [])
rgthree_nodes = [
n for n in nodes
if n.get("type", "").startswith("rgthree.")
]
result["custom_data"]["rgthree_nodes"] = rgthree_nodes
except Exception as e:
logger.debug(f"Failed to extract rgthree JSON data: {e}")
return result
Phase 4: Documentation (Week 4)
4.1 Plugin Development Guide
File: docs/PLUGIN_DEVELOPMENT_GUIDE.md
# Plugin Development Guide
## Quick Start
### 1. Create Plugin File
my_plugin.py
from mjr_am_backend.features.metadata.plugin_system.base import ( MetadataExtractorPlugin, ExtractionResult, )
class MyExtractor(MetadataExtractorPlugin): @property def name(self): return "my_extractor"
@property def supported_extensions(self): return ['.png']
@property def priority(self): return 50
async def extract(self, filepath): # Your extraction logic here return self._create_success_result({"key": "value"})
### 2. Install Plugin
Copy to plugin directory:
cp my_plugin.py ~/.comfyui/majoor_plugins/extractors/
### 3. Reload Plugins
In Assets Manager UI: Settings โ Plugins โ Reload
Or via API:
curl -X POST http://localhost:8188/mjr/am/plugins/reload
## Plugin API Reference
See `base.py` for full API documentation.
## Examples
See `plugins/examples/` for complete examples.
๐ Security Model
Plugin Validation
# mjr_am_backend/features/metadata/plugin_system/validator.py
import ast
import re
from pathlib import Path
from typing import List, Tuple
class PluginValidator:
"""Validates plugins for security issues."""
DANGEROUS_PATTERNS = [
r"__import__\s*\(\s*['\"]os['\"]\s*\)",
r"__import__\s*\(\s*['\"]subprocess['\"]\s*\)",
r"subprocess\.(call|run|Popen|check_output)",
r"os\.(system|popen|spawn|exec)",
r"eval\s*\(",
r"exec\s*\(",
r"compile\s*\(",
r"__builtins__",
r"importlib\.(reload|import_module)",
]
@classmethod
def validate(cls, plugin_path: Path) -> Tuple[bool, List[str]]:
"""
Validate a plugin file.
Returns:
(is_valid, list_of_warnings)
"""
warnings = []
try:
content = plugin_path.read_text(encoding='utf-8')
except Exception as e:
return False, [f"Cannot read file: {e}"]
# Pattern-based checks
for pattern in cls.DANGEROUS_PATTERNS:
if re.search(pattern, content):
warnings.append(f"Dangerous pattern detected: {pattern}")
# AST-based checks
try:
tree = ast.parse(content)
warnings.extend(cls._check_ast(tree))
except SyntaxError as e:
return False, [f"Syntax error: {e}"]
is_valid = len(warnings) == 0
return is_valid, warnings
@classmethod
def _check_ast(cls, tree: ast.AST) -> List[str]:
"""AST-based security checks."""
warnings = []
for node in ast.walk(tree):
# Check for dangerous imports
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in ['os', 'subprocess', 'ctypes']:
warnings.append(
f"Dangerous import: {alias.name}"
)
if isinstance(node, ast.ImportFrom):
if node.module in ['os', 'subprocess', 'ctypes']:
warnings.append(f"Dangerous import from: {node.module}")
return warnings
Sandboxed Execution
# mjr_am_backend/features/metadata/plugin_system/sandbox.py
import asyncio
import functools
from typing import Any, Callable, TypeVar
T = TypeVar('T')
class ExecutionSandbox:
"""
Sandboxed execution for plugin methods.
Limits:
- Timeout: 30 seconds max
- Memory: 512MB max (via resource limits on Unix)
- Filesystem: Read-only access to specified paths
"""
DEFAULT_TIMEOUT = 30.0 # seconds
@classmethod
async def run_with_timeout(
cls,
func: Callable,
*args,
timeout: float = DEFAULT_TIMEOUT,
**kwargs
) -> T:
"""Run function with timeout."""
try:
loop = asyncio.get_event_loop()
return await asyncio.wait_for(
loop.run_in_executor(None, functools.partial(func, *args, **kwargs)),
timeout=timeout
)
except asyncio.TimeoutError:
raise TimeoutError(f"Plugin execution timed out after {timeout}s")
@classmethod
def restrict_filesystem(
cls,
allowed_paths: list
) -> Callable:
"""
Decorator to restrict filesystem access.
Usage:
@restrict_filesystem(['/allowed/path'])
def extract(self, filepath):
...
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Validate all path arguments
for arg in args:
if isinstance(arg, str):
cls._validate_path(arg, allowed_paths)
for value in kwargs.values():
if isinstance(value, str):
cls._validate_path(value, allowed_paths)
return func(*args, **kwargs)
return wrapper
return decorator
@staticmethod
def _validate_path(path: str, allowed_paths: list) -> None:
"""Validate path is within allowed directories."""
from pathlib import Path
resolved = Path(path).resolve()
for allowed in allowed_paths:
try:
resolved.relative_to(Path(allowed).resolve())
return # Path is within allowed directory
except ValueError:
continue
raise PermissionError(
f"Access denied: {path} is outside allowed directories"
)
๐งช Testing Strategy
Unit Tests
# tests/plugins/test_plugin_loader.py
import pytest
from pathlib import Path
from mjr_am_backend.features.metadata.plugin_system.loader import PluginLoader
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MockExtractor(MetadataExtractorPlugin):
@property
def name(self): return "mock_extractor"
@property
def supported_extensions(self): return ['.png']
@property
def priority(self): return 50
async def extract(self, filepath):
return self._create_success_result({"test": "data"})
class TestPluginLoader:
@pytest.fixture
def loader(self, tmp_path):
return PluginLoader([tmp_path], auto_discover=False)
def test_register_extractor(self, loader):
extractor = MockExtractor()
loader.register_extractor(extractor)
assert "mock_extractor" in loader.extractor_names
assert loader.get_extractor_by_name("mock_extractor") is extractor
def test_get_extractor_by_extension(self, loader):
loader.register_extractor(MockExtractor())
extractor = loader.get_extractor("test.png")
assert extractor is not None
assert extractor.name == "mock_extractor"
def test_priority_ordering(self, loader):
class HighPriority(MockExtractor):
@property
def priority(self): return 100
class LowPriority(MockExtractor):
@property
def priority(self): return 10
loader.register_extractor(LowPriority())
loader.register_extractor(HighPriority())
# Should get high priority first
extractor = loader.get_extractor("test.png")
assert extractor.priority == 100
# tests/plugins/test_plugin_validator.py
from mjr_am_backend.features.metadata.plugin_system.validator import (
PluginValidator,
)
class TestPluginValidator:
def test_safe_plugin(self, tmp_path):
plugin_file = tmp_path / "safe_plugin.py"
plugin_file.write_text("""
class SafeExtractor:
pass
""")
is_valid, warnings = PluginValidator.validate(plugin_file)
assert is_valid is True
assert len(warnings) == 0
def test_dangerous_import(self, tmp_path):
plugin_file = tmp_path / "dangerous.py"
plugin_file.write_text("""
import os
os.system('echo hello')
""")
is_valid, warnings = PluginValidator.validate(plugin_file)
assert "Dangerous import: os" in warnings
Integration Tests
# tests/plugins/test_plugin_integration.py
import pytest
from mjr_am_backend.features.metadata.service import MetadataService
class TestPluginIntegration:
@pytest.fixture
def metadata_service(self, tmp_path):
plugin_dir = tmp_path / "plugins"
plugin_dir.mkdir()
# Create test plugin
plugin_file = plugin_dir / "test_extractor.py"
plugin_file.write_text("""
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class TestExtractor(MetadataExtractorPlugin):
@property
def name(self): return "test_extractor"
@property
def supported_extensions(self): return ['.png']
@property
def priority(self): return 100
async def extract(self, filepath):
return self._create_success_result({"from_plugin": True})
""")
return MetadataService(
exiftool=None,
ffprobe=None,
settings=None,
plugin_dirs=[plugin_dir]
)
@pytest.mark.asyncio
async def test_plugin_extraction(self, metadata_service, tmp_path):
# Create test file
test_file = tmp_path / "test.png"
test_file.write_bytes(b"fake png")
result = await metadata_service.extract_metadata(
str(test_file),
"image"
)
assert result.get("from_plugin") is True
๐ฆ Migration Guide
For Existing Code
Step 1: Update MetadataService Initialization
Before:
metadata_service = MetadataService(exiftool, ffprobe, settings)
After:
metadata_service = MetadataService(
exiftool,
ffprobe,
settings,
plugin_dirs=None # Uses defaults
)
Step 2: Update Extractor Calls
Before:
from .extractor_registry import get_extractor
extractor = get_extractor("png")
result = await extractor(filepath)
After:
# No change needed - service handles plugin routing
result = await metadata_service.extract_metadata(filepath, "image")
Step 3: Add Plugin Directory to .gitignore
# Plugin directories
plugins/*.pyc
plugins/__pycache__/
~/.comfyui/majoor_plugins/
output/_mjr_plugins/
For Plugin Developers
Migrating from Custom Forks
If you've created custom extractors by modifying the core codebase:
- Extract your logic into a plugin class
- Move to plugin directory (
~/.comfyui/majoor_plugins/extractors/) - Remove core modifications
- Test with plugin system
Example Migration:
Before (Core Modification):
# mjr_am_backend/features/metadata/extractors.py
async def extract_my_custom_metadata(filepath):
# Your custom logic
pass
After (Plugin):
# ~/.comfyui/majoor_plugins/extractors/my_extractor.py
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self): return "my_extractor"
@property
def supported_extensions(self): return ['.png']
@property
def priority(self): return 100
async def extract(self, filepath):
# Your custom logic here
return self._create_success_result({...})
๐ Performance Considerations
Plugin Loading
- Cold Start: 100-500ms for 10 plugins
- Hot Reload: 50-200ms
- Memory: ~5-10MB per plugin
Extraction Overhead
- Plugin Routing: <1ms per file
- Priority Sorting: Cached, negligible
- Fallback Chain: Adds 2-5ms if plugins fail
Optimization Strategies
# 1. Lazy loading - plugins loaded on first use
class LazyPluginLoader:
def __init__(self):
self._cache = {}
def get_extractor(self, filepath):
ext = Path(filepath).suffix
if ext not in self._cache:
self._cache[ext] = self._load_for_extension(ext)
return self._cache[ext]
# 2. Result caching
from functools import lru_cache
class CachedExtractor:
@lru_cache(maxsize=1000)
def extract_cached(self, filepath_hash):
...
# 3. Parallel extraction for batch operations
async def extract_batch(filepaths, max_concurrency=4):
semaphore = asyncio.Semaphore(max_concurrency)
async def extract_one(fp):
async with semaphore:
return await extractor.extract(fp)
return await asyncio.gather(*[extract_one(fp) for fp in filepaths])
๐ฏ Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| Plugin Load Time | <500ms | Startup logs |
| Extraction Success Rate | >95% | Plugin registry stats |
| Memory Overhead | <50MB | Process monitoring |
| Plugin Compatibility | 100% | Test suite |
| Security Violations | 0 | Validator logs |
๐ Appendices
A. Plugin Template
"""
[Plugin Name] Extractor
[Description]
"""
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
ExtractorMetadata,
)
class [PluginName]Extractor(MetadataExtractorPlugin):
@property
def name(self) -> str:
return "[plugin_name]_extractor"
@property
def supported_extensions(self) -> list[str]:
return ['.png', '.webp']
@property
def priority(self) -> int:
return 50
@property
def metadata(self) -> ExtractorMetadata:
return ExtractorMetadata(
name=self.name,
version="1.0.0",
author="Your Name",
description="Your description",
license="MIT"
)
async def extract(self, filepath: str) -> ExtractionResult:
try:
# Your extraction logic
data = {}
return self._create_success_result(data)
except Exception as e:
return self._create_error_result(str(e))
B. Environment Variables
# Plugin directories (colon-separated on Unix, semicolon on Windows)
MJR_PLUGIN_DIRS=~/.comfyui/majoor_plugins:/opt/majoor_plugins
# Plugin security
MJR_PLUGIN_VALIDATION=strict # strict, permissive, disabled
MJR_PLUGIN_TIMEOUT=30 # seconds
# Plugin logging
MJR_PLUGIN_LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
C. Troubleshooting
| Issue | Solution |
|---|---|
| Plugin not loading | Check logs for validation errors |
| Extraction fails | Verify file path permissions |
| Slow performance | Check plugin timeout settings |
| Memory leaks | Implement cleanup() method |
Document End
docs/PLUGIN_SYSTEM_DESIGN.md
PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md
Plugin System Implementation Summary
Date: March 16, 2026 Status: Implementation Complete (Ready for Integration)
๐ฆ Files Created
Core Plugin System (mjr_am_backend/features/metadata/plugin_system/)
| File | Lines | Description |
|---|---|---|
__init__.py | 20 | Package exports |
base.py | 180 | Abstract base class and dataclasses |
loader.py | 350 | Plugin discovery and loading |
registry.py | 300 | Runtime state and persistence |
validator.py | 250 | Security validation |
manager.py | 350 | High-level lifecycle management |
| Total | ~1,450 | 6 modules |
Documentation (docs/)
| File | Lines | Description |
|---|---|---|
PLUGIN_SYSTEM_DESIGN.md | 1,200 | Full architecture design |
PLUGIN_QUICK_REFERENCE.md | 400 | Quick reference guide |
| Total | ~1,600 | 2 documents |
Example Plugins (plugins/)
| File | Lines | Description |
|---|---|---|
__init__.py | 50 | Package init |
README.md | 150 | Plugin usage guide |
examples/wanvideo_extractor.py | 300 | WanVideo extractor example |
examples/custom_node_extractor.py | 400 | Template with documentation |
| Total | ~900 | 4 files |
Grand Total: ~4,000 lines of code
๐๏ธ Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Plugin System Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MetadataService (Modified) โ โ
โ โ - Uses PluginManager for extraction โ โ
โ โ - Falls back to built-in extractors โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PluginManager โ โ
โ โ - initialize() โ โ
โ โ - extract(filepath) โ โ
โ โ - list_plugins() โ โ
โ โ - reload() โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PluginLoader โ โ PluginRegistry โ โ
โ โ - discover() โ โ - enable/disable โ โ
โ โ - validate() โ โ - state tracking โ โ
โ โ - load modules โ โ - persistence โ โ
โ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Loaded Plugins โ โ
โ โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ โ
โ โ โ wanvideo โ โ rgthree โ โ custom โ โ โ
โ โ โ extractor โ โ extractor โ โ extractor โ โ โ
โ โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Plugin Interface
Abstract Base Class
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self) -> str: ...
@property
def supported_extensions(self) -> list[str]: ...
@property
def priority(self) -> int: ...
async def extract(self, filepath: str) -> ExtractionResult: ...
Plugin Lifecycle
- Discovery - Scan plugin directories for
.pyfiles - Validation - Security check (no eval, exec, os.system, etc.)
- Loading - Import module, instantiate extractors
- Registration - Register in loader and registry
- Runtime - Handle extraction requests
- Cleanup - Release resources on unload
๐ Security Features
Validation Checks
| Check | Description |
|---|---|
| Pattern-based | Blocks dangerous patterns (eval, exec, subprocess) |
| AST-based | Analyzes imports and function calls |
| File size | Max 1MB per plugin |
| Complexity | Warns on high complexity (>50 functions) |
Blocked Patterns
# Code execution
eval(), exec(), compile()
# OS access
os.system(), os.popen(), subprocess.*
# Network access
socket.*, requests.*, urllib.*
# Unsafe deserialization
pickle.load(), marshal.load()
Validation Modes
- strict - Fail on any warning (default)
- permissive - Fail only on critical issues
- disabled - No validation (not recommended)
๐ Features
Plugin Loader
- โ Auto-discovery from multiple directories
- โ Security validation before loading
- โ Priority-based extractor selection
- โ Hot-reload support
- โ Error tracking and reporting
- โ Plugin information tracking
Plugin Registry
- โ State persistence (JSON config)
- โ Enable/disable plugins
- โ Statistics tracking (extractions, errors, confidence)
- โ Performance metrics (avg extraction time)
- โ Import/export configuration
Plugin Manager
- โ Lifecycle management (init, shutdown)
- โ Extraction with fallback support
- โ Event hooks (on_extract, on_error)
- โ Statistics and monitoring
- โ Hot-reload capability
๐ง Integration Steps
Step 1: Modify MetadataService
# mjr_am_backend/features/metadata/service.py
from .plugin_system.manager import PluginManager
class MetadataService:
def __init__(self, exiftool, ffprobe, settings, plugin_dirs=None):
# Existing initialization
self.exiftool = exiftool
self.ffprobe = ffprobe
self.settings = settings
# NEW: Initialize plugin system
self.plugin_manager = PluginManager(
plugin_dirs=plugin_dirs,
config_path=INDEX_DIR_PATH / "plugin_config.json"
)
async def extract_metadata(self, filepath: str, file_type: str):
# NEW: Try plugin extractors first
result = await self.plugin_manager.extract(
filepath,
fallback_extractor=lambda fp: self._extract_with_builtin(fp, file_type)
)
if result.success:
return result.data
# Fallback to built-in extractors
return await self._extract_with_builtin(filepath, file_type)
Step 2: Add API Routes
# mjr_am_backend/routes/handlers/plugins.py
from aiohttp import web
def register_plugin_routes(routes: web.RouteTableDef):
@routes.get("/mjr/am/plugins/list")
async def list_plugins(request):
services = request.app["services"]
metadata = services.get("metadata")
plugins = metadata.plugin_manager.list_plugins()
return web.json_response({"ok": True, "data": plugins})
@routes.post("/mjr/am/plugins/{name}/enable")
async def enable_plugin(request):
name = request.match_info["name"]
services = request.app["services"]
metadata = services.get("metadata")
success = metadata.plugin_manager.enable_plugin(name)
return web.json_response({"ok": success})
@routes.post("/mjr/am/plugins/reload")
async def reload_plugins(request):
services = request.app["services"]
metadata = services.get("metadata")
count = await metadata.plugin_manager.reload()
return web.json_response({"ok": True, "data": {"reloaded": count}})
Step 3: Register Routes
# mjr_am_backend/routes/registry.py
from .handlers.plugins import register_plugin_routes
def register_all_routes():
# ... existing route registrations
register_plugin_routes(routes)
๐งช Testing Strategy
Unit Tests
# tests/plugins/test_plugin_loader.py
class TestPluginLoader:
def test_discover_plugins(self, tmp_path):
loader = PluginLoader([tmp_path])
count = loader.discover_plugins()
assert count > 0
def test_get_extractor_by_priority(self, tmp_path):
loader = PluginLoader([tmp_path], auto_discover=False)
loader.register_extractor(HighPriorityExtractor())
loader.register_extractor(LowPriorityExtractor())
extractor = loader.get_extractor("test.png")
assert extractor.priority == 100 # Highest priority
Integration Tests
# tests/plugins/test_plugin_integration.py
class TestPluginIntegration:
@pytest.mark.asyncio
async def test_plugin_extraction(self, metadata_service):
result = await metadata_service.extract_metadata("test.png", "image")
assert result.get("success") is True
๐ Performance Metrics
Plugin Loading
| Metric | Target | Actual |
|---|---|---|
| Cold start (10 plugins) | <500ms | ~300ms |
| Hot reload | <200ms | ~150ms |
| Memory per plugin | <10MB | ~5MB |
Extraction Overhead
| Metric | Target | Actual |
|---|---|---|
| Plugin routing | <1ms | ~0.5ms |
| Priority sorting | Cached | ~0ms |
| Fallback chain | <5ms | ~3ms |
๐ฏ Success Criteria
| Criterion | Status | Notes |
|---|---|---|
| Plugin interface defined | โ | Abstract base class complete |
| Plugin discovery working | โ | Auto-discovery from directories |
| Security validation | โ | Pattern + AST-based checks |
| Hot-reload support | โ | Reload without restart |
| State persistence | โ | JSON config file |
| Statistics tracking | โ | Extractions, errors, confidence |
| Documentation complete | โ | Design doc + quick reference |
| Example plugins | โ | WanVideo + template |
| Tests written | โณ | Ready to implement |
| Integration complete | โณ | Requires MetadataService modification |
๐ Next Steps
Immediate (Required for Functionality)
- Modify MetadataService to use PluginManager
- Add API routes for plugin management
- Register routes in registry.py
- Update deps.py to initialize plugin system
Short-Term (Enhancements)
- Write unit tests for all plugin modules
- Create frontend UI for plugin management
- Add more example plugins (rgthree, ControlNet, etc.)
- Document plugin API in main README
Long-Term (Future)
- Plugin marketplace - Share plugins via PyPI
- Plugin versioning - Automatic updates
- Sandboxed execution - Better isolation
- Performance profiling - Identify bottlenecks
๐ Related Documents
docs/PLUGIN_SYSTEM_DESIGN.md- Full architecture designdocs/PLUGIN_QUICK_REFERENCE.md- Quick reference guideplugins/README.md- Plugin installation guideplugins/examples/- Example plugins
๐ฆ Distribution
Plugin Package Structure
majoor-my-plugin/
โโโ setup.py
โโโ README.md
โโโ my_plugin/
โโโ __init__.py
โโโ extractor.py
# Installation
pip install majoor-my-plugin
PyPI Publishing
# setup.py
from setuptools import setup
setup(
name="majoor-wanvideo-extractor",
version="1.0.0",
py_modules=["wanvideo_extractor"],
entry_points={
"majoor.plugins": [
"wanvideo = wanvideo_extractor:WanVideoExtractor",
]
},
)
Implementation Status: โ Core system complete, ready for integration
Estimated Integration Time: 2-4 hours
Risk Level: Low (isolated module, backward compatible)
docs/PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md
PRIVACY_OFFLINE.md
Majoor Assets Manager - Privacy & Offline Guide
Version: 2.4.5 Last Updated: April 7, 2026
Overview
This page explains what Majoor Assets Manager processes locally, what can require network access, what the visible tokens are used for, and what "offline" means in practice.
Short Answer
- AI inference is intended to run locally on your machine.
- Images and prompts are not uploaded to a hosted cloud inference API for semantic search, captions, similarity, or prompt alignment.
- Internet access is mainly required for the initial HuggingFace model download.
- Once the required models are cached locally, AI features can work offline.
- No usage telemetry is intentionally sent to the developer.
What Stays Local
After models are available locally, these features run inside your local ComfyUI / Majoor process:
- semantic search
- find similar
- prompt alignment
- caption generation
- auto-tagging
This means Majoor does not rely on a hosted remote AI inference backend for those features.
What Uses The Network
Model download
AI features may download models from HuggingFace on first use. Those model files are then cached locally.
Typical cached model location:
~/.cache/huggingface/hub/
Optional HuggingFace token
The HuggingFace token is optional and only affects HuggingFace Hub access:
- better download reliability
- better Hub rate limits
- access to model download endpoints when needed
It is not a hosted AI inference key.
Remote access security
The Majoor API token is unrelated to HuggingFace. It protects remote write access to the local Majoor backend when ComfyUI is exposed on LAN, behind a reverse proxy, or through a tunnel.
It is not used to send prompts or images to an external AI service.
Token Clarification
Majoor exposes two token concepts that are easy to confuse:
HuggingFace Token
- optional
- used for model downloads from HuggingFace Hub
- improves download access and rate limits
- not used as a cloud inference token
Majoor API Token
- used for securing remote write operations
- applies to the local Majoor backend
- relevant when ComfyUI is reachable remotely
- not used for AI inference uploads
Offline Use
What works offline
AI features can work offline after the required models already exist in the local HuggingFace cache.
Non-AI Majoor features do not depend on HuggingFace model downloads.
What does not work offline on first use
If the model files are not already cached locally, first-time AI bootstrap requires network access to download them.
Important Nuance
The privacy statement here is specifically about:
- where inference runs
- whether prompts or images are uploaded for AI processing
That is different from saying there is zero upstream trust surface.
Model loading still depends on local HuggingFace / Transformers packages and downloaded model files. Some compatibility loaders may rely on upstream model packaging behavior. So the correct claim is:
- local inference, no hosted cloud upload of prompts/images for AI processing
not:
- zero external code or zero trust surface
Recommended Wording For Users
You can summarize Majoor's behavior like this:
AI features run locally on the user's machine. Prompts and images are not sent to a hosted cloud inference service for semantic search, similarity, captions, or alignment. Internet access is mainly needed only for the initial HuggingFace model download, and once models are cached locally the AI features can be used offline.
Related Documents
docs/PRIVACY_OFFLINE.md
RATINGS_TAGS_COLLECTIONS.md
Majoor Assets Manager - Ratings, Tags & Collections Guide
Version: 2.4.5 Last Updated: April 5, 2026
Overview
The Majoor Assets Manager provides powerful organizational tools including ratings, tags, and collections to help you manage and organize your generated assets. This guide covers all features related to asset organization and categorization.
Recent highlights: Improved bulk operations, better Windows Explorer rating sync, and collection performance optimizations.
Rating System
Understanding Ratings
The rating system allows you to assign star ratings from 0 to 5 stars to your assets:
- 0 Stars: Poor quality or unusable
- 1 Star: Below average quality
- 2 Stars: Average quality
- 3 Stars: Good quality
- 4 Stars: Very good quality
- 5 Stars: Excellent quality
Setting Ratings
Via Context Menu
- Right-click on an asset card
- Select "Rate" from the context menu
- Choose the desired star rating (0-5)
- The rating is saved immediately
Via Keyboard Shortcuts
- Select an asset card
- Press the corresponding number key (0-5)
- The rating is applied instantly
Via Rating Editor
- Click on the rating stars directly on the asset card
- Select the desired number of stars
- The rating is saved automatically
Rating Features
Bulk Rating
- Select multiple assets using Ctrl/Cmd+click or Shift+click
- Apply the same rating to all selected assets
- Right-click and choose "Rate" to apply to all selections
Rating Filters
- Filter assets by minimum rating threshold
- Show only assets rated 3 stars or higher
- Quickly find your best results using rating filters
Rating Persistence
- Ratings are stored in the SQLite index database
- Ratings persist between ComfyUI sessions
- Ratings can be synchronized to file metadata (when external tools available)
Rating Synchronization
When ExifTool is available, ratings can be synced to file metadata:
- Ratings stored in file metadata for portability
- Maintains ratings when files are moved/copied
- Compatible with other applications that read ratings
- Enabled/disabled via settings
Windows rating mapping (why 99?)
When sync is enabled, the manager writes both star ratings (0โ5) and Windows-style percent fields (RatingPercent / Microsoft:SharedUserRating) for best Explorer compatibility.
- 0โ โ 0
- 1โ โ 1
- 2โ โ 25
- 3โ โ 50
- 4โ โ 75
- 5โ โ 99 (many Windows handlers treat 99 as โmaxโ)
Tagging System
Understanding Tags
Tags are customizable keywords that help categorize and organize your assets:
- Free-form text labels (e.g., "character", "landscape", "fantasy")
- Multiple tags per asset
- Hierarchical tagging support (e.g., "style:anime", "color:warm")
- Searchable across all assets
Adding Tags
Via Context Menu
- Right-click on an asset card
- Select "Edit Tags" from the context menu
- Type tags separated by commas or spaces
- Press Enter to save
Via Tags Editor
- Click on an asset to select it
- Open the details sidebar (press 'D')
- Find the tags section
- Add or remove tags as needed
Bulk Tagging
- Select multiple assets
- Right-click and choose "Edit Tags"
- Add tags that apply to all selected assets
- Tags are applied to all selected items
Managing Tags
Tag Suggestions
- System suggests previously used tags
- Intelligent autocomplete for faster tagging
- Recently used tags appear at the top of suggestions
Tag Validation
- Prevents duplicate tags on the same asset
- Validates tag format and content
- Sanitizes special characters when needed
Tag Search
- Search for assets by specific tags
- Combine tag searches with other filters
- Use partial tag names for broader results
Tag Features
Tag Hierarchy
- Use colons to create hierarchical tags: "subject:person:face"
- Organize tags in logical groups
- Facilitate more granular categorization
Tag Colors
- Visual indication of different tag types
- Customizable tag appearance
- Consistent color coding across the interface
Tag Statistics
- View tag frequency across your collection
- Identify most commonly used tags
- Analyze your tagging patterns
Tag Synchronization
When ExifTool is available, tags can be synced to file metadata:
- Tags stored in file metadata for portability
- Maintains tags when files are moved/copied
- Compatible with other applications that read tags
- Enabled/disabled via settings
Collections System
Understanding Collections
Collections are user-created groups of assets that can span multiple directories:
- Named groups of related assets
- Can include assets from different scopes (Outputs, Inputs, Custom)
- Persistent storage in JSON format
- Shareable between users
Creating Collections
From Selected Assets
- Select one or more assets
- Right-click and choose "Add to Collection"
- Select an existing collection or create a new one
- Name your new collection appropriately
- Assets are added to the collection
From Search Results
- Apply search and filters to find desired assets
- Select all results (Ctrl+A or Cmd+A)
- Right-click and choose "Add to Collection"
- Create a new collection for the filtered results
Empty Collections
- Right-click in the Collections tab
- Choose "Create New Collection"
- Name your collection
- Add assets later as needed
Managing Collections
Collection Operations
- Rename: Change collection name via context menu
- Delete: Remove entire collection (doesn't delete assets)
- Clear: Remove all items from collection (doesn't delete assets)
- Export: Save collection as JSON file for sharing
- Import: Load collection from JSON file
Item Management
- Add Items: Add assets to existing collections
- Remove Items: Remove specific assets from collections
- Bulk Operations: Add/remove multiple items at once
- Duplicate Prevention: Automatic detection of duplicates
Collection Features
Smart Collections
- Dynamic collections based on criteria
- Automatically updated as new assets match criteria
- Based on tags, ratings, date ranges, or other attributes
Nested Collections
- Collections within collections (hierarchical)
- Organize large collections into subcategories
- Flexible organization structures
Collection Sharing
- Export collections as JSON files
- Import collections from other users
- Share curated sets of assets
- Collaborative collection building
Collection Limits
- Maximum 50,000 items per collection (configurable)
- Performance optimization for large collections
- Warning when approaching limits
- Suggestions for splitting large collections
Collection Views
Grid View
- Thumbnail display of collection contents
- Visual browsing of assets
- Consistent with main interface design
List View
- Detailed information display
- Sortable columns (name, date, size, rating)
- More information per asset
Compact View
- Dense layout for many items
- Efficient space utilization
- Quick scanning of large collections
Integration with Other Features
Search Integration
- Search within specific collections
- Cross-collection search capability
- Filter search results by collection membership
- Search for assets across all collections
Filtering Integration
- Filter by collection membership
- Combine collection filters with other filters
- Show only assets from specific collections
- Exclude assets from certain collections
Viewer Integration
- View collection context in viewer
- Navigate between collection items
- Show collection information in metadata panel
- Maintain collection context during comparisons
Rating and Tag Integration
- Apply ratings/tags to entire collections
- Filter collections by ratings/tags
- Bulk operations on collection contents
- Maintain rating/tag statistics per collection
Best Practices
Rating Best Practices
- Establish consistent rating criteria
- Use ratings consistently across similar assets
- Regularly review and adjust ratings as needed
- Use rating filters to quickly find quality assets
Tagging Best Practices
- Develop a consistent tagging vocabulary
- Use specific but not overly granular tags
- Maintain tag hierarchy for organization
- Regularly review and consolidate similar tags
- Use tags that will be meaningful later
Collection Best Practices
- Create collections based on clear criteria
- Use descriptive names for collections
- Regularly clean up unused collections
- Split large collections into more manageable groups
- Share valuable collections with others
Performance Considerations
Large Collections
- Collections with many items may take longer to load
- Interface remains responsive during loading
- Pagination helps manage large collections
- Consider splitting very large collections
Tag Performance
- Many tags per asset don't significantly impact performance
- Tag search remains fast regardless of quantity
- Regular cleanup of unused tags improves performance
Rating Performance
- Rating operations are nearly instantaneous
- Rating filters apply quickly to large datasets
- Bulk rating operations are optimized
Troubleshooting
Rating Issues
- Ratings not saving: Check database permissions
- Ratings not displaying: Refresh the view or rescan
- Sync issues: Verify ExifTool installation for file sync
Tagging Issues
- Tags not appearing: Check for typos or special characters
- Tag search not working: Ensure proper tag format
- Sync issues: Verify ExifTool installation for file sync
Collection Issues
- Collection not saving: Check write permissions to index directory
- Items not adding: Verify collection limits haven't been reached
- Collection not loading: Check JSON file integrity
Common Solutions
- Refresh Index: Use Ctrl/Cmd+S to rescan and refresh
- Clear Cache: Clear browser cache if interface seems stuck
- Check Logs: Look at ComfyUI console for detailed error messages
- Database Maintenance: Run optimization tools periodically
Advanced Features
Tag Expressions
- Complex tag queries using boolean logic
- Combine tags with AND/OR operations
- Parentheses for complex expressions
- Negation operators for exclusions
Rating Statistics
- View rating distributions across collections
- Track rating trends over time
- Identify most/least rated assets
- Generate reports on rating patterns
Collection Analytics
- View collection usage statistics
- Track most accessed collections
- Identify underutilized collections
- Analyze collection overlap and relationships
Automation Possibilities
- Script-based tag assignment
- Automated rating based on criteria
- Scheduled collection maintenance
- Integration with external tools
Security & Privacy
Local Storage
- Ratings and tags stored locally
- No external data transmission
- Full control over your organizational data
- Backup your index database regularly
File Metadata
- Optional synchronization to file metadata
- Controlled via settings
- Respects file permissions
- Preserves existing file metadata
Ratings, Tags & Collections Guide Version: 1.0 Last Updated: April 5, 2026
docs/RATINGS_TAGS_COLLECTIONS.md
SEARCH_FILTERING.md
Majoor Assets Manager - Search & Filtering Guide
Version: 2.4.5 Last Updated: April 5, 2026
Overview
The Majoor Assets Manager provides powerful search and filtering capabilities to help you quickly find and organize your assets. This guide explains all the search and filtering features available in the extension.
Recent highlights: Enhanced workflow type filters, file size filtering, resolution filtering, and inline attribute filters.
Full-Text Search
How It Works
The Assets Manager uses SQLite FTS5 with BM25 ranking to provide fast and accurate search results. The search indexes:
- Filenames and file extensions
- Embedded metadata (prompts, models, parameters)
- Workflow information
- Tags and ratings
- File content where applicable
Basic Search
- Locate the search bar at the top of the Assets Manager interface
- Type any text you want to search for
- Press Enter or wait for results to appear automatically
- Results are displayed with relevance ranking
Search Syntax
Simple Terms
- Type any word or phrase to search for it
- Example:
landscapefinds all assets with "landscape" in their metadata
Exact Phrases
- Use quotes to search for exact phrases
- Example:
"fantasy character"finds assets with that exact phrase
Multiple Terms
- Search for multiple terms simultaneously
- Results containing more terms rank higher
- Example:
portrait fantasy digitalfinds assets matching any or all terms
Search Scopes
Search works across all available scopes:
- Outputs: Search in your ComfyUI output directory
- Inputs: Search in your ComfyUI input directory
- Custom: Search in user-defined directories
- Collections: Search within saved collections
Filtering Options
Kind Filter
The kind filter dropdown exposes the four file buckets that the backend consistently understands: image, video, audio, and model3d. Pick the bucket that best fits your search and the grid will stay in sync with the backend filters.
If you need to target a specific extension (for example ext:webp or ext:gif), type the ext:<extension> token directly in the search bar. The server parses these tokens and applies them as extension filters even if the dropdown remains on โAllโ or a different category.
Rating Filter
Filter by star ratings (0-5 stars):
- Click the rating filter dropdown
- Select minimum rating threshold
- Only assets with equal or higher ratings will be displayed
- Useful for quickly finding your best results
Workflow Filter
Toggle to show only assets with embedded workflow information:
- Shows only assets that contain workflow data
- Helpful when looking for specific generation parameters
- Particularly useful for reproducible results
Date Filters
Filter assets by creation or modification dates:
- Open the date filter panel
- Select date range using calendar pickers
- Apply the filter to narrow results
- Useful for finding recent work or historical assets
UTC note: The server evaluates date filters using UTC boundaries so every โtodayโ/โthis weekโ span is consistent regardless of the machine timezone.
Advanced Filters
Inline Attribute Filters
Type tokens like rating:5 or ext:png directly into the search input to add filters without touching the dropdowns. The backend recognizes these attribute-style tokens, so the grid applies the extra constraint even when the UI controls stay untouched.
Hide PNG Siblings
When working with video generations that include PNG previews:
- Enable this option to hide PNG files when video previews exist
- Reduces clutter from duplicate content in different formats
- Keeps your view focused on the primary asset type
Sort Options
Sort results by various criteria:
- Relevance: Based on search term matching (default)
- Name: Alphabetical by filename
- Date: Chronological by creation/modification date
- Size: By file size (ascending/descending)
- Rating: By star rating (ascending/descending)
- Kind: By file type
Search Tips & Best Practices
Effective Search Strategies
- Be Specific: Use specific terms for better results
- Instead of:
character - Try:
"fantasy warrior"or"cyberpunk city"
- Combine Filters: Use multiple filters together
- Rating + Date + Kind filters can quickly narrow results
- Use Quotes: For exact phrase matching
"negative prompt: ugly"finds assets with that exact phrase
- Leverage Metadata: Search for model names, samplers, or parameters
model:SDXLfinds assets generated with SDXLsteps:30finds assets with 30 sampling steps
Performance Considerations
- First search in a scope may take longer as indexing occurs
- Subsequent searches are faster due to cached indexes
- Very large directories may take time to scan initially
- Results are loaded in pages for smooth performance
Search Result Information
Each search result displays:
- Thumbnail: Visual preview of the asset
- Filename: Original file name
- Extension Badge: File type with collision indicator (e.g.,
PNG+for duplicates) - Rating: Star rating if assigned
- Tags: Preview of assigned tags
- Metadata: Brief generation information when available
Using Filters Together
Sequential Filtering
Apply filters in sequence for refined results:
- Start with a broad search term
- Apply kind filter to narrow by file type
- Use rating filter to show only quality results
- Apply date filter to focus on timeframe
- Sort results by preference
Resetting Filters
- Click the "Clear All Filters" button to reset all active filters
- Or individually disable filters using their respective controls
- Search terms remain unless cleared separately
Collections Integration
Searching Within Collections
- Switch to the Collections scope to search within saved collections
- Individual collections can be searched independently
- Cross-collection searches are also supported
Adding Filtered Results to Collections
- Apply your desired search and filters
- Select the results you want to save
- Right-click and choose "Add to Collection"
- Create a new collection or add to existing one
- The filtered results are preserved in the collection
Troubleshooting Search Issues
No Results Found
- Check spelling of search terms
- Try broader terms or synonyms
- Verify the correct scope is selected
- Ensure the directory has been scanned (trigger with Ctrl/Cmd+S)
Too Many Results
- Add more specific search terms
- Apply kind filters to narrow by file type
- Use rating filters to show only quality results
- Apply date filters to focus on recent work
Slow Search Performance
- First search in a large directory takes longer
- Subsequent searches are faster with cached indexes
- Consider excluding very large directories that don't contain relevant assets
- Check system resources and close other applications if needed
Missing Metadata in Search
- Verify external tools (ExifTool, FFprobe) are installed
- Check file permissions for metadata access
- Some file formats may not contain embeddable metadata
- Re-index the directory if metadata should be present
Advanced Search Techniques
Boolean Operations
While direct boolean operators aren't supported, you can achieve similar results:
- AND: Include multiple terms (results must contain all terms)
- OR: Use broader terms that match any of your interests
- NOT: Manually exclude unwanted results after searching
Pattern Recognition
- Search for common patterns in your workflow names
- Use recurring artist/model names as search terms
- Look for specific parameter combinations in metadata
Workflow-Based Searching
- Search for specific sampler names:
euler,ddim,dpm++ - Find results from particular models:
realisticvision,dreamshaper - Locate generations with specific settings:
cfg_scale:7,steps:40
Search Statistics
Result Count Display
The interface shows:
- Total assets in current scope
- Number of assets matching current filters
- Current page and total pages
- Time taken for the search operation
Performance Metrics
- Search duration is displayed for each query
- Indexing status shows when background processes are running
- Memory usage indicators help monitor performance
Search & Filtering Guide Version: 1.0 Last Updated: April 5, 2026
docs/SEARCH_FILTERING.md
SECURITY_ENV_VARS.md
Majoor Assets Manager - Security Model & Environment Variables Guide
Version: 2.4.5 Last Updated: April 7, 2026
Overview
The Majoor Assets Manager implements a comprehensive security model to protect your system while providing powerful asset management capabilities. This guide covers the security architecture, threat models, and security-related environment variables.
Recent highlights: Enhanced API token support, improved Safe Mode controls, better remote access configuration, and a configurable index database directory for network-drive setups.
Security Architecture
Defense Layers
The Assets Manager employs multiple layers of security:
Application Layer Security
- Input validation and sanitization
- Path containment and validation
- File type verification
- Access control mechanisms
Network Layer Security
- CSRF protection on state-changing endpoints
- Origin validation for sensitive requests
- Rate limiting on expensive operations
- Secure communication protocols
File System Security
- Root containment validation
- Path traversal prevention
- Permission checking
- Symlink handling controls
Trust Model
- User Input: All user input is validated and sanitized
- File System: Only allowed directories are accessible
- External Tools: Tools run with limited privileges
- Network: Requests validated against origin policies
Authentication & Authorization
Write access guard (recommended)
ComfyUI is often used locally, but it can be exposed via --listen, reverse proxies, or tunnels. To reduce risk, Majoor blocks destructive/write operations from non-local clients by default.
- Default behavior (no token configured): allow write operations only from loopback clients (
127.0.0.1/::1/localhost). - Token behavior: if
MAJOOR_API_TOKEN(orMJR_API_TOKEN) is set, remote write operations require it. - Loopback keeps the compatibility behavior by default.
- Send the token via
X-MJR-Token: <token>orAuthorization: Bearer <token>. - Remote first-run bootstrap: if no persistent API token is configured yet, an authenticated ComfyUI user can bootstrap the initial remote session token automatically over HTTPS without setting
MAJOOR_ALLOW_BOOTSTRAP=1. - Loopback recovery bootstrap: local browser sessions on loopback can recover a Majoor write session after restart/new tab without a separate ComfyUI sign-in step.
- Legacy hash-only recovery: if an older install only has a stored token hash, the first successful bootstrap rotates to a new persistent plaintext token automatically so future sessions can recover cleanly.
- Strict local auth: set
MAJOOR_REQUIRE_AUTH=1if you want loopback writes to require the token too.
Overrides (use with care)
MAJOOR_REQUIRE_AUTH=1forces token auth even for loopback (requiresMAJOOR_API_TOKEN).MAJOOR_ALLOW_REMOTE_WRITE=1allows remote write operations without a token (unsafe).MAJOOR_ALLOW_BOOTSTRAP=1temporarily enables initial/bootstrap-tokenprovisioning for remote clients even when no authenticated ComfyUI user context is available. Disable it after first successful bootstrap.
Where to set these variables
Set these on the machine that runs ComfyUI, in the same shell, batch file, service definition, or launcher script that starts python main.py.
Recent builds expose the main remote-access controls directly in Majoor Settings as well. For many home/LAN setups, a signed-in ComfyUI user can now complete the initial remote setup from the UI without manually editing startup scripts.
Recommended UI path for a trusted LAN setup:
- Open Majoor Settings.
- Enable
Recommended Remote LAN Setup. - Confirm the browser session is authorized.
That preset generates a strong token if needed, forces token auth for writes, keeps anonymous remote writes disabled, automatically enables HTTP token transport only when the current session is a non-loopback HTTP LAN connection, and injects the token into the current browser session immediately.
Majoor also shows the current session state directly inside Assets Manager through the runtime widget, with a Write auth: line such as Write auth: active ...ABCD.
Windows batch example:
@echo off
set MAJOOR_API_TOKEN=change-this-to-a-long-random-secret
set MJR_AM_INDEX_DIRECTORY=C:\mjr_index
cd /d "C:\path\to\ComfyUI"
python main.py --listen 0.0.0.0 --port 8188
Linux/macOS shell example:
export MAJOOR_API_TOKEN="change-this-to-a-long-random-secret"
export MJR_AM_INDEX_DIRECTORY=/var/local/mjr_index
cd /path/to/ComfyUI
python main.py --listen 0.0.0.0 --port 8188
If you change the value after ComfyUI is already running, restart ComfyUI so the new environment is loaded.
Client-side token handling
Set the same secret inside ComfyUI's Settings modal at Security -> Majoor: API Token if you want a fixed shared token across multiple browsers/devices.
Runtime behavior:
- the current browser session keeps its active write token in
sessionStorage(tab/session scoped) - bootstrap/rotate flows can also set the companion
httpOnlycookiemjr_write_token - the settings API does not expose the plaintext token after save; it only reports non-secret status fields such as
token_configuredandtoken_hint - browser-local settings intentionally scrub the plaintext token after it has been pushed into the active session
Safe Mode (default enabled)
Safe Mode adds an explicit opt-in layer for operations that modify files or user metadata.
MAJOOR_SAFE_MODE(default: enabled)- Set
MAJOOR_SAFE_MODE=0to disable Safe Mode. MAJOOR_ALLOW_WRITE=1- Allows rating/tags writes while Safe Mode is enabled.
MAJOOR_ALLOW_DELETE=1- Enables asset deletion (disabled by default).
MAJOOR_ALLOW_RENAME=1- Enables asset rename (disabled by default).
MAJOOR_ALLOW_OPEN_IN_FOLDER=1- Enables
/mjr/am/open-in-folder(disabled by default).
Access Control
- File access limited to ComfyUI's allowed directories
- No direct access to system files outside allowed paths
- Permission inheritance from ComfyUI's file system access
- No elevation of privileges possible
CSRF Protection
Request Validation
All state-changing endpoints require additional validation:
XMLHttpRequest Header
- Requests must include
X-Requested-With: XMLHttpRequest - Prevents simple form submissions from external sites
- Standard technique for AJAX endpoint protection
CSRF Token Alternative
- Alternative validation using
X-CSRF-Tokenheader - Provides fallback for different client implementations
- Tokens validated against session state
Origin Validation
- When
Originheader is present, it's validated againstHost - Prevents cross-origin requests from unauthorized domains
- Protects against certain types of cross-site attacks
Rate Limiting
Per-Client Limits
Rate limiting is implemented on expensive endpoints:
Affected Endpoints
- Search operations (
/mjr/am/search) - Index scanning (
/mjr/am/scan) - Metadata extraction (
/mjr/am/metadata) - Batch ZIP operations (
/mjr/am/batch-zip)
Limit Configuration
- Implemented in-memory per-client
- Client identity based on IP address
- Configurable thresholds for different operations
- Resets after specified time intervals
Trusted Proxy Support
X-Forwarded-Forheader honored only from trusted proxies- Controlled by
MAJOOR_TRUSTED_PROXIESenvironment variable - Prevents IP spoofing in proxy environments
Path Safety & Containment
Root Validation
All file operations validate root containment:
Allowed Roots
- ComfyUI output directory
- ComfyUI input directory
- Custom roots defined by user
- Collections directory
Path Validation Process
- Normalize the requested path
- Resolve to absolute path
- Verify path starts with allowed root
- Reject if outside allowed boundaries
Symlink Handling
- Symlinks are handled carefully to prevent directory traversal
- Optional symlink support via
MJR_ALLOW_SYMLINKSenvironment variable - Disabled by default for security
- When enabled, still validates final resolved path
Path Traversal Prevention
- Input paths are normalized to prevent
../attacks - Absolute path resolution ensures final location is verified
- Multiple validation layers prevent bypass attempts
File Operation Security
Safe File Operations
- All file operations go through security validation
- Read operations limited to allowed directories
- Write operations limited to index and temporary directories
- Delete operations require additional confirmation
Batch ZIP Security
- ZIP building streams directly from file handles
- Prevents TOCTOU (Time-of-Check-Time-of-Use) race conditions
- No temporary file copies created during ZIP building
- Automatic cleanup of temporary ZIP files
File Type Validation
- File types validated before processing
- Dangerous file types blocked from certain operations
- MIME type checking where possible
- Extension validation for security-sensitive operations
Network Security
HTTP Security Headers
Applied to Majoor API endpoints only:
Content Security Policy
Content-Security-Policy: default-src 'none'- Prevents execution of injected content
- Applies only to API responses
Content Type Options
X-Content-Type-Options: nosniff- Prevents MIME-type confusion attacks
- Ensures content is treated as declared type
Frame Options
X-Frame-Options: DENY- Prevents embedding in iframes
- Protects against clickjacking
Referrer Policy
Referrer-Policy: strict-origin-when-cross-origin- Limits referrer information leakage
- Balances privacy with functionality
API Versioning Security
- Versioned routes redirect to canonical endpoints
/mjr/am/v1/...redirects to/mjr/am/...using 308- Maintains security headers across versions
- Preserves method and query parameters
External Tool Security
Tool Execution Safety
- External tools (ExifTool, FFprobe) run with limited privileges
- Input validation before passing to external tools
- Timeout enforcement to prevent hanging operations
- Output validation after tool execution
Path Validation for Tools
- File paths validated before passing to external tools
- No direct shell execution, only tool invocation
- Input sanitization for all tool parameters
- Tool output parsing with validation
Tool Location Security
- Tools can be specified via environment variables
- Default to system PATH for security
- No arbitrary tool execution possible
- Tool paths validated at startup
Environment Variable Security
Secure Configuration
Environment variables provide secure configuration without code changes:
Configuration Validation
- Environment variables validated at startup
- Invalid values fall back to defaults safely
- No unsafe defaults used
- Clear error messages for invalid configurations
Sensitive Information Handling
- No passwords or secrets stored in environment
- File paths validated for security
- No direct system command execution via environment
- Configuration changes logged appropriately
Trusted Proxy Configuration
MAJOOR_TRUSTED_PROXIEScontrols proxy trust- Default:
127.0.0.1,::1(localhost only) - Prevents IP spoofing in proxy environments
- Critical for rate limiting effectiveness
Threat Model
Identified Threats
Information Disclosure
- Unauthorized access to file metadata
- Exposure of file system structure
- Leakage of generation parameters
- Mitigation: Path validation, access controls
Path Traversal
- Access to files outside allowed directories
- Reading system files or other users' data
- Writing to unauthorized locations
- Mitigation: Path containment, normalization
Resource Exhaustion
- Denial of service through expensive operations
- Memory exhaustion via large metadata
- Database locking through concurrent operations
- Mitigation: Rate limiting, timeouts, resource limits
Code Execution
- Arbitrary command execution through file operations
- Injection through file names or paths
- Malicious file content processing
- Mitigation: Input validation, sandboxing
Cross-Site Attacks
- CSRF attacks on API endpoints
- XSS through metadata injection
- Clickjacking of interface elements
- Mitigation: CSRF tokens, security headers
Attack Vectors
Direct API Access
- Unauthorized access to HTTP endpoints
- Brute force attempts on protected endpoints
- Protection: Origin validation, rate limiting
File System Manipulation
- Creation of malicious file names
- Symlink attacks in custom roots
- Protection: Path validation, containment
Input Injection
- Malformed metadata in files
- Special characters in file names
- Protection: Input sanitization, validation
Security Monitoring
Request Logging
- Optional request logging via observability settings
- Logs include client IP, endpoint, and timestamp
- Sensitive data not logged (file contents, metadata)
- Helpful for detecting unusual access patterns
Error Handling
- Secure error messages that don't leak system information
- Internal errors logged but not exposed to users
- Validation errors provide guidance without revealing internals
- Stack traces hidden from end users
Audit Trail
- File operations logged with appropriate detail
- Configuration changes tracked where applicable
- Access patterns monitored for anomalies
- Retention policies for security logs
Security Best Practices
Deployment Security
Network Isolation
- Run ComfyUI behind appropriate firewalls
- Limit network access to trusted users
- Use VPN or other secure access methods
- Monitor network traffic for anomalies
File System Permissions
- Run ComfyUI with minimal required privileges
- Restrict file system permissions appropriately
- Regular permission audits
- Separate user accounts for different functions
Regular Updates
- Keep Assets Manager updated to latest version
- Update external tools (ExifTool, FFprobe) regularly
- Apply security patches promptly
- Test updates in non-production environments first
Configuration Security
Principle of Least Privilege
- Grant only necessary file system access
- Disable unnecessary features
- Use restrictive default settings
- Regular security configuration reviews
Environment Hardening
- Secure environment variable storage
- Validate all configuration inputs
- Use secure defaults where possible
- Regular security configuration audits
Operational Security
Access Control
- Limit access to authorized users
- Regular access reviews
- Session management best practices
- Audit user activities appropriately
Monitoring and Detection
- Monitor for unusual access patterns
- Alert on security-relevant events
- Regular security log reviews
- Incident response procedures
Security-Related Environment Variables
Trusted Proxy Configuration
- MAJOOR_TRUSTED_PROXIES
- Purpose: IPs/CIDRs allowed to supply
X-Forwarded-For/X-Real-IP - Default:
127.0.0.1,::1 - Format: Comma-separated IPs or CIDR blocks
- Security Impact: Controls which proxies can affect rate limiting
- Example:
MAJOOR_TRUSTED_PROXIES=127.0.0.1,192.168.1.0/24,10.0.0.0/8
Symlink Controls
- MJR_ALLOW_SYMLINKS
- Purpose: Allow symlink/junction custom roots
- Default:
off(disabled) - Options:
on,off,true,false - Security Impact: Controls directory traversal risk
- Example:
MJR_ALLOW_SYMLINKS=on
Database Security
- MAJOOR_DB_TIMEOUT
- Purpose: Database operation timeout
- Default: 30.0 seconds
- Security Impact: Prevents resource exhaustion
- Example:
MAJOOR_DB_TIMEOUT=60.0
- MAJOOR_DB_QUERY_TIMEOUT
- Purpose: Maximum query execution time
- Default: 60.0 seconds
- Security Impact: Prevents long-running queries
- Example:
MAJOOR_DB_QUERY_TIMEOUT=45.0
Resource Limits
- MAJOOR_MAX_METADATA_JSON_BYTES
- Purpose: Maximum metadata JSON size stored in DB/cache
- Default: 2097152 (2MB)
- Security Impact: Prevents memory exhaustion
- Example:
MAJOOR_MAX_METADATA_JSON_BYTES=4194304
- MJR_COLLECTION_MAX_ITEMS
- Purpose: Max items per collection JSON
- Default: 50000
- Security Impact: Prevents resource exhaustion
- Example:
MJR_COLLECTION_MAX_ITEMS=100000
Dependency Management
- MJR_AM_NO_AUTO_PIP
- Purpose: Disable best-effort dependency auto-install on startup
- Default: Auto-install enabled
- Security Impact: Controls external package installation
- Options:
1,true,yes,onto disable - Example:
MJR_AM_NO_AUTO_PIP=1
Security Testing
Self-Assessment Checklist
Regularly verify security configuration:
Access Controls
- [ ] File system access limited to intended directories
- [ ] Custom roots validated and secure
- [ ] No unauthorized file access possible
Network Security
- [ ] CSRF protection active on state-changing endpoints
- [ ] Rate limiting functioning properly
- [ ] Security headers applied correctly
Input Validation
- [ ] Path traversal prevented
- [ ] File type validation working
- [ ] External tool inputs sanitized
Penetration Testing Considerations
When performing security testing:
Authorized Testing Only
- Ensure proper authorization before testing
- Test in isolated environments when possible
- Document and report findings appropriately
Testing Areas
- Path traversal attempts
- Input validation bypasses
- Authentication bypasses
- Resource exhaustion attacks
- Cross-site scripting attempts
Incident Response
Security Event Classification
- Low Risk: Minor configuration issues
- Medium Risk: Potential information disclosure
- High Risk: System compromise or data breach
- Critical Risk: Active exploitation in progress
Response Procedures
- Isolate affected systems
- Document the incident
- Assess scope and impact
- Apply immediate mitigations
- Investigate root cause
- Implement permanent fixes
- Communicate appropriately
- Review and improve procedures
Security Model & Environment Variables Guide Version: 1.0 Last Updated: April 5, 2026
docs/SECURITY_ENV_VARS.md
SETTINGS_CONFIGURATION.md
Majoor Assets Manager - Settings & Configuration Guide
Version: 2.4.5 Last Updated: April 15, 2026
Overview
The Majoor Assets Manager offers extensive configuration options to customize the interface, performance, and functionality to match your workflow. This guide covers all available settings and configuration options.
Recent highlights: Floating Viewer settings, output path configuration from the UI, a fully Settings-driven remote write security flow, and a configurable Index DB directory for users on network drives or NAS storage.
Browser-Based Settings
Storage Location
Browser-based settings are stored in localStorage under the mjrSettings key:
- Settings persist between browser sessions
- Settings are specific to each browser/computer
- Settings don't sync across different browsers or devices
- Settings are tied to the specific domain where ComfyUI is hosted
Accessing Settings
Settings are primarily adjusted through the user interface:
- Open the Assets Manager in ComfyUI
- Look for settings icons or configuration panels
- Adjust settings as needed
- Settings are saved automatically
Security Settings In The UI
Majoor now exposes the main remote write controls directly in Settings, including:
Recommended Remote LAN SetupRequire Token For All WritesAllow Remote Full AccessAllow HTTP Token TransportMajoor: API Token
For the common trusted-LAN case, Recommended Remote LAN Setup is the intended one-click path. It generates a server-side token if needed, applies the recommended flags, and authorizes the current browser session immediately.
Token Types And What They Mean
There are two unrelated token concepts in the UI:
- Majoor: API Token: protects remote write access to the local Majoor backend.
- HuggingFace Token: optional token used for downloading AI models from HuggingFace Hub with better rate limits.
The HuggingFace token is not a cloud inference API key. Its presence does not mean prompts or images are sent to an external hosted AI service.
Browser Session Vs Server Token State
Not all security values live in the same place:
- browser settings in
localStoragekeep UI preferences and non-secret token state such astokenConfiguredandtokenHint - the active write token for the current tab/browser session lives in
sessionStorage - the persistent shared token lives server-side once saved through Majoor Settings or environment variables
This means a browser can show that a token exists on the server without storing the plaintext token permanently in browser local storage.
Visual Write Authorization Status
When the Assets Manager panel is open, the runtime status widget in the lower-right corner now includes a Write auth: line.
Examples:
Write auth: active ...ABCDWrite auth: missing in this browser ...ABCDWrite auth: not required
Use that line as the quickest confirmation that the current browser session can perform write operations.
Display Settings
Page Size
- Purpose: Controls how many assets are loaded per request
- Default: Usually 50-100 assets per page
- Range: Typically 10-500 assets per page
- Impact: Larger pages mean fewer requests but more memory usage
- Recommendation: Adjust based on your system resources and usage patterns
Sidebar Position
- Options: Left or Right
- Default: Usually Right
- Purpose: Controls where the details sidebar appears
- Impact: Affects layout and workflow depending on screen orientation
- Recommendation: Choose based on your screen setup and preferences
Hide PNG Siblings
- Purpose: Hide PNG files when video previews exist
- Default: Usually Off/Disabled
- Function: Reduces clutter from duplicate content in different formats
- Impact: Cleaner interface when working with video generations that include PNG previews
- Recommendation: Enable if you frequently work with video generations
Performance Settings
Auto-Scan on Startup
- Auto-scan on Startup: Automatically scan when ComfyUI starts
- Default: Usually Disabled to save resources
- Impact: Ensures fresh index but uses system resources
- Recommendation: Enable for frequently updated directories
Status Poll Interval
- Purpose: How often to check background tasks and status
- Default: Usually 1-5 seconds
- Range: 0.5-30 seconds
- Impact: More frequent polling provides more responsive status updates but uses more resources
- Recommendation: 1-2 seconds for good balance of responsiveness and resource usage
Tags Cache TTL
- Purpose: Time-to-live for tag caching in milliseconds
- Default: Usually 30,000ms (30 seconds)
- Range: 1,000ms to 300,000ms (1 second to 5 minutes)
- Impact: Longer TTL means fewer requests but potentially stale data
- Recommendation: 30,000ms (30 seconds) for good balance
Metadata & Viewer Settings
Floating Viewer Default Toggles
- UI location: Settings โ Majoor Assets Manager โบ Viewer
- Majoor: MFV Live Stream Enabled by Default
- Setting key:
viewer.mfvLiveDefault - Default: Enabled / On
- Purpose: Controls whether Live Stream starts enabled when the Floating Viewer opens, initializes, or resets. Live Stream follows final generation outputs after execution; selected-node previews are handled by Node Stream.
- Majoor: MFV KSampler Preview Enabled by Default
- Setting key:
viewer.mfvPreviewDefault - Default: Enabled / On
- Purpose: Controls whether the KSampler denoising preview starts enabled when the Floating Viewer opens, initializes, or resets. This stream shows sampler preview blobs during execution, not selected-node media.
Media Probe Backend
- Auto: Automatically choose the best available backend
- ExifTool: Use ExifTool exclusively for metadata extraction
- FFprobe: Use FFprobe exclusively (especially for video)
- Both: Use both tools when available
- Default: Auto
- Impact: Affects metadata extraction speed and completeness
- Recommendation: Keep as Auto unless troubleshooting specific issues
Workflow Minimap Display
- Show Node Labels: Display text labels on workflow minimap
- Show Connection Lines: Display connections between nodes
- Show Parameter Values: Display parameter values on nodes
- Minimap Size: Adjust the size of the workflow minimap
- Default: Varies by preference
- Impact: Affects readability of workflow previews
- Recommendation: Enable based on your need for workflow detail
Observability
- Request Logging: Enable detailed logging of API requests
- Performance Metrics: Track timing and performance data
- Debug Information: Enable additional debugging output
- Default: Usually Disabled
- Impact: Provides detailed information for troubleshooting but increases log volume
- Recommendation: Enable only when troubleshooting issues
File System Settings
Index Directory
- Purpose: Override where the SQLite index database and related index files are stored.
- Default:
<output_directory>/_mjr_index/(next to your assets) - Why change it: If your output directory lives on a network share (NAS, SMB, CIFS) or a slow/remote disk, SQLite may suffer file-locking issues. Moving the index to a fast local disk (e.g.
C:\mjr_indexon Windows) solves that without touching your asset files. - UI location: Settings โ Paths โ Majoor: Index Directory
- Takes effect after: ComfyUI restart. The old index directory is not deleted automatically; run a fresh scan after restart.
- Clear the override: Leave the field empty and save to revert to the default (
<output>/_mjr_index/).
Custom Roots
- Adding Custom Directories: Add additional directories to browse
- Path Validation: Ensures paths are valid and accessible
- Symlink Support: Allow symbolic links in custom roots (if enabled)
- Access Permissions: Respects file system permissions
- Default: None initially
- Impact: Expands browsing capabilities beyond standard directories
- Recommendation: Add directories you frequently access
Database Settings
Connection Management
- Max Connections: Maximum simultaneous database connections
- Connection Timeout: Time to wait for database operations
- Query Timeout: Maximum time for individual queries
- Default: Usually 8 connections, 30-second timeouts
- Impact: Affects performance with concurrent operations
- Recommendation: Adjust based on system resources and usage patterns
Optimization
- Auto-Optimize: Automatically optimize database periodically
- Manual Optimization: Run optimization tools manually
- Maintenance Schedule: When optimization occurs
- Default: Usually Manual
- Impact: Improves performance but requires temporary locking
- Recommendation: Schedule during low-usage periods
Advanced Configuration
AI Privacy And Offline Summary
- AI inference is intended to run locally after models are available on disk.
- First model bootstrap may require internet access to download model files from HuggingFace.
- After download and cache, AI features can run offline.
- Non-AI Majoor features do not require HuggingFace model downloads.
Environment Variables (Backend)
Directory Configuration
- MAJOOR_OUTPUT_DIRECTORY / MJR_AM_OUTPUT_DIRECTORY: Override default output directory
- Default: ComfyUI's output directory
- Format: Full path to directory
- Impact: Changes where the indexer looks for assets
- Example:
MAJOOR_OUTPUT_DIRECTORY=/path/to/my/output
- MJR_AM_INDEX_DIRECTORY / MAJOOR_INDEX_DIRECTORY: Override the directory where the SQLite index database is stored
- Default:
<output_directory>/_mjr_index/ - Format: Full absolute path to an existing or creatable directory
- Impact: Moves the index DB (and vectors DB) to a different disk or path; assets themselves stay in the output directory
- Takes effect: at ComfyUI startup (or restart after saving from the UI)
- Example:
MJR_AM_INDEX_DIRECTORY=C:\mjr_index - Example:
MJR_AM_INDEX_DIRECTORY=/var/local/mjr_index - Priority: env var โ sidecar file (
.mjr_index_directory_override) โ default - Also configurable from: Settings โ Paths โ Majoor: Index Directory (persists to the sidecar file)
External Tool Paths
- MAJOOR_EXIFTOOL_PATH / MAJOOR_EXIFTOOL_BIN: Path to ExifTool executable
- Default:
exiftool(assumes in PATH) - Format: Full path to exiftool executable
- Impact: Used for metadata extraction and file tagging
- Example:
MAJOOR_EXIFTOOL_PATH=/usr/local/bin/exiftool
- MAJOOR_FFPROBE_PATH / MAJOOR_FFPROBE_BIN: Path to FFprobe executable
- Default:
ffprobe(assumes in PATH) - Format: Full path to ffprobe executable
- Impact: Used for video/audio metadata extraction
- Example:
MAJOOR_FFPROBE_PATH=/usr/local/bin/ffprobe
Media Processing
- MAJOOR_MEDIA_PROBE_BACKEND: Media extraction backend selection
- Options:
auto,exiftool,ffprobe,both - Default:
auto - Impact: Determines which tools are used for metadata extraction
- Example:
MAJOOR_MEDIA_PROBE_BACKEND=both
Database Tuning
- MAJOOR_DB_TIMEOUT: Database operation timeout in seconds
- Default: 30.0
- Range: 1.0 to 300.0
- Impact: Maximum time to wait for database operations
- Example:
MAJOOR_DB_TIMEOUT=60.0
- MAJOOR_DB_MAX_CONNECTIONS: Maximum database connections
- Default: 8
- Range: 1 to 50
- Impact: Concurrency level for database operations
- Example:
MAJOOR_DB_MAX_CONNECTIONS=12
- MAJOOR_DB_QUERY_TIMEOUT: Maximum query execution time
- Default: 60.0 seconds
- Range: 1.0 to 300.0
- Impact: Prevents long-running queries from blocking
- Example:
MAJOOR_DB_QUERY_TIMEOUT=45.0
Performance Tuning
- MAJOOR_TO_THREAD_TIMEOUT: Timeout for background thread operations
- Default: 30 seconds
- Range: 10 to 600 seconds
- Impact: Maximum time for background operations
- Example:
MAJOOR_TO_THREAD_TIMEOUT=60
- MAJOOR_MAX_METADATA_JSON_BYTES: Maximum metadata JSON size
- Default: 2097152 (2MB)
- Range: 1024 to 104857600 (100MB)
- Impact: Limits memory usage for metadata storage
- Example:
MAJOOR_MAX_METADATA_JSON_BYTES=4194304
Collection Management
- MJR_COLLECTION_MAX_ITEMS: Maximum items per collection
- Default: 50000
- Range: 1000 to 1000000
- Impact: Prevents extremely large collections from impacting performance
- Example:
MJR_COLLECTION_MAX_ITEMS=100000
Security & Networking
- MJR_ALLOW_SYMLINKS: Allow symbolic links in custom roots
- Options:
on,off,true,false - Default:
off - Impact: Enables browsing of linked directories
- Example:
MJR_ALLOW_SYMLINKS=on
- MAJOOR_TRUSTED_PROXIES: IPs/CIDRs allowed for forwarded headers
- Default:
127.0.0.1,::1 - Format: Comma-separated IP addresses or CIDR blocks
- Impact: Security setting for proxy environments
- Example:
MAJOOR_TRUSTED_PROXIES=127.0.0.1,192.168.1.0/24
Dependency Management
- MJR_AM_NO_AUTO_PIP: Disable automatic dependency installation
- Options:
1,true,yes,onto disable - Default: Automatic installation enabled
- Impact: Prevents automatic pip installs at startup
- Example:
MJR_AM_NO_AUTO_PIP=1
Setting Up Environment Variables
Windows
Create a batch file to set environment variables:
@echo off
set MAJOOR_MEDIA_PROBE_BACKEND=auto
set MAJOOR_DB_TIMEOUT=60.0
REM Start ComfyUI with the environment variables
cd /d "C:\path\to\ComfyUI"
python main.py --auto-launch
pause
Unix/Linux/macOS
Create a shell script to set environment variables:
#!/bin/bash
export MAJOOR_MEDIA_PROBE_BACKEND=auto
export MAJOOR_DB_TIMEOUT=60.0
# Start ComfyUI with the environment variables
cd /path/to/ComfyUI
python main.py --auto-launch
Or add to your shell profile (.bashrc, .zshrc, etc.):
export MAJOOR_MEDIA_PROBE_BACKEND=auto
export MAJOOR_DB_TIMEOUT=60.0
Configuration Best Practices
Performance Optimization
- Adjust page size based on your system's RAM
- Set appropriate timeouts based on your storage speed
- Monitor database performance and adjust connections as needed
Security Considerations
- Keep default security settings unless you have specific requirements
- Only enable symlink support if you trust the linked directories
- Configure trusted proxies appropriately in networked environments
- Regularly update external tools (ExifTool, FFprobe) for security
Resource Management
- Monitor memory usage with large collections
- Adjust cache settings based on available RAM
- Set appropriate timeouts for your storage system
- Consider SSD storage for better database performance
Backup and Recovery
- Regularly backup the index database (
assets.sqlite) - Backup custom root configurations
- Maintain copies of important collections
- Document your configuration settings for recovery
Troubleshooting Configuration Issues
Common Problems
Settings Not Saving
- Clear browser cache and cookies
- Check browser localStorage quota
- Verify no browser extensions are interfering
- Try a different browser
Performance Issues
- Reduce page size if experiencing slowdowns
- Disable auto-scan if not needed
- Adjust database connection settings
- Check system resources (RAM, disk space)
External Tool Issues
- Verify tools are in PATH or set explicit paths
- Check tool permissions and access rights
- Ensure tools are properly installed
- Test tools independently before using with Assets Manager
Diagnostic Steps
- Check ComfyUI console for error messages
- Verify all required dependencies are installed
- Test external tools independently
- Review environment variable settings
- Try default settings to isolate configuration issues
Migration and Updates
Settings Preservation
- Browser settings are preserved across updates
- Environment variables need to be reapplied after system restarts
- Custom root configurations are stored in the index directory
- Collections are preserved as JSON files
Configuration Updates
- New settings are added with sensible defaults
- Old settings remain unchanged during updates
- Review new settings after updates to optimize functionality
- Check release notes for configuration changes
_Settings & Configuration Guide Version: 1.0_ _Last Updated: April 5, 2026_
docs/SETTINGS_CONFIGURATION.md
SHORTCUTS.md
Keyboard Shortcuts
Version: 2.4.5 Last Updated: April 14, 2026
Quick reference for Majoor Assets Manager shortcuts. For full details including MFV shortcuts, see HOTKEYS_SHORTCUTS.md.
Grid View
| Shortcut | Action |
|---|---|
| 1-5 | Set Rating |
| 0 | Clear Rating |
| Enter / Space | Open Viewer |
| D | Toggle Details Sidebar |
| T | Edit Tags |
| B / Shift+B | Add/Remove Collection |
| Ctrl+A / Ctrl+D | Select/Deselect All |
| F2 | Rename |
| Del | Delete |
| Ctrl+Shift+C | Copy Path |
| Ctrl+Shift+E | Open in Explorer |
| Ctrl+F / Ctrl+K | Focus Search |
Viewer
| Shortcut | Action |
|---|---|
| Esc | Close Viewer |
| Space | Play/Pause |
| Left Arrow / Right Arrow | Prev/Next Asset, or step frame when the focused MFV player is active |
| F | Fullscreen |
| D | Info Panel |
| I | Set In Point (video) / Pixel Probe (image) |
| O | Set Out Point (video) |
| Home | Go to In Point (video) |
| End | Go to Out Point (video) |
| L | Loupe |
| Z | Zebra |
| G | Grid Overlay |
| Alt+1 | 1:1 View |
| + / - | Zoom |
docs/SHORTCUTS.md
TESTING.md
Testing
Version: 2.4.5 Last Updated: April 5, 2026
This project uses pytest (backend) and Vitest (frontend) for comprehensive test coverage. On Windows, batch runners are provided for convenience and generate both:
- JUnit XML (
.xml) - Styled HTML report (
.html)
Before running the quality gate or local tests, install contributor tooling:
pip install -r requirements-dev.txt
python scripts/install_local_hooks.py
Runtime dependencies stay in requirements.txt; optional AI/vector dependencies stay in requirements-vector.txt. See docs/DEPENDENCY_POLICY.md for the canonical dependency policy.
Reports are written to:
tests/__reports__/
Open the index:
tests/__reports__/index.html
Test Coverage
Backend (Python)
- Core functionality (index, search, routes)
- Metadata extraction (ExifTool, FFprobe, geninfo)
- Database operations (schema, migrations)
- Security (CSRF, auth, path validation)
- Feature tests (collections, batch ZIP, viewer)
- Regression tests
Frontend (JavaScript)
- Vue.js components
- API client
- UI utilities
- Event handling
- Drag & drop
Quick Commands
All Tests (Cross-Platform)
# From repo root
python -m pytest -q
# With verbose output
python -m pytest -v
# With coverage
pytest tests/ --cov=mjr_am_backend --cov-report=html
Single Test File
python -m pytest tests/core/test_routes.py -v
Single Test Folder
python -m pytest tests/metadata/ -v
Single Test Function
python -m pytest tests/core/test_routes.py::test_routes -v
Frontend Tests
# Run JavaScript tests
npm run test:js
# Watch mode
npm run test:js:watch
Canonical Quality Gate
# Full repo gate
python scripts/run_quality_gate.py
# Fast Python-only gate (useful before pushing)
python scripts/run_quality_gate.py --python-only --skip-tests
# Tox wrapper for the fast Python-only gate
tox -e quality
The canonical gate runs encoding/BOM checks, ruff, mypy, bandit, pip-audit, xenon/radon complexity checks, backend tests, frontend tests, and npm audit. The pip-audit step audits requirements.txt directly so the local package itself does not need to be published on PyPI for the gate to pass. CI uses the same script for the Python quality job so local and CI behavior stay aligned.
The Python coverage gate currently enforces a minimum combined backend/shared coverage threshold of 60%, which gives the CI pipeline a regression floor without making legacy cleanup block unrelated work.
During the migration to stricter quality thresholds, ruff is enforced with autofix on commit for changed Python files, while mypy and the changed-file complexity gate run on pre-push. The changed-file complexity threshold is aligned locally with the CI limit of 25, and repository-wide hygiene, security, and complexity checks continue to run across the repo.
Local Hooks
This repository ships local Git hooks via pre-commit:
python scripts/install_local_hooks.py
Installed behavior:
pre-commit: BOM/encoding hygiene,ruff --fix,eslint --fix,prettier --writepre-push: mandatorymypyplusscripts/run_changed_quality_gate.py
The changed-file quality gate is also runnable manually:
python scripts/run_changed_quality_gate.py
For frontend tests in the Node Vitest environment, prefer the shared helpers in js/tests/helpers/vitestEnvironment.mjs. Team rule: use partial Vue mocks by default, based on the real vue module, instead of full replacement mocks.
Batch runners (Windows)
From the repo root:
- Full suite:
run_tests.bat(delegates totests/run_tests_all.bat)
Category runners:
tests/config/run_tests_config.battests/core/run_tests_core.battests/database/run_tests_database.battests/features/run_tests_features.battests/metadata/run_tests_metadata.battests/rating_tags/run_tests_rating_tags.battests/regressions/run_tests_regressions.bat
All batch runners support /nopause for non-interactive runs:
run_tests.bat /nopause
Test artifacts (DB/WAL/SHM)
Some tests create SQLite files. Pytest temp files are stored under:
tests/__pytest_tmp__/
This includes .db, .db-wal, *.db-shm, and other runtime artifacts.
Parser samples (metadata extraction)
tests/metadata/test_parser_folder_scan.py scans every file under:
tests/parser/(recursive)
If you don't want to commit large samples, you can point the test to an external folder:
$env:MJR_TEST_PARSER_DIR = "C:\path\to\parser"
python -m pytest tests/metadata/test_parser_folder_scan.py -qdocs/TESTING.md
THREAT_MODEL.md
Threat Model โ Majoor Assets Manager
Version: 2.4.5 Date: April 5, 2026 (Updated from 2026-01-24)
Scope
This model covers the Majoor Assets Manager backend routes under /mjr/am/* and the associated local filesystem/database operations.
Recent highlights: Enhanced API token authentication, improved CSRF protection, and better rate limiting for remote access scenarios.
Assets to Protect
- User files on disk (input/output/custom roots).
- SQLite index database and collections JSON files.
- ComfyUI server availability (avoid crashes / runaway CPU / locks).
- Privacy of metadata (prompts/workflows embedded in images/videos).
Trust Boundaries
- Browser/UI โ ComfyUI HTTP server: untrusted inputs via query params and JSON bodies.
- Extension backend โ Filesystem: path traversal and symlink/junction concerns.
- Extension backend โ External tools (
exiftool,ffprobe): command execution and parsing. - Extension backend โ SQLite: injection/locking/corruption risks.
Threats & Mitigations
1) Path Traversal / Unauthorized File Access
Threat: An attacker (or malicious workflow) calls /mjr/am/* with paths pointing outside allowed roots.
Mitigations:
- Normalize paths and reject invalid/unsafe paths (
_safe_rel_path,_is_within_root, allow-lists). - Custom roots are resolved and validated (root-id โ canonical directory).
- Avoid exposing arbitrary โopen fileโ primitives; use explicit allow-listed actions.
2) Symlink/Junction Escape (TOCTOU)
Threat: A path initially validated as within root later resolves to a different location (symlink swap).
Mitigations:
- Resolve roots/targets with
resolve(strict=True)where possible. - Validate after resolution and before use.
- Treat mid-scan disappearance as a benign failure and continue (best-effort).
3) CSRF / Cross-Site Requests
Threat: A local web page triggers state-changing endpoints (delete/rename/stage) against localhost.
Mitigations:
- CSRF checks on state-changing routes.
- Prefer returning
Result.Err("CSRF", ...)instead of throwing.
4) Denial of Service (Large Listings / Heavy Scans)
Threat: Repeated list/index requests cause CPU/disk thrash.
Mitigations:
- Bounded queues for background work (scan pending max).
- Rate limiting for expensive endpoints.
- TTL-based filesystem listing cache with bounded size.
5) External Tool Execution Risks
Threat: Tool invocation on untrusted media can be slow or crash; tool may be missing.
Mitigations:
- Timeouts and best-effort fallbacks (
code="DEGRADED"). - Never crash UI: return
Result.Err/Okwith degraded metadata. - Avoid shell string interpolation; prefer argv lists.
6) Data Corruption / Concurrent Writes
Threat: Concurrent updates corrupt JSON stores or SQLite schema state.
Mitigations:
- Atomic writes for JSON stores (write tmp + replace).
- SQLite locking strategy and bounded transactions.
Residual Risk
- Localhost endpoints are inherently accessible to local malware.
- Media parsing remains a complex attack surface (mitigated by timeouts and least-privilege execution).
Non-Goals
- Remote multi-user authentication/authorization (ComfyUI is typically local-only).
- Protecting against a fully compromised host OS.
docs/THREAT_MODEL.md
VIEWER_FEATURE_TUTORIAL.md
Majoor Assets Manager - Viewer Feature Tutorial
Version: 2.4.5 Last Updated: April 15, 2026
Overview
For a dedicated Majoor Floating Viewer walkthrough with focused screenshots, see MFV_GUIDE.md.
The Majoor Assets Manager provides two viewer experiences:
1. Majoor Floating Viewer (MFV) โ NEW! ๐
A lightweight, draggable floating panel for real-time generation comparison:
- Live Stream Mode: Automatically follows final generation outputs and is enabled by default
- KSampler Preview: Streams denoising-step previews during execution and is enabled by default
- Compare Modes: Simple, A/B Compare, Side-by-Side, Grid Compare (up to 4 assets)
- Multi-Pin References (A/B/C/D): Pin up to 4 images simultaneously for comparison
- Node Parameters Sidebar: Edit workflow node widgets directly in the viewer
- Run Button: Queue prompt from viewer toolbar without switching to canvas
- Node Stream: Click nodes to preview available frontend media, including ImageOps live previews
- Real-time Updates: Watch generations as they complete
- Portable: Move anywhere on screen, resize, dock
Best for: Real-time workflow monitoring, quick comparisons, node preview, parameter tweaking
2. Standard Viewer
A full-featured overlay viewer with advanced analysis tools:
- Enhancement Tools: Exposure, gamma, channel isolation
- Analysis Tools: False color, zebra, histogram, waveform, vectorscope
- Visual Overlays: Grid, pixel probe, loupe
- Video Controls: Timeline, in/out points, speed control
Best for: Detailed analysis, quality inspection, metadata review
Table of Contents
Majoor Floating Viewer (MFV)
- Opening the Floating Viewer
- Live Stream Mode
- Compare Modes
- Multi-Pin References (A/B/C/D)
- Node Parameters Sidebar
- Sidebar Position Setting
- Node Stream
- MFV Controls
- MFV Keyboard Shortcuts
Standard Viewer
- Opening the Standard Viewer
- Viewer Layout
- Viewer Navigation
- Image Enhancement Tools
- Analysis Tools
- Visual Overlays
- Professional Analysis Tools (Scopes)
- Video-Specific Features
- Export Capabilities
- Standard Viewer Hotkeys
Both Viewers
Majoor Floating Viewer (MFV)
Opening the Floating Viewer
There are several ways to open the Majoor Floating Viewer:
Method 1: Toolbar Button
- Open the Assets Manager panel
- Click the Floating Viewer button in the toolbar (icon: overlapping rectangles)
- The MFV panel appears on screen
Method 2: Context Menu
- Right-click on any asset in the grid
- Select "Open in Floating Viewer"
- The MFV panel opens with that asset
Method 3: Node Stream
- Open the MFV
- Enable Node Stream or press N
- Click a node in ComfyUI to show its available preview media
Method 4: Keyboard Shortcut
- Press V while the Assets Manager panel is hovered or focused to toggle the Floating Viewer
Live Stream Mode
Live Stream Mode tracks final generation outputs after workflow execution.
Default behavior:
- Live Stream starts enabled by default
- KSampler Preview also starts enabled by default
- You can change both in Settings โ Majoor Assets Manager โบ Viewer
Enabling Live Stream
- Open the Floating Viewer
- Click the Live Stream button (icon: broadcast tower)
- Or press L to toggle
How It Works
- Monitors ComfyUI generation output events
- Automatically switches to newly generated output files
- Does not follow canvas node selection; use Node Stream for selected nodes
- No manual refresh needed after execution completes
Use Cases
- Workflow Development: Watch final outputs as runs complete
- Batch Generation: Monitor progress across multiple prompts
- Parameter Tuning: Compare completed runs
- Queue Monitoring: Keep the latest output visible while prompts execute
Compare Modes
The MFV supports a single-asset view plus three comparison workflows:
Simple Mode (Default)
- Single image display
- Full resolution preview
- Standard navigation
Shortcut: Press C until Simple mode is active again
A/B Compare Mode
- Toggle between two images rapidly
- Click asset to set as A or B
- Useful for subtle difference detection
How to Use:
- Enable A/B Compare mode (press C from Simple mode)
- Click first asset โ Set as A
- Click second asset โ Set as B
- Use C again later to move on to Side-by-Side or back to Simple mode
Use Cases:
- Compare different sampler results
- Check before/after edits
- Evaluate parameter changes
Side-by-Side Mode
- Display both images simultaneously
- Split view (vertical or horizontal)
- Synchronized zoom and pan
How to Use:
- Enable Side-by-Side mode (press C until Side-by-Side is active)
- Select two assets
- Both display side by side
- Use the mode button or toolbar controls if you want to move into Grid compare
Use Cases:
- Direct visual comparison
- Composition analysis
- Color grading comparison
Multi-Pin References (A/B/C/D)
The Multi-Pin system lets you pin up to 4 reference images simultaneously for comparison.
How It Works
- Open the Floating Viewer
- In the toolbar, locate the A B C D toggle buttons next to the mode button
- Click a letter to pin the currently displayed image to that slot
- Click the same letter again to unpin it
- Multiple slots can be active at the same time
Pin Behavior
- Pinned slots are locked: When Live Stream brings in a new generation, pinned slots keep their content while unpinned slots update automatically
- Compare with pins: In A/B or Side-by-Side mode, pin A as a reference and let B follow live generations to compare every new result against a fixed baseline
- Multi-pin in Grid Compare: Pin A, B, C, and D to lock 4 images for simultaneous comparison in Grid Compare mode
Use Cases
- Baseline comparison: Pin your best result in slot A and iterate freely
- Parameter sweep: Pin 4 different sampler results and compare visually
- Before/after editing: Pin the original in A, the edited version in B
Node Parameters Sidebar
The Node Parameters sidebar displays and lets you edit the ComfyUI node widgets directly inside the Floating Viewer.
Opening the Sidebar
- Open the Floating Viewer
- Click the Node Parameters button (sliders icon) in the toolbar โ it is located at the far right of the toolbar
- The sidebar slides open on the right (default position)
What It Shows
- All widgets from the workflow node that produced the current image
- Grouped by node: each node section has a colored header showing the node title
- Widget types supported: text fields, number inputs, combo/dropdown selectors, toggles
Editing Widgets
- Text fields: Click and type. Long text (prompts) use a resizable text area
- Click the expand button (โ) to toggle between collapsed (80px) and expanded (680px) height
- Text labels appear above the text area for better readability
- Numbers: Type a value or use the stepper
- Combos: Select from the dropdown list
- Changes are applied back to the ComfyUI graph in real time
Run Button
The Run (โถ) button at the far right of the toolbar queues the current workflow, allowing you to iterate without switching back to the ComfyUI canvas.
Use Cases
- Prompt iteration: Edit the positive/negative prompt and re-run without leaving the viewer
- Seed tweaking: Change the seed value and queue immediately
- Sampler comparison: Switch samplers from the sidebar and compare outputs
- CFG tuning: Adjust CFG scale, run, and compare final outputs via Live Stream
Sidebar Position Setting
You can change where the Node Parameters sidebar appears inside the Floating Viewer.
Changing the Position
- Go to Settings โ Majoor Assets Manager โบ Viewer
- Find Node Parameters sidebar position
- Select one of:
- right (default) โ sidebar opens on the right side
- left โ sidebar opens on the left side
- bottom โ sidebar opens at the bottom as a horizontal panel
- The change applies immediately โ no page reload required
Recommendations
- Right works best for most layouts and screen sizes
- Left is useful when your ComfyUI canvas is on the right
- Bottom is ideal for wide screens or when you prefer a short, wide parameter panel
Node Stream
Node Stream lets you preview selected node content when the frontend already has media for that node.
Enabling Node Stream
- Open the Floating Viewer
- Click the Node Stream button (icon: node graph)
- Or press N to toggle
How It Works
- Click LoadImage / loader nodes -> preview the loaded media
- Click SaveImage / preview nodes -> preview an existing generated preview/output
- Click ImageOps nodes -> preview the embedded live canvas when available
- Click other intermediate nodes -> preview only if they expose
node.imgs, widget media, or a downstream preview/save node
Limitations
- Node Stream cannot read raw backend tensors (
IMAGE,MASK,LATENT) before execution. - Python node outputs become available only after ComfyUI executes the node.
- ImageOps live previews are frontend canvases, so they can update without queueing but are not the backend tensor itself.
- If no frontend media exists for the selected node or downstream path, the MFV has nothing to display.
Use Cases
- Debugging: Check available intermediate previews
- Workflow Building: Verify node connections
- Quality Control: Inspect outputs at each stage
- Teaching: Show workflow data flow
MFV Controls
Panel Controls
- Move: Drag from panel header
- Resize: Drag panel edges or corners
- Close: Click X button or press Esc
- Minimize: Click minimize button (optional)
Zoom & Pan
- Zoom In: Mouse wheel up or Up Arrow
- Zoom Out: Mouse wheel down or Down Arrow
- Reset Zoom: Press 0 (fit to screen)
- Actual Size: Press 1 (1:1 pixel)
- Pan: Click and drag when zoomed in
Navigation
- Previous Asset: Left Arrow when the inline player is not focused
- Next Asset: Right Arrow when the inline player is not focused
- First Asset: Home
- Last Asset: End
MFV Keyboard Shortcuts
General
| Shortcut | Action |
|---|---|
| Esc | Close Floating Viewer |
| C | Cycle compare modes: A/B, Side-by-side, Off (Simple mode) |
| K | Toggle KSampler denoising preview on or off |
| L | Toggle Live Stream final-output following on or off |
| N | Toggle Node Stream selected-node previewing |
Video Controls
| Shortcut | Action |
|---|---|
| Space | Play/Pause the focused inline player |
| Left Arrow | Step backward one frame |
| Right Arrow | Step forward one frame |
Tip: click once on the MFV media player to focus it, then use the playback keys above. The Gen Info overlay remains visible and automatically sits above the player controls when they are present.
See HOTKEYS_SHORTCUTS.md for complete shortcut list.
Standard Viewer
Viewer Navigation
Basic Navigation
- Zoom: Use mouse wheel to zoom in/out
- Pan: Click and drag to move around the image
- Fit to Screen: Automatically scales image to fit window
- Actual Size: View image at 1:1 pixel ratio (Alt+1)
Asset Navigation
- Next/Previous: Navigate between assets in the current set
- Keyboard Arrows: Use arrow keys to move between assets
- Direct Selection: Click on thumbnails if available
Image Enhancement Tools
Exposure Adjustment (EV)
The exposure adjustment tool allows you to modify the brightness of your image:
- Accessing the Tool:
- Locate the exposure control in the toolbar
- Usually represented by a sun or EV icon
- Adjusting Exposure:
- Adjust exposure compensation from -5 to +5 EV
- Real-time preview shows changes immediately
- Use to evaluate details in shadows/highlights
- Preserves color relationships while adjusting brightness
- Use Cases:
- Revealing details in underexposed areas
- Recovering highlights in overexposed regions
- Comparing exposure differences between assets
- Evaluating dynamic range
Gamma Correction
Gamma correction adjusts the midtone brightness:
- Accessing the Tool:
- Find the gamma control in the toolbar
- Usually labeled with ฮณ or gamma symbol
- Adjusting Gamma:
- Adjust gamma curve from 0.25 to 4.0
- Alters midtone brightness without affecting highlights/shadows as much
- Real-time application with live preview
- Useful for fine-tuning contrast in specific tonal ranges
- Use Cases:
- Enhancing midtone contrast
- Adjusting overall tonal balance
- Fine-tuning for display characteristics
- Preparing for different output conditions
Channel Isolation
The viewer allows you to isolate different color channels:
- Available Channels:
- RGB: View all color channels combined
- R: View only red channel information
- G: View only green channel information
- B: View only blue channel information
- Alpha: View transparency/alpha channel
- Luma: View luminance information only
- Using Channel Isolation:
- Select the desired channel from the toolbar
- Observe the isolated channel information
- Compare between different channels
- Identify channel-specific issues
- Use Cases:
- Identifying color channel imbalances
- Detecting channel-specific noise
- Analyzing alpha channel transparency
- Evaluating luminance distribution
Analysis Tools
False Color Mode
False color mode helps identify exposure issues:
- Activating False Color:
- Enable false color mode from the toolbar
- Usually represented by a color palette icon
- Interpreting Results:
- Identifies overexposed and underexposed areas
- Highlights clipped highlights (typically white/magenta)
- Shows blocked shadows (typically blue/black)
- Helps evaluate dynamic range and exposure
- Use Cases:
- Checking for highlight clipping
- Identifying shadow detail loss
- Evaluating overall exposure balance
- Comparing exposure between different assets
Zebra Patterns (Z)
Zebra patterns indicate areas approaching clipping:
- Enabling Zebra:
- Activate zebra patterns from the toolbar
- Usually labeled with "Z" or zebra icon
- Understanding Zebra:
- Displays 100% IRE (95% for Rec.2020) areas as zebra stripes
- Helps identify areas approaching clipping
- Adjustable threshold levels
- Useful for maintaining highlight detail
- Use Cases:
- Protecting highlight detail
- Setting optimal exposure levels
- Comparing exposure between shots
- Quality control for consistent exposure
Visual Overlays
Grid Overlays (G)
Grid overlays assist with composition:
- Available Grid Types:
- Off: No grid overlay
- Thirds: Rule of thirds grid lines
- Center: Center crosshair
- Safe Area: TV broadcast safe area guides
- Golden Ratio: Golden ratio composition guides
- Using Grid Overlays:
- Cycle through grid types using "G" key
- Assess compositional balance
- Align elements to grid lines
- Compare compositions between assets
- Use Cases:
- Composition analysis
- Alignment verification
- Rule of thirds evaluation
- Broadcast compliance checking
Pixel Probe (I)
Pixel probe provides detailed pixel information:
- Activating Pixel Probe:
- Enable pixel probe from toolbar or press "I"
- Hover over image to see information
- Information Provided:
- Exact pixel coordinates
- RGB/RGBA values at cursor position
- Hex color code
- Luminance value information
- Use Cases:
- Color accuracy verification
- Detail inspection
- Color sampling
- Quality assessment at pixel level
Loupe Magnification (L)
Loupe provides magnified view of specific areas:
- Using Loupe:
- Activate loupe from toolbar or press "L"
- Hover over area of interest
- View magnified representation
- Features:
- Adjustable magnification level
- Shows fine detail at pixel level
- Helpful for quality assessment
- Real-time magnification
- Use Cases:
- Quality inspection
- Artifact detection
- Detail analysis
- Sharpness evaluation
Professional Analysis Tools (Scopes)
Histogram
The histogram shows tonal distribution:
- Accessing Histogram:
- Enable from scopes section
- Shows RGB channel distribution
- Interpretation:
- X-axis represents tonal range (shadows to highlights)
- Y-axis represents pixel count
- Separate curves for R, G, B channels
- Identifies tonal distribution patterns
- Use Cases:
- Exposure evaluation
- Contrast analysis
- Color balance assessment
- Dynamic range evaluation
Waveform
The waveform shows luminance distribution:
- Accessing Waveform:
- Enable from scopes section
- Shows luminance across image
- Interpretation:
- Horizontal axis represents image width
- Vertical axis represents luminance levels
- Brighter areas appear higher in the display
- Darker areas appear lower in the display
- Use Cases:
- Exposure evaluation
- Luminance distribution analysis
- Highlight/shadow placement
- Consistency checking
Vectorscope
The vectorscope shows color information:
- Accessing Vectorscope:
- Enable from scopes section
- Shows color information in chromaticity diagram
- Interpretation:
- Shows color saturation and hue
- Identifies color distribution
- Reveals color casts or imbalances
- Shows skin tone placement
- Use Cases:
- Color balance correction
- Skin tone evaluation
- Saturation assessment
- Color cast identification
Video-Specific Features
Video Playback Controls
- Play/Pause: Standard play/pause functionality
- Loop/Once: Toggle between continuous and single playback
- Speed Control: Adjustable playback speed (0.25x to 4x)
- Frame Stepping: Precise frame-by-frame navigation (Left/Right Arrow keys when player bar is focused)
Timeline Features
- Seek Bar: Drag to jump to specific points in video
- Current Time: Display of current playback position
- Duration: Total video duration display
- Frame Counter: Current frame number display
In/Out Points
- Set In Point: Mark start of desired segment (I key)
- Set Out Point: Mark end of desired segment (O key)
- Segment Playback: Play only the marked segment
- Clear Points: Remove In/Out markers
Hardware Acceleration (WebGL)
The viewer automatically detects WebGL support to enable GPU-accelerated video rendering:
- Benefits: Real-time Exposure, Gamma, and Zebra analysis on 4K videos without CPU load
- Fallback: If WebGL is unavailable, it automatically switches to a robust CPU-based renderer (Canvas 2D)
Export Capabilities
Save Current Frame
- Capture current video frame as PNG
- Preserves full resolution and quality
- Saves to default output directory
- Includes generation metadata if available
Copy to Clipboard
- Copy current view to system clipboard
- Best-effort format selection
- May not work on all platforms
- Useful for quick sharing
Download Original
- Download the original asset file
- Maintains original format and quality
- Preserves embedded metadata
- Safe file transfer protocol
Viewer Hotkeys
All viewer hotkeys are captured and don't affect ComfyUI or browser:
- Esc: Close viewer
- 0-5: Set rating (single view)
- Left/Right Arrow: Step video frames (when player bar is focused)
- F: Toggle fullscreen
- Z: Toggle zebra patterns
- G: Cycle through grid overlays
- I: Toggle pixel probe
- L: Toggle loupe magnification
- C: Copy last probed color hex value
- Alt+1: Toggle 1:1 zoom
Comparison Integration
The viewer seamlessly integrates with the comparison feature:
- Switch between single view and comparison modes
- Maintain analysis tools during comparison
- Apply enhancements to both compared assets
- Use analysis tools on comparison results
Troubleshooting Viewer Issues
Performance Problems
- Slow Rendering: Reduce enhancement effects or close other applications
- Memory Issues: Close viewer tabs when not actively using them
- Codec Problems: Install appropriate codecs for video files
- Browser Compatibility: Ensure using supported browsers
Tool Malfunctions
- Missing Tools: Verify WebGL support in your browser
- Unresponsive Controls: Refresh the viewer or browser
- Display Issues: Check browser zoom level and display settings
- Audio Problems: Check system audio settings for video files
File-Specific Issues
- Unsupported Formats: Convert to supported formats if needed
- Corrupted Files: Verify file integrity
- Large Files: Be patient with initial loading of large files
- Network Issues: Ensure stable connection for remote files
Best Practices
Efficient Viewing
- Use keyboard shortcuts for faster navigation
- Apply analysis tools systematically
- Take advantage of export features for sharing
- Use comparison modes for parameter evaluation
Quality Assessment
- Use pixel probe for detailed inspection
- Apply false color to check exposure
- Use zebra patterns to protect highlights
- Employ scopes for professional analysis
Workflow Integration
- Combine viewer tools with rating system
- Use export features for review workflows
- Integrate with collection management
- Apply consistent analysis methodology
Performance Optimization
- Close viewers when not actively using them
- Use lower resolution previews when possible
- Organize assets into collections to reduce load times
- Keep frequently accessed assets in smaller collections
_Viewer Feature Tutorial Version: 2.4.5_ _Last Updated: April 14, 2026_
docs/VIEWER_FEATURE_TUTORIAL.md
node-stream-reactivation.md
Node Stream Notes
Current Status
Node Stream is enabled. The single source of truth is:
js/features/viewer/nodeStream/nodeStreamFeatureFlag.js
The active implementation is selection-only: when the MFV Node Stream button is on, selecting a ComfyUI canvas node streams the best available preview into the Floating Viewer. It supports standard ComfyUI node previews, downstream preview nodes, load widgets, and ImageOps live-preview canvases exposed through node.__imageops_state.canvas.
Node Stream is the only MFV feature that follows canvas node selection. Live Stream is reserved for final generation outputs, and KSampler Preview is reserved for denoising-step blobs from the ComfyUI API.
If NODE_STREAM_FEATURE_ENABLED is set to false:
- the Floating Viewer toolbar does not show the Node Stream button,
- the
Nshortcut does nothing, floatingViewerManagerignores Node Stream activation and payloads,entry.jsdoes not initialize the controller or exposewindow.MajoorNodeStream,NodeStreamControllerstays inert even if called directly.
Files Involved
js/features/viewer/nodeStream/nodeStreamFeatureFlag.jsjs/features/viewer/nodeStream/NodeStreamController.jsjs/features/viewer/nodeStream/imageOpsPreviewBridge.jsjs/features/viewer/FloatingViewer.jsjs/features/viewer/floatingViewerManager.jsjs/entry.jsjs/app/events.jsjs/tests/node_stream_controller.vitest.mjs
Smoke Tests
Use the real local stack and verify these cases from the ComfyUI canvas:
- Core
LoadImage - Core
LoadImageMask ImageOpsPreview- Any ImageOps processing node with an embedded live preview
LayerUtility: ImageBlendLayerUtility: ImageBlendAdvanceLayerMask: MaskPreviewMaskPreview+fromcomfyui_essentialsPreviewImageOrMaskandImageAndMaskPreviewfromcomfyui-kjnodes- at least one WAS Suite image node if present locally
Expected Behavior
- Selection-only behavior: previews follow node clicks, not execution events.
- If a selected node has no direct preview, downstream preview/save nodes may be
- ImageOps live preview canvases update in the MFV when their render signature changes.
- The feature stays compatible with MFV lifecycle teardown and hot-reload.
used as the preview source.
Stream Feature Boundaries
Live Stream
- Source:
NEW_GENERATION_OUTPUTevents. - Trigger: workflow execution finishes and ComfyUI reports output files.
- Displays: final generated files only.
- Does not follow selected canvas nodes.
- Limitation: no raw tensor access and no no-queue preview.
KSampler Preview
- Source: ComfyUI API
b_preview_with_metadataor legacyb_previewevents. - Trigger: sampler denoising during workflow execution.
- Displays: transient preview blobs, usually JPEG/PNG frames.
- Limitation: only available while ComfyUI emits sampler previews.
Node Stream
- Source: selected ComfyUI canvas node.
- Trigger: user selects a node while Node Stream is active.
- Displays: frontend-observable media such as
node.imgs, widget<img>/<video>, - Limitation: cannot read backend tensors (
IMAGE,MASK,LATENT) before node
load-widget filenames, downstream preview/save media, and ImageOps live canvases.
execution. For ImageOps, the no-queue live path is the frontend canvas, not the Python tensor output.
docs/node-stream-reactivation.md