Files
MajoorWaldi-ComfyUI-Majoor-…/user_guide.html
T

11737 lines
584 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<title>Majoor Assets Manager - User Guide</title>
<style>
:root{
--bg: #0f1115;
--panel: rgba(21,25,35,0.92);
--text: #e7eaf0;
--muted: rgba(231,234,240,0.68);
--border: rgba(231,234,240,0.10);
--accent: #5fb3ff;
--shadow: 0 18px 45px rgba(0,0,0,0.35);
--radius: 14px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
}
*{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0;
font-family: var(--sans);
color: var(--text);
background:
radial-gradient(1200px 600px at 20% -20%, rgba(95,179,255,0.18), transparent 60%),
radial-gradient(900px 550px at 100% 0%, rgba(57,217,138,0.12), transparent 55%),
var(--bg);
}
a{ color: var(--accent); text-decoration:none; }
a:hover{ text-decoration: underline; }
mark{ background: rgba(95,179,255,0.22); color: rgba(231,234,240,0.96); padding: 0 2px; border-radius: 4px; }
code{ font-family: var(--mono); font-size: 12px; padding: 2px 6px; border-radius: 8px; border: 1px solid rgba(231,234,240,0.10); background: rgba(16,20,29,0.70); color: rgba(231,234,240,0.92); }
pre{ margin: 10px 0; padding: 12px 12px; border-radius: 12px; border: 1px solid rgba(231,234,240,0.10); background: rgba(16,20,29,0.90); overflow:auto; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.18); }
pre code{ padding:0; border:0; background: transparent; font-size: 12px; }
hr{ border:0; border-top: 1px solid rgba(231,234,240,0.10); margin: 18px 0; }
blockquote{ margin: 10px 0; padding: 10px 12px; border-left: 3px solid rgba(95,179,255,0.55); background: rgba(95,179,255,0.06); border-radius: 12px; color: rgba(231,234,240,0.88); }
.table-wrap{ overflow:auto; border-radius: 12px; border: 1px solid rgba(231,234,240,0.10); background: rgba(16,20,29,0.55); }
table{ width:100%; border-collapse: collapse; }
th, td{ padding: 9px 10px; vertical-align: top; border-top: 1px solid rgba(231,234,240,0.08); }
th{ text-align:left; font-size: 12px; color: var(--muted); font-weight: 700; border-top: 0; }
.wrap{ max-width: 1180px; margin:0 auto; padding: 22px 16px 70px; }
.top{
display:flex; gap:14px; align-items:flex-end; justify-content:space-between;
padding: 16px 18px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
box-shadow: var(--shadow);
}
.brand{ display:flex; gap: 12px; align-items:center; }
.logo{
width: 36px; height: 36px;
border-radius: 12px;
border: 1px solid rgba(231,234,240,0.12);
background:
radial-gradient(120px 70px at 30% 20%, rgba(95,179,255,0.25), transparent 55%),
rgba(0,0,0,0.25);
display:flex; align-items:center; justify-content:center;
font-size: 18px;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15);
}
.logo.icon{ background-image: url('ressources/icon.png'); background-size: cover; background-position: center; font-size: 0; }
.title{ font-size: 20px; font-weight: 800; letter-spacing: 0.2px; }
.subtitle{ color: var(--muted); font-size: 12px; margin-top: 6px; line-height: 1.4; }
.pillrow{ display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
.pill{
display:inline-flex; gap: 8px; align-items:center;
padding: 7px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(255,255,255,0.03);
font-size: 12px;
color: var(--muted);
}
.dot{ width: 9px; height: 9px; border-radius: 999px; background: var(--accent); box-shadow: 0 0 0 3px rgba(95,179,255,0.14); }
.nav{
margin-top: 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: rgba(21,25,35,0.75);
box-shadow: var(--shadow);
padding: 10px 10px;
position: sticky;
top: 10px;
z-index: 5;
}
.navrow{ display:flex; align-items:center; justify-content:space-between; gap: 10px; flex-wrap:wrap; }
.search{ flex: 1 1 280px; min-width: 220px; padding: 9px 10px; border-radius: 999px; border: 1px solid rgba(231,234,240,0.12); background: rgba(16,20,29,0.60); color: rgba(231,234,240,0.92); outline:none; }
.search:focus{ border-color: rgba(95,179,255,0.45); box-shadow: 0 0 0 3px rgba(95,179,255,0.12); }
.navlinks{ display:flex; gap: 8px; flex-wrap:wrap; align-items:center; }
.matches{ color: var(--muted); font-size: 12px; padding: 4px 8px; border-radius: 999px; border: 1px solid rgba(231,234,240,0.10); background: rgba(255,255,255,0.02); }
.nav a{
display:inline-block;
padding: 8px 10px;
border-radius: 999px;
border: 1px solid rgba(231,234,240,0.10);
background: rgba(255,255,255,0.02);
color: rgba(231,234,240,0.86);
font-size: 12px;
white-space: nowrap;
}
.nav a:hover{
border-color: rgba(95,179,255,0.35);
background: rgba(95,179,255,0.10);
text-decoration:none;
}
.grid{ display:grid; grid-template-columns: 1fr; gap: 14px; margin-top: 14px; }
.panel{
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--panel);
box-shadow: var(--shadow);
overflow:hidden;
}
.panel-h{
padding: 12px 14px;
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01));
display:flex; align-items:center; justify-content:space-between; gap: 10px;
}
.panel-h h2{
margin:0;
font-size: 14px;
letter-spacing: 0.2px;
font-weight: 800;
color: rgba(231,234,240,0.92);
}
.panel-b{ padding: 12px 14px 14px; color: rgba(231,234,240,0.90); }
.panel-b p{ margin: 10px 0; color: rgba(231,234,240,0.86); line-height: 1.55; }
.panel-b ul, .panel-b ol{ margin: 10px 0 10px 18px; color: rgba(231,234,240,0.86); }
.panel-b li{ margin: 6px 0; }
.section{ scroll-margin-top: 12px; }
.section h1{ font-size: 20px; margin: 6px 0 10px; }
.section h2{ font-size: 17px; margin: 18px 0 10px; }
.section h3{ font-size: 14px; margin: 16px 0 8px; color: rgba(231,234,240,0.92); }
.section h4{ font-size: 13px; margin: 14px 0 6px; color: rgba(231,234,240,0.88); }
.toplink{ display:inline-block; margin-top: 10px; font-size: 12px; color: var(--muted); }
.toplink:hover{ color: rgba(231,234,240,0.90); text-decoration:none; }
</style>
</head>
<body>
<div class="wrap">
<header class="top" id="top">
<div class="brand">
<div class="logo icon" aria-hidden="true"> </div>
<div>
<div class="title">🗂️ Majoor Assets Manager — User Guide</div>
<div class="subtitle">Generated from <code>docs/</code> for a readable single-page experience.</div>
</div>
</div>
<div class="pillrow">
<div class="pill"><span class="dot"></span> Docs: 33</div>
<div class="pill"><span class="dot"></span> Updated: January 2026</div>
</div>
</header>
<nav class="nav">
<div class="navrow">
<input id="q" class="search" placeholder="Search (filters + highlights)..." />
<div id="matches" class="matches">Sections: 34</div>
<div class="navlinks">
<a class="navlink" href="#top" data-text="top">Top</a><a class="navlink" href="#documentation_index" data-text="documentation_index.md documentation index (curated list)">DOCUMENTATION_INDEX.md</a><a class="navlink" href="#ai_features" data-text="ai_features.md additional documentation">AI_FEATURES.md</a><a class="navlink" href="#api_reference" data-text="api_reference.md additional documentation">API_REFERENCE.md</a><a class="navlink" href="#architecture_map" data-text="architecture_map.md additional documentation">ARCHITECTURE_MAP.md</a><a class="navlink" href="#contributing" data-text="contributing.md additional documentation">CONTRIBUTING.md</a><a class="navlink" href="#custom_nodes" data-text="custom_nodes.md additional documentation">CUSTOM_NODES.md</a><a class="navlink" href="#db_maintenance" data-text="db_maintenance.md additional documentation">DB_MAINTENANCE.md</a><a class="navlink" href="#dependency_policy" data-text="dependency_policy.md additional documentation">DEPENDENCY_POLICY.md</a><a class="navlink" href="#desktop_popup_workaround" data-text="desktop_popup_workaround.md additional documentation">DESKTOP_POPUP_WORKAROUND.md</a><a class="navlink" href="#drag_drop" data-text="drag_drop.md additional documentation">DRAG_DROP.md</a><a class="navlink" href="#floating_viewer_workflow_sidebar" data-text="floating_viewer_workflow_sidebar.md additional documentation">FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</a><a class="navlink" href="#frontend_imperative_design" data-text="frontend_imperative_design.md additional documentation">FRONTEND_IMPERATIVE_DESIGN.md</a><a class="navlink" href="#frontend_lifecycle_conventions" data-text="frontend_lifecycle_conventions.md additional documentation">FRONTEND_LIFECYCLE_CONVENTIONS.md</a><a class="navlink" href="#graph_map" data-text="graph_map.md additional documentation">GRAPH_MAP.md</a><a class="navlink" href="#hotkeys_shortcuts" data-text="hotkeys_shortcuts.md additional documentation">HOTKEYS_SHORTCUTS.md</a><a class="navlink" href="#installation" data-text="installation.md additional documentation">INSTALLATION.md</a><a class="navlink" href="#mfv_guide" data-text="mfv_guide.md additional documentation">MFV_GUIDE.md</a><a class="navlink" href="#migration_ts_tracker" data-text="migration_ts_tracker.md additional documentation">MIGRATION_TS_TRACKER.md</a><a class="navlink" href="#plugin_quick_reference" data-text="plugin_quick_reference.md additional documentation">PLUGIN_QUICK_REFERENCE.md</a><a class="navlink" href="#plugin_system_design" data-text="plugin_system_design.md additional documentation">PLUGIN_SYSTEM_DESIGN.md</a><a class="navlink" href="#plugin_system_implementation_summary" data-text="plugin_system_implementation_summary.md additional documentation">PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md</a><a class="navlink" href="#privacy_offline" data-text="privacy_offline.md additional documentation">PRIVACY_OFFLINE.md</a><a class="navlink" href="#ratings_tags_collections" data-text="ratings_tags_collections.md additional documentation">RATINGS_TAGS_COLLECTIONS.md</a><a class="navlink" href="#search_filtering" data-text="search_filtering.md additional documentation">SEARCH_FILTERING.md</a><a class="navlink" href="#security_env_vars" data-text="security_env_vars.md additional documentation">SECURITY_ENV_VARS.md</a><a class="navlink" href="#settings_configuration" data-text="settings_configuration.md additional documentation">SETTINGS_CONFIGURATION.md</a><a class="navlink" href="#shortcuts" data-text="shortcuts.md additional documentation">SHORTCUTS.md</a><a class="navlink" href="#testing" data-text="testing.md additional documentation">TESTING.md</a><a class="navlink" href="#threat_model" data-text="threat_model.md additional documentation">THREAT_MODEL.md</a><a class="navlink" href="#viewer_feature_tutorial" data-text="viewer_feature_tutorial.md additional documentation">VIEWER_FEATURE_TUTORIAL.md</a><a class="navlink" href="#workflows" data-text="workflows.md additional documentation">WORKFLOWS.md</a><a class="navlink" href="#workflow_scope_implementation_plan" data-text="workflow_scope_implementation_plan.md additional documentation">WORKFLOW_SCOPE_IMPLEMENTATION_PLAN.md</a><a class="navlink" href="#node-stream-reactivation" data-text="node-stream-reactivation.md additional documentation">node-stream-reactivation.md</a>
</div>
</div>
</nav>
<main class="grid">
<section class="panel section" id="intro">
<div class="panel-h"><h2>Introduction</h2></div>
<div class="panel-b">
<p>Tip: edit <code>docs/DOCUMENTATION_INDEX.md</code> to change the curated order, then regenerate with <code>python scripts/build_user_guide_html.py</code>.</p>
</div>
</section>
<section class="panel section" id="documentation_index">
<div class="panel-h"><h2>DOCUMENTATION_INDEX.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Documentation index (curated list)</div>
<div class="section"><h1 id="documentation_index-majoor-assets-manager-documentation-index">Majoor Assets Manager - Documentation Index</h1>
<p>Welcome to the documentation hub for <strong>Majoor Assets Manager</strong> for ComfyUI.</p>
<p>Majoor is best understood as one workflow:</p>
<ol>
<li><strong>Browse</strong> your ComfyUI media library.</li>
<li><strong>Inspect</strong> prompts, metadata, workflows, and visual details.</li>
<li><strong>Reuse</strong> useful assets and workflow context back in ComfyUI.</li>
</ol>
<p><strong>Current Version</strong>: 2.4.9 <strong>Last Updated</strong>: June 11, 2026</p>
<h2 id="documentation_index-start-here">Start Here</h2>
<div class="table-wrap"><table>
<thead><tr><th>Goal</th><th>Read This</th></tr></thead>
<tbody>
<tr><td>Install the extension</td><td><strong><a href="#installation">Installation Guide</a></strong></td></tr>
<tr><td>Learn the daily workflow</td><td><strong><a href="user_guide.html">User Guide (HTML)</a></strong></td></tr>
<tr><td>Find assets quickly</td><td><strong><a href="#search_filtering">Search &amp;amp; Filtering</a></strong></td></tr>
<tr><td>Learn shortcuts</td><td><strong><a href="#hotkeys_shortcuts">Hotkeys &amp;amp; Shortcuts</a></strong></td></tr>
<tr><td>See what changed</td><td><strong><a href="CHANGELOG.md">Changelog</a></strong></td></tr>
</tbody></table></div>
<h2 id="documentation_index-browse">Browse</h2>
<p>Use these docs for the main library experience: grid browsing, search, organization, and file actions.</p>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#search_filtering">SEARCH_FILTERING.md</a></strong></td><td>Full-text search, filters, sorting, and discovery</td></tr>
<tr><td><strong><a href="#workflows">WORKFLOWS.md</a></strong></td><td>Workflow tab library: save, import, load, inspect, categorize, thumbnail, and tag workflows</td></tr>
<tr><td><strong><a href="#ratings_tags_collections">RATINGS_TAGS_COLLECTIONS.md</a></strong></td><td>Ratings, tags, collections, and organization</td></tr>
<tr><td><strong><a href="#drag_drop">DRAG_DROP.md</a></strong></td><td>Drag to canvas, staging, OS drag-out, clean export, ZIP download</td></tr>
<tr><td><strong><a href="#db_maintenance">DB_MAINTENANCE.md</a></strong></td><td>Index database reset, recovery, and index directory configuration</td></tr>
</tbody></table></div>
<h2 id="documentation_index-inspect">Inspect</h2>
<p>Use these docs when you want to understand how an asset was made or compare outputs.</p>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#mfv_guide">MFV_GUIDE.md</a></strong></td><td>Majoor Floating Viewer: live review, compare modes, pins, streams</td></tr>
<tr><td><strong><a href="#viewer_feature_tutorial">VIEWER_FEATURE_TUTORIAL.md</a></strong></td><td>Viewer tools, analysis overlays, media controls</td></tr>
<tr><td><strong><a href="#graph_map">GRAPH_MAP.md</a></strong></td><td>Workflow graph navigation and node detail inspection</td></tr>
<tr><td><strong><a href="#floating_viewer_workflow_sidebar">FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</a></strong></td><td>Node Parameters sidebar in the Floating Viewer</td></tr>
</tbody></table></div>
<h2 id="documentation_index-reuse">Reuse</h2>
<p>Use these docs for moving assets and workflow information back into ComfyUI.</p>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#workflows">WORKFLOWS.md</a></strong></td><td>Save, import, load, inspect, and organize reusable ComfyUI workflows</td></tr>
<tr><td><strong><a href="#drag_drop">DRAG_DROP.md</a></strong></td><td>Load assets onto nodes or canvas, stage files to input</td></tr>
<tr><td><strong><a href="#custom_nodes">CUSTOM_NODES.md</a></strong></td><td>Majoor Save Image, Majoor Save Video, and Majoor GenInfo Override node reference</td></tr>
<tr><td><strong><a href="#shortcuts">SHORTCUTS.md</a></strong></td><td>Quick gestures such as S+Drag and L+Drop</td></tr>
</tbody></table></div>
<h2 id="documentation_index-advanced-features">Advanced Features</h2>
<p>These are powerful capabilities, but they are not required for the basic Browse / Inspect / Reuse workflow.</p>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#ai_features">AI_FEATURES.md</a></strong></td><td>Semantic search, visual similarity, auto-tags, captions, vector backfill</td></tr>
<tr><td><strong><a href="#privacy_offline">PRIVACY_OFFLINE.md</a></strong></td><td>Local AI behavior, model downloads, offline use, token clarification</td></tr>
<tr><td><strong><a href="#settings_configuration">SETTINGS_CONFIGURATION.md</a></strong></td><td>Runtime settings, UI settings, index path overrides, environment variables</td></tr>
<tr><td><strong><a href="#security_env_vars">SECURITY_ENV_VARS.md</a></strong></td><td>Remote access security, API tokens, safe defaults</td></tr>
<tr><td><strong><a href="#threat_model">THREAT_MODEL.md</a></strong></td><td>Threats, mitigations, and residual risk</td></tr>
<tr><td><strong><a href="#api_reference">API_REFERENCE.md</a></strong></td><td>Backend endpoint reference</td></tr>
</tbody></table></div>
<h2 id="documentation_index-development-and-architecture">Development And Architecture</h2>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#architecture_map">ARCHITECTURE_MAP.md</a></strong></td><td>Package responsibilities and internal boundaries</td></tr>
<tr><td><strong><a href="#testing">TESTING.md</a></strong></td><td>Tests, reports, and quality gate</td></tr>
<tr><td><strong><a href="#contributing">CONTRIBUTING.md</a></strong></td><td>Developer onboarding and contribution guidance</td></tr>
<tr><td><strong><a href="#dependency_policy">DEPENDENCY_POLICY.md</a></strong></td><td>Runtime, optional, and contributor dependency rules</td></tr>
<tr><td><strong><a href="docs/adr">adr/</a></strong></td><td>Architecture Decision Records</td></tr>
</tbody></table></div>
<h2 id="documentation_index-plugin-system">Plugin System</h2>
<div class="table-wrap"><table>
<thead><tr><th>Document</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong><a href="#plugin_quick_reference">PLUGIN_QUICK_REFERENCE.md</a></strong></td><td>Quick start for plugin development</td></tr>
<tr><td><strong><a href="#plugin_system_design">PLUGIN_SYSTEM_DESIGN.md</a></strong></td><td>Full plugin architecture design</td></tr>
<tr><td><strong><a href="#plugin_system_implementation_summary">PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md</a></strong></td><td>Implementation status and details</td></tr>
</tbody></table></div>
<h2 id="documentation_index-quality-and-verification">Quality And Verification</h2>
<p>Use the canonical gate from the repo root:</p>
<pre><code class="language-bash">python scripts/run_quality_gate.py</code></pre>
<p>Useful variants:</p>
<pre><code class="language-bash">python scripts/run_quality_gate.py --python-only --skip-tests
tox -e quality
npm run test:js</code></pre>
<p>The quality gate covers backend tests, frontend tests, linting, typing, dependency checks, and security scans.</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/DOCUMENTATION_INDEX.md</code>
</div>
</div>
</section>
<section class="panel section" id="ai_features">
<div class="panel-h"><h2>AI_FEATURES.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="ai_features-majoor-assets-manager-ai-features-guide">Majoor Assets Manager — AI Features Guide</h1>
<p><strong>Comprehensive guide to AI-powered features in Majoor Assets Manager</strong></p>
<p><em>Version: 2.4.9</em> <em>Last Updated: April 15, 2026</em></p>
<hr />
<h2 id="ai_features-table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#ai_features-overview">Overview</a></li>
<li><a href="#ai_features-ai-models-technologies">AI Models &amp;amp; Technologies</a></li>
<li><a href="#ai_features-enabling-ai-features">Enabling AI Features</a></li>
<li><a href="#ai_features-core-ai-features">Core AI Features</a></li>
<ul>
<li><a href="#ai_features-1-semantic-search">Semantic Search</a></li>
<li><a href="#ai_features-2-find-similar">Find Similar</a></li>
<li><a href="#ai_features-3-ai-auto-tags">AI Auto-Tags</a></li>
<li><a href="#ai_features-4-enhanced-captions-florence-2">Enhanced Captions (Florence-2)</a></li>
<li><a href="#ai_features-5-prompt-alignment-score">Prompt Alignment Score</a></li>
<li><a href="#ai_features-6-smart-collections">Smart Collections</a></li>
<li><a href="#ai_features-7-discover-groups">Discover Groups</a></li>
</ul>
<li><a href="#ai_features-how-it-works">How It Works</a></li>
<li><a href="#ai_features-configuration-tuning">Configuration &amp;amp; Tuning</a></li>
<li><a href="#ai_features-performance-considerations">Performance Considerations</a></li>
<li><a href="#ai_features-troubleshooting">Troubleshooting</a></li>
<li><a href="#ai_features-api-reference">API Reference</a></li>
<li><a href="#ai_features-privacy-security">Privacy &amp;amp; Security</a></li>
</ul>
<hr />
<h2 id="ai_features-overview">Overview</h2>
<p>Majoor Assets Manager includes a suite of <strong>AI-powered features</strong> 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.</p>
<h3 id="ai_features-what-ai-features-provide">What AI Features Provide</h3>
<div class="table-wrap"><table>
<thead><tr><th>Feature</th><th>Description</th><th>Use Case</th></tr></thead>
<tbody>
<tr><td><strong>Semantic Search</strong></td><td>Search using natural language instead of keywords</td><td>"sunset over mountains" finds matching images</td></tr>
<tr><td><strong>Find Similar</strong></td><td>Discover visually similar assets</td><td>Find variations of a successful generation</td></tr>
<tr><td><strong>AI Auto-Tags</strong></td><td>Automatic tag suggestions based on image content</td><td>Auto-tag "portrait", "landscape", "cyberpunk"</td></tr>
<tr><td><strong>Enhanced Captions</strong></td><td>AI-generated detailed image descriptions</td><td>Generate searchable captions for images</td></tr>
<tr><td><strong>Prompt Alignment</strong></td><td>Score how well image matches its prompt</td><td>Verify generation quality</td></tr>
<tr><td><strong>Smart Collections</strong></td><td>Auto-group assets by visual similarity</td><td>Create themed collections automatically</td></tr>
<tr><td><strong>Discover Groups</strong></td><td>Cluster library by visual themes</td><td>Explore your library's content patterns</td></tr>
</tbody></table></div>
<h3 id="ai_features-key-benefits">Key Benefits</h3>
<ul>
<li><strong>Natural Language Search</strong>: No need to remember exact filenames or tags</li>
<li><strong>Visual Discovery</strong>: Find assets by appearance, not just metadata</li>
<li><strong>Automated Organization</strong>: Reduce manual tagging workload</li>
<li><strong>Quality Insights</strong>: Understand prompt-image alignment</li>
<li><strong>Themed Grouping</strong>: Automatically discover patterns in your library</li>
</ul>
<hr />
<h2 id="ai_features-ai-models-technologies">AI Models &amp; Technologies</h2>
<h3 id="ai_features-primary-models">Primary Models</h3>
<div class="table-wrap"><table>
<thead><tr><th>Model</th><th>Purpose</th><th>Dimensions</th><th>Source</th></tr></thead>
<tbody>
<tr><td><strong>SigLIP2 SO400M</strong></td><td>Image &amp; text embeddings</td><td>1152</td><td>Google</td></tr>
<tr><td><strong>X-CLIP Base</strong></td><td>Video embeddings</td><td>768</td><td>Microsoft</td></tr>
<tr><td><strong>Florence-2 Base</strong></td><td>Image captioning</td><td>N/A</td><td>Microsoft</td></tr>
</tbody></table></div>
<h3 id="ai_features-technology-stack">Technology Stack</h3>
<ul>
<li><strong>Sentence Transformers</strong>: Model loading and inference</li>
<li><strong>Faiss</strong>: Vector similarity search (Facebook AI Similarity Search)</li>
<li><strong>Transformers</strong>: HuggingFace transformers for model inference</li>
<li><strong>SQLite with vector extension</strong>: Embedding storage</li>
</ul>
<h3 id="ai_features-model-download-caching">Model Download &amp; Caching</h3>
<p>Models are downloaded automatically on first use and cached locally:</p>
<pre><code>Cache Location: ~/.cache/huggingface/hub/
Models:
- google/siglip-so400m-patch14-384 (SigLIP2)
- microsoft/xclip-base-patch32 (X-CLIP)
- microsoft/Florence-2-base (Florence-2)</code></pre>
<p><strong>Initial Download Sizes:</strong></p>
<ul>
<li>SigLIP2: ~1.2 GB</li>
<li>X-CLIP: ~600 MB</li>
<li>Florence-2: ~800 MB</li>
</ul>
<hr />
<h2 id="ai_features-enabling-ai-features">Enabling AI Features</h2>
<h3 id="ai_features-method-1-via-settings-ui-recommended">Method 1: Via Settings UI (Recommended)</h3>
<ol>
<li>Open <strong>Assets Manager</strong> panel in ComfyUI</li>
<li>Click <strong>Settings</strong> (gear icon)</li>
<li>Find <strong>AI Features</strong> section</li>
<li>Toggle <strong>Enable AI semantic search</strong> to ON</li>
<li>Wait for initial model download (progress shown in console)</li>
<li>Click <strong>Save Settings</strong></li>
</ol>
<h3 id="ai_features-method-2-via-environment-variable">Method 2: Via Environment Variable</h3>
<p>Add to your environment before starting ComfyUI:</p>
<pre><code class="language-bash"># Windows (PowerShell)
$env:MJR_AM_ENABLE_VECTOR_SEARCH="1"
# Windows (Command Prompt)
set MJR_AM_ENABLE_VECTOR_SEARCH=1
# Linux/macOS
export MJR_AM_ENABLE_VECTOR_SEARCH=1</code></pre>
<h3 id="ai_features-verification">Verification</h3>
<p>After enabling:</p>
<ol>
<li>Check console for model loading messages:</li>
</ol>
<pre><code> INFO: Loading multimodal embedding model 'google/siglip-so400m-patch14-384' …
INFO: SigLIP2 model loaded and ready: 'google/siglip-so400m-patch14-384' (dim=1152)</code></pre>
<ol>
<li>In Assets Manager, look for:</li>
<ul>
<li>Sparkles icon (🔮) in search bar for semantic search toggle</li>
<li>"Find Similar" button in asset context menu</li>
<li>AI-related options in Collections panel</li>
</ul>
</ol>
<h3 id="ai_features-initial-setup-workflow">Initial Setup Workflow</h3>
<pre><code>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</code></pre>
<h3 id="ai_features-per-asset-vector-operations">Per-Asset Vector Operations</h3>
<p>You can trigger scan and backfill vectors for <strong>individual assets</strong> directly from the grid view:</p>
<h4 id="ai_features-via-card-status-dot">Via Card Status Dot</h4>
<ol>
<li><strong>Locate the status indicator</strong> on an asset card (small dot in corner)</li>
<li><strong>Click the status dot</strong> to open the asset actions menu</li>
<li>Select one of:</li>
<ul>
<li><strong>"Index Asset"</strong> — Trigger metadata scan for this asset only</li>
<li><strong>"Generate Vector"</strong> — Compute AI embedding for this asset</li>
<li><strong>"Re-index"</strong> — Force re-scan and re-compute vectors</li>
</ul>
</ol>
<h4 id="ai_features-via-sparkles-icon-prime-icon">Via Sparkles Icon (Prime Icon)</h4>
<ol>
<li><strong>Hover over an asset card</strong> to reveal action buttons</li>
<li><strong>Click the sparkles icon</strong> (🔮) if visible on the card</li>
<li>Choose from AI-specific actions:</li>
<ul>
<li><strong>Generate Caption</strong> — Run Florence-2 captioning</li>
<li><strong>Compute Alignment</strong> — Calculate prompt alignment score</li>
<li><strong>Suggest Tags</strong> — Generate AI auto-tags</li>
<li><strong>Find Similar</strong> — Show visually similar assets</li>
</ul>
</ol>
<h4 id="ai_features-when-to-use-per-asset-operations">When to Use Per-Asset Operations</h4>
<div class="table-wrap"><table>
<thead><tr><th>Scenario</th><th>Recommended Action</th></tr></thead>
<tbody>
<tr><td>New asset added after backfill</td><td>Click status dot → Generate Vector</td></tr>
<tr><td>Caption missing/outdated</td><td>Click sparkles → Generate Caption</td></tr>
<tr><td>Poor alignment score</td><td>Click sparkles → Re-compute Alignment</td></tr>
<tr><td>Asset not appearing in semantic search</td><td>Status dot → Index Asset, then Generate Vector</td></tr>
<tr><td>Testing AI features on single image</td><td>Use sparkles icon actions</td></tr>
</tbody></table></div>
<h4 id="ai_features-visual-indicators">Visual Indicators</h4>
<div class="table-wrap"><table>
<thead><tr><th>Indicator</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td>🟢 Green dot</td><td>Asset fully indexed with vectors</td></tr>
<tr><td>🟡 Yellow dot</td><td>Asset indexed, vectors pending</td></tr>
<tr><td>🔴 Red dot</td><td>Indexing failed or vectors unavailable</td></tr>
<tr><td>🔮 Sparkles visible</td><td>AI features available for this asset</td></tr>
<tr><td>⏳ Spinning icon</td><td>Processing in progress</td></tr>
</tbody></table></div>
<p>&lt;div class="tip"&gt; &lt;strong&gt;💡 Tip:&lt;/strong&gt; For best results, run a full backfill first, then use per-asset operations to update individual items as needed. &lt;/div&gt;</p>
<hr />
<h2 id="ai_features-core-ai-features">Core AI Features</h2>
<h3 id="ai_features-1-semantic-search">1. Semantic Search</h3>
<p>Search your asset library using natural language queries instead of keywords.</p>
<h4 id="ai_features-how-to-use">How to Use</h4>
<ol>
<li>Open <strong>Assets Manager</strong> panel</li>
<li>Click the <strong>sparkles icon</strong> (🔮) in the search bar to enable semantic mode</li>
<li>Type a natural language query:</li>
<ul>
<li>"sunset over mountains with orange sky"</li>
<li>"cyberpunk city at night"</li>
<li>"portrait of a woman with blue hair"</li>
</ul>
<li>Press Enter to search</li>
</ol>
<h4 id="ai_features-query-examples">Query Examples</h4>
<div class="table-wrap"><table>
<thead><tr><th>Query Type</th><th>Example</th></tr></thead>
<tbody>
<tr><td><strong>Visual description</strong></td><td>"red sports car on mountain road"</td></tr>
<tr><td><strong>Style/Mood</strong></td><td>"dark gothic castle in mist"</td></tr>
<tr><td><strong>Color-based</strong></td><td>"images with dominant blue tones"</td></tr>
<tr><td><strong>Subject-based</strong></td><td>"anime character with long white hair"</td></tr>
<tr><td><strong>French queries</strong></td><td>"chien" → finds dog images (auto-translated)</td></tr>
</tbody></table></div>
<h4 id="ai_features-how-it-works">How It Works</h4>
<ol>
<li>Your query is converted to a text embedding using SigLIP2</li>
<li>The embedding is compared against all indexed image embeddings</li>
<li>Results are ranked by cosine similarity (closest matches first)</li>
<li>Results are hydrated with full asset metadata for display</li>
</ol>
<h4 id="ai_features-tips">Tips</h4>
<ul>
<li>Be descriptive: "sunset beach with palm trees" works better than "beach"</li>
<li>Use color words: "green forest", "blue ocean", "red sunset"</li>
<li>Include style keywords: "anime", "photorealistic", "cyberpunk"</li>
<li>Semantic search works across languages (FR → EN auto-translation)</li>
</ul>
<hr />
<h3 id="ai_features-2-find-similar">2. Find Similar</h3>
<p>Discover assets visually similar to a selected asset.</p>
<h4 id="ai_features-how-to-use">How to Use</h4>
<ol>
<li><strong>Select an asset</strong> in the grid view (click once)</li>
<li><strong>Right-click</strong> → <strong>Find Similar</strong> (or click the Clone icon)</li>
<li>View visually similar assets ranked by similarity score</li>
<li>Adjust <code>top_k</code> parameter to show more/fewer results (default: 20)</li>
</ol>
<h4 id="ai_features-use-cases">Use Cases</h4>
<ul>
<li><strong>Variation Discovery</strong>: Find successful variations of a generation</li>
<li><strong>Style Exploration</strong>: Discover assets with similar aesthetic</li>
<li><strong>Quality Comparison</strong>: Compare different attempts at same prompt</li>
<li><strong>Theme Grouping</strong>: Find all assets matching a visual theme</li>
</ul>
<h4 id="ai_features-similarity-score">Similarity Score</h4>
<p>Results include a similarity score (0.0 to 1.0):</p>
<ul>
<li><strong>0.85+</strong>: Very similar (near-duplicates or variations)</li>
<li><strong>0.70-0.85</strong>: Highly similar (same theme/style)</li>
<li><strong>0.55-0.70</strong>: Moderately similar (related concepts)</li>
<li><strong>&lt;0.55</strong>: Loosely related (shared elements only)</li>
</ul>
<h4 id="ai_features-technical-details">Technical Details</h4>
<ul>
<li>Reference asset is excluded from results</li>
<li>Uses cosine similarity in embedding space</li>
<li>Searches across all scopes (Outputs, Inputs, Custom)</li>
<li>Respects current scope filter if applied</li>
</ul>
<hr />
<h3 id="ai_features-3-ai-auto-tags">3. AI Auto-Tags</h3>
<p>Automatically suggested tags based on image content analysis.</p>
<h4 id="ai_features-how-it-works">How It Works</h4>
<ol>
<li>Image embedding is compared against predefined tag vocabulary</li>
<li>Tags with similarity above threshold are suggested</li>
<li>Auto-tags are stored separately from user tags</li>
<li>Tags can be viewed and applied to assets</li>
</ol>
<h4 id="ai_features-available-auto-tags">Available Auto-Tags</h4>
<p>The system includes 20+ canonical tags:</p>
<div class="table-wrap"><table>
<thead><tr><th>Category</th><th>Tags</th></tr></thead>
<tbody>
<tr><td><strong>Subject</strong></td><td>portrait, landscape, character, food, vehicle, nature, architecture</td></tr>
<tr><td><strong>Style</strong></td><td>cyberpunk, anime, photorealistic, abstract, fantasy, sci-fi, horror</td></tr>
<tr><td><strong>Medium</strong></td><td>watercolor, pixel-art, 3d-render, sketch, black-and-white</td></tr>
<tr><td><strong>Content</strong></td><td>nsfw (adult content detection)</td></tr>
</tbody></table></div>
<h4 id="ai_features-how-to-view-auto-tags">How to View Auto-Tags</h4>
<ol>
<li>Open an asset in the <strong>Viewer</strong></li>
<li>Look at the <strong>Sidebar</strong> → <strong>AI Tags</strong> section</li>
<li>See suggested tags with confidence scores</li>
</ol>
<h4 id="ai_features-how-to-apply-auto-tags">How to Apply Auto-Tags</h4>
<ol>
<li>In Viewer sidebar, click <strong>Apply All</strong> to add all AI tags</li>
<li>Or click individual tags to add selectively</li>
<li>Tags are merged with your existing tags</li>
</ol>
<h4 id="ai_features-configuration">Configuration</h4>
<p>Adjust sensitivity via environment variable:</p>
<pre><code class="language-bash"># Lower threshold = more tags (less precise)
# Higher threshold = fewer tags (more precise)
MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.06 # Default</code></pre>
<hr />
<h3 id="ai_features-4-enhanced-captions-florence-2">4. Enhanced Captions (Florence-2)</h3>
<p>Generate detailed AI-powered image descriptions.</p>
<h4 id="ai_features-how-to-generate">How to Generate</h4>
<ol>
<li>Open an asset in the <strong>Viewer</strong></li>
<li>In the sidebar, find <strong>Image Description</strong> section</li>
<li>Click <strong>Generate</strong> button</li>
<li>Wait for caption generation (5-30 seconds)</li>
<li>Generated caption appears in sidebar</li>
</ol>
<h4 id="ai_features-caption-quality">Caption Quality</h4>
<p>Florence-2 generates detailed, descriptive captions:</p>
<p><strong>Example Input</strong>: Anime-style image of a girl with blue hair</p>
<p><strong>Generated Caption</strong>:</p>
<blockquote>"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."</blockquote>
<h4 id="ai_features-use-cases">Use Cases</h4>
<ul>
<li><strong>Accessibility</strong>: Describe images for visually impaired users</li>
<li><strong>Search Enhancement</strong>: Captions are indexed for semantic search</li>
<li><strong>Documentation</strong>: Auto-generate descriptions for asset catalogs</li>
<li><strong>Prompt Analysis</strong>: Compare generated caption with original prompt</li>
</ul>
<h4 id="ai_features-batch-generation">Batch Generation</h4>
<p>For multiple assets, use the API:</p>
<pre><code class="language-bash">POST /mjr/am/vector/caption/{asset_id}</code></pre>
<p>Or use the "Backfill vectors" feature which generates captions for all images.</p>
<h4 id="ai_features-storage">Storage</h4>
<ul>
<li>Captions stored in <code>asset_metadata.enhanced_caption</code> field</li>
<li>Persisted in SQLite database</li>
<li>Survives index resets (stored separately from vectors)</li>
</ul>
<hr />
<h3 id="ai_features-5-prompt-alignment-score">5. Prompt Alignment Score</h3>
<p>Measure how well an image matches its generation prompt.</p>
<h4 id="ai_features-what-it-measures">What It Measures</h4>
<p>The alignment score quantifies the semantic similarity between:</p>
<ul>
<li>The original generation prompt (from metadata)</li>
<li>The visual content of the image (from embedding)</li>
</ul>
<h4 id="ai_features-score-interpretation">Score Interpretation</h4>
<div class="table-wrap"><table>
<thead><tr><th>Score Range</th><th>Interpretation</th></tr></thead>
<tbody>
<tr><td><strong>0.80-1.00</strong></td><td>Excellent alignment (image matches prompt very well)</td></tr>
<tr><td><strong>0.65-0.80</strong></td><td>Good alignment (minor deviations)</td></tr>
<tr><td><strong>0.50-0.65</strong></td><td>Moderate alignment (noticeable differences)</td></tr>
<tr><td><strong>0.30-0.50</strong></td><td>Poor alignment (significant prompt drift)</td></tr>
<tr><td><strong>&lt;0.30</strong></td><td>Very poor alignment (image doesn't match prompt)</td></tr>
</tbody></table></div>
<h4 id="ai_features-how-its-calculated">How It's Calculated</h4>
<p>The score uses <strong>multi-signal fusion</strong>:</p>
<ol>
<li><strong>Multi-segment image↔text score</strong> (60% weight)</li>
<ul>
<li>Prompt split into segments</li>
<li>Each segment scored against image</li>
<li>Length-weighted average with best-segment bonus</li>
</ul>
</ol>
<ol>
<li><strong>Caption↔prompt text similarity</strong> (20% weight)</li>
<ul>
<li>Florence-2 caption compared to prompt</li>
<li>Text-to-text coherence measure</li>
</ul>
</ol>
<ol>
<li><strong>Semantic dimension score</strong> (20% weight)</li>
<ul>
<li>Subject, style, medium, mood, color dimensions</li>
<li>Each dimension scored separately</li>
</ul>
</ol>
<p><strong>Negative Prompt Penalty</strong>: If negative prompt exists, its similarity to image reduces the score (penalizes unwanted content leakage).</p>
<h4 id="ai_features-how-to-view">How to View</h4>
<ol>
<li>Open asset in <strong>Viewer</strong></li>
<li>Look for <strong>Prompt Alignment</strong> section in sidebar</li>
<li>Score displayed as percentage (0-100%)</li>
</ol>
<h4 id="ai_features-use-cases">Use Cases</h4>
<ul>
<li><strong>Quality Control</strong>: Identify generations that didn't match intent</li>
<li><strong>Prompt Engineering</strong>: Refine prompts based on alignment feedback</li>
<li><strong>Curation</strong>: Filter library by alignment quality</li>
<li><strong>Model Comparison</strong>: Compare different models' prompt adherence</li>
</ul>
<hr />
<h3 id="ai_features-6-smart-collections">6. Smart Collections</h3>
<p>Automatically create collections based on AI suggestions.</p>
<h4 id="ai_features-how-to-use">How to Use</h4>
<ol>
<li>Open <strong>Collections</strong> panel</li>
<li>Click <strong>Smart Suggestions</strong> button</li>
<li>Review AI-suggested collections</li>
<li>Click <strong>Create</strong> to accept a suggestion</li>
</ol>
<h4 id="ai_features-suggestion-types">Suggestion Types</h4>
<p>The AI analyzes your library and suggests:</p>
<ul>
<li><strong>Theme-based</strong>: "Cyberpunk Collection", "Nature Scenes"</li>
<li><strong>Style-based</strong>: "Anime Collection", "Photorealistic"</li>
<li><strong>Color-based</strong>: "Blue Tones", "Warm Colors"</li>
<li><strong>Subject-based</strong>: "Portraits", "Landscapes", "Vehicles"</li>
</ul>
<h4 id="ai_features-how-suggestions-work">How Suggestions Work</h4>
<ol>
<li>Asset embeddings are clustered using k-means</li>
<li>Each cluster is analyzed for common themes</li>
<li>Representative tags and descriptions generated</li>
<li>Suggestions presented for approval</li>
</ol>
<h4 id="ai_features-configuration">Configuration</h4>
<p>Adjust number of suggestions:</p>
<pre><code class="language-json">{
"k": 8 // Number of clusters (2-20)
}</code></pre>
<p>More clusters = more specific collections Fewer clusters = broader collections</p>
<hr />
<h3 id="ai_features-7-discover-groups">7. Discover Groups</h3>
<p>Cluster your entire library by visual similarity.</p>
<h4 id="ai_features-how-to-use">How to Use</h4>
<ol>
<li>Open <strong>Collections</strong> panel</li>
<li>Click <strong>Discover Groups</strong> button</li>
<li>Wait for clustering to complete</li>
<li>Browse discovered groups</li>
<li>Convert groups to collections as desired</li>
</ol>
<h4 id="ai_features-group-characteristics">Group Characteristics</h4>
<p>Each group includes:</p>
<ul>
<li><strong>Preview</strong>: Sample images from the group</li>
<li><strong>Size</strong>: Number of assets in group</li>
<li><strong>Tags</strong>: Common AI-suggested tags</li>
<li><strong>Representative Query</strong>: Natural language description</li>
</ul>
<h4 id="ai_features-use-cases">Use Cases</h4>
<ul>
<li><strong>Library Exploration</strong>: Discover patterns in your assets</li>
<li><strong>Cleanup</strong>: Identify duplicate or near-duplicate generations</li>
<li><strong>Curation</strong>: Find themed subsets for projects</li>
<li><strong>Analysis</strong>: Understand your generation habits</li>
</ul>
<h4 id="ai_features-performance">Performance</h4>
<p>Clustering time depends on library size:</p>
<div class="table-wrap"><table>
<thead><tr><th>Assets</th><th>Time (approx.)</th></tr></thead>
<tbody>
<tr><td>100-500</td><td>5-15 seconds</td></tr>
<tr><td>500-2000</td><td>15-60 seconds</td></tr>
<tr><td>2000-10000</td><td>1-5 minutes</td></tr>
<tr><td>10000+</td><td>5-15 minutes</td></tr>
</tbody></table></div>
<hr />
<h2 id="ai_features-how-it-works">How It Works</h2>
<h3 id="ai_features-architecture-overview">Architecture Overview</h3>
<pre><code>┌─────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘</code></pre>
<h3 id="ai_features-embedding-pipeline">Embedding Pipeline</h3>
<pre><code>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</code></pre>
<h3 id="ai_features-vector-storage-schema">Vector Storage Schema</h3>
<pre><code class="language-sql">CREATE TABLE asset_embeddings (
asset_id INTEGER PRIMARY KEY,
vector BLOB NOT NULL, -- Packed float32 array
model_name TEXT NOT NULL, -- e.g., "google/siglip-so400m-patch14-384"
aesthetic_score REAL, -- Prompt alignment score (0-1)
auto_tags TEXT, -- JSON array of tag strings
enhanced_caption TEXT, -- Florence-2 generated caption
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);</code></pre>
<hr />
<h2 id="ai_features-configuration-tuning">Configuration &amp; Tuning</h2>
<h3 id="ai_features-environment-variables">Environment Variables</h3>
<div class="table-wrap"><table>
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>MJR_AM_ENABLE_VECTOR_SEARCH</code></td><td><code>1</code></td><td>Enable/disable AI features</td></tr>
<tr><td><code>MJR_AM_VECTOR_INDEX_ON_SCAN</code></td><td><code>0</code></td><td>Compute vector embeddings automatically during scans</td></tr>
<tr><td><code>MJR_AM_VECTOR_CAPTION_ON_INDEX</code></td><td><code>0</code></td><td>Generate Florence captions during vector indexing/backfill</td></tr>
<tr><td><code>MJR_VECTOR_CONCURRENCY</code></td><td><code>2</code></td><td>Concurrent vector embedding workers; use <code>1</code> to reduce transient VRAM spikes</td></tr>
<tr><td><code>MJR_AM_VECTOR_UNLOAD_AFTER_USE</code></td><td><code>0</code></td><td>Unload Majoor AI models after heavy vector actions and clear torch CUDA cache</td></tr>
<tr><td><code>MJR_AM_VECTOR_MODEL</code></td><td><code>google/siglip-so400m-patch14-384</code></td><td>Image/text model</td></tr>
<tr><td><code>MJR_AM_VECTOR_VIDEO_MODEL</code></td><td><code>microsoft/xclip-base-patch32</code></td><td>Video model</td></tr>
<tr><td><code>MJR_AM_PROMPT_MODEL</code></td><td><code>microsoft/Florence-2-base</code></td><td>Caption model</td></tr>
<tr><td><code>MJR_AM_VECTOR_DIM</code></td><td><code>1152</code></td><td>Embedding dimension</td></tr>
<tr><td><code>MJR_AM_VECTOR_AUTOTAG_THRESHOLD</code></td><td><code>0.06</code></td><td>Auto-tag sensitivity</td></tr>
<tr><td><code>MJR_AM_VECTOR_SIMILAR_TOPK</code></td><td><code>20</code></td><td>Default similar results</td></tr>
<tr><td><code>MJR_AM_VECTOR_KEYFRAME_INTERVAL</code></td><td><code>5.0</code></td><td>Video keyframe interval (sec)</td></tr>
<tr><td><code>MJR_AM_VECTOR_BATCH_SIZE</code></td><td><code>32</code></td><td>Embedding batch size</td></tr>
<tr><td><code>MJR_AM_AI_VERBOSE_LOGS</code></td><td><code>0</code></td><td>Verbose AI logging</td></tr>
</tbody></table></div>
<h3 id="ai_features-model-selection">Model Selection</h3>
<h4 id="ai_features-imagetext-model-options">Image/Text Model Options</h4>
<div class="table-wrap"><table>
<thead><tr><th>Model</th><th>Dimensions</th><th>Speed</th><th>Quality</th><th>Use Case</th></tr></thead>
<tbody>
<tr><td><code>google/siglip-so400m-patch14-384</code></td><td>1152</td><td>Medium</td><td>High</td><td>Default, balanced</td></tr>
<tr><td><code>google/siglip-base-patch16-224</code></td><td>768</td><td>Fast</td><td>Medium</td><td>Lower RAM systems</td></tr>
<tr><td><code>google/siglip-large-patch16-384</code></td><td>1024</td><td>Slow</td><td>Very High</td><td>Quality-focused</td></tr>
</tbody></table></div>
<h4 id="ai_features-video-model-options">Video Model Options</h4>
<div class="table-wrap"><table>
<thead><tr><th>Model</th><th>Dimensions</th><th>Speed</th><th>Quality</th></tr></thead>
<tbody>
<tr><td><code>microsoft/xclip-base-patch32</code></td><td>768</td><td>Medium</td><td>Good</td></tr>
<tr><td><code>microsoft/xclip-large-patch14</code></td><td>1024</td><td>Slow</td><td>Better</td></tr>
</tbody></table></div>
<h4 id="ai_features-caption-model-options">Caption Model Options</h4>
<div class="table-wrap"><table>
<thead><tr><th>Model</th><th>Size</th><th>Speed</th><th>Quality</th></tr></thead>
<tbody>
<tr><td><code>microsoft/Florence-2-base</code></td><td>~230M</td><td>Fast</td><td>Good</td></tr>
<tr><td><code>microsoft/Florence-2-large</code></td><td>~580M</td><td>Medium</td><td>Better</td></tr>
</tbody></table></div>
<h3 id="ai_features-tuning-examples">Tuning Examples</h3>
<h4 id="ai_features-faster-embedding-lower-quality">Faster Embedding (Lower Quality)</h4>
<pre><code class="language-bash">export MJR_AM_VECTOR_MODEL="google/siglip-base-patch16-224"
export MJR_AM_VECTOR_BATCH_SIZE=64
export MJR_AM_VECTOR_DIM=768</code></pre>
<h4 id="ai_features-higher-quality-slower">Higher Quality (Slower)</h4>
<pre><code class="language-bash">export MJR_AM_VECTOR_MODEL="google/siglip-large-patch16-384"
export MJR_AM_PROMPT_MODEL="microsoft/Florence-2-large"
export MJR_AM_VECTOR_AUTOTAG_THRESHOLD=0.30</code></pre>
<h4 id="ai_features-low-ram-system">Low-RAM System</h4>
<pre><code class="language-bash">export MJR_AM_VECTOR_BATCH_SIZE=8
export MJR_AM_VECTOR_MODEL="google/siglip-base-patch16-224"</code></pre>
<h4 id="ai_features-low-vram-comfyui-setup">Low-VRAM ComfyUI Setup</h4>
<p>Use these when ComfyUI generation models need priority over Majoor AI features:</p>
<pre><code class="language-bash">export MJR_AM_ENABLE_VECTOR_SEARCH=0
export MJR_AM_VECTOR_INDEX_ON_SCAN=0
export MJR_AM_VECTOR_CAPTION_ON_INDEX=0
export MJR_VECTOR_CONCURRENCY=1
export MJR_AM_VECTOR_UNLOAD_AFTER_USE=1</code></pre>
<p>The same controls are available in Settings -&gt; Search -&gt; AI:</p>
<ul>
<li><strong>Enable AI semantic search</strong></li>
<li><strong>Index vectors during scans</strong></li>
<li><strong>Generate AI captions during indexing</strong></li>
<li><strong>Vector indexing concurrency</strong></li>
<li><strong>Unload AI models after use</strong></li>
<li><strong>Memory purge now</strong></li>
<li><strong>Memory purge</strong> in Index Status</li>
</ul>
<hr />
<h2 id="ai_features-performance-considerations">Performance Considerations</h2>
<h3 id="ai_features-resource-requirements">Resource Requirements</h3>
<div class="table-wrap"><table>
<thead><tr><th>Component</th><th>Minimum</th><th>Recommended</th><th>Optimal</th></tr></thead>
<tbody>
<tr><td><strong>RAM</strong></td><td>8 GB</td><td>16 GB</td><td>32 GB</td></tr>
<tr><td><strong>VRAM</strong></td><td>0 GB (CPU)</td><td>4 GB</td><td>8+ GB</td></tr>
<tr><td><strong>Storage</strong></td><td>5 GB free</td><td>10 GB free</td><td>20+ GB free</td></tr>
<tr><td><strong>CPU</strong></td><td>4 cores</td><td>8 cores</td><td>12+ cores</td></tr>
</tbody></table></div>
<h3 id="ai_features-model-loading-times">Model Loading Times</h3>
<div class="table-wrap"><table>
<thead><tr><th>Model</th><th>Cold Load</th><th>Cached</th></tr></thead>
<tbody>
<tr><td>SigLIP2 SO400M</td><td>30-60s</td><td>5-10s</td></tr>
<tr><td>X-CLIP Base</td><td>20-40s</td><td>3-8s</td></tr>
<tr><td>Florence-2 Base</td><td>15-30s</td><td>2-5s</td></tr>
</tbody></table></div>
<h3 id="ai_features-embedding-speed">Embedding Speed</h3>
<div class="table-wrap"><table>
<thead><tr><th>Asset Type</th><th>CPU Only</th><th>GPU (RTX 3060)</th></tr></thead>
<tbody>
<tr><td>Image (1080p)</td><td>2-5 sec</td><td>0.5-1 sec</td></tr>
<tr><td>Video (1 min)</td><td>10-30 sec</td><td>3-10 sec</td></tr>
<tr><td>Text caption</td><td>0.5-2 sec</td><td>0.1-0.5 sec</td></tr>
</tbody></table></div>
<h3 id="ai_features-backfill-performance">Backfill Performance</h3>
<p><strong>Backfill vectors</strong> computes embeddings for all assets without vectors:</p>
<div class="table-wrap"><table>
<thead><tr><th>Library Size</th><th>Time (CPU)</th><th>Time (GPU)</th></tr></thead>
<tbody>
<tr><td>100 assets</td><td>2-5 min</td><td>30-60 sec</td></tr>
<tr><td>500 assets</td><td>10-25 min</td><td>2-5 min</td></tr>
<tr><td>1000 assets</td><td>20-50 min</td><td>5-10 min</td></tr>
<tr><td>5000 assets</td><td>2-4 hours</td><td>25-50 min</td></tr>
</tbody></table></div>
<h3 id="ai_features-optimization-tips">Optimization Tips</h3>
<ol>
<li><strong>Enable GPU acceleration</strong> (CUDA):</li>
</ol>
<pre><code class="language-bash"> pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118</code></pre>
<ol>
<li><strong>Adjust batch size</strong> for your hardware:</li>
<ul>
<li>Low RAM: <code>MJR_AM_VECTOR_BATCH_SIZE=8</code></li>
<li>High RAM: <code>MJR_AM_VECTOR_BATCH_SIZE=64</code></li>
</ul>
</ol>
<ol>
<li><strong>Use CPU-only mode</strong> if GPU memory is limited:</li>
</ol>
<pre><code class="language-bash"> export CUDA_VISIBLE_DEVICES=""</code></pre>
<ol>
<li><strong>Unload Majoor AI models after heavy actions</strong>:</li>
<ul>
<li>Enable <strong>Unload AI models after use</strong> in Settings -&gt; Search -&gt; AI.</li>
<li>Or click <strong>Memory purge</strong> in Index Status to immediately drop Majoor SigLIP/X-CLIP/Florence references, ask ComfyUI to unload loaded generation models, and call torch CUDA cache cleanup.</li>
<li>The purge is skipped while ComfyUI is actively executing.</li>
</ul>
</ol>
<ol>
<li><strong>Limit index size</strong> for large libraries:</li>
<ul>
<li>Vector searcher caps at 100,000 assets</li>
<li>Most recent assets prioritized</li>
</ul>
</ol>
<ol>
<li><strong>Schedule backfill</strong> during off-hours:</li>
<ul>
<li>Run overnight for large libraries</li>
<li>System remains usable during backfill</li>
</ul>
</ol>
<hr />
<h2 id="ai_features-troubleshooting">Troubleshooting</h2>
<h3 id="ai_features-common-issues">Common Issues</h3>
<h4 id="ai_features-1-ai-features-not-appearing">1. AI Features Not Appearing</h4>
<p><strong>Symptoms</strong>: No sparkles icon, no "Find Similar" button</p>
<p><strong>Solutions</strong>:</p>
<ul>
<li>Check if AI features are enabled in Settings</li>
<li>Verify <code>MJR_AM_ENABLE_VECTOR_SEARCH=1</code> in environment</li>
<li>Restart ComfyUI after enabling</li>
<li>Check console for model loading errors</li>
</ul>
<h4 id="ai_features-2-model-download-fails">2. Model Download Fails</h4>
<p><strong>Symptoms</strong>: Error downloading models, timeout</p>
<p><strong>Solutions</strong>:</p>
<pre><code class="language-bash"># Check internet connectivity
ping huggingface.co
# Manual model download
huggingface-cli download google/siglip-so400m-patch14-384
# Set HF mirror (for China/regions with restrictions)
export HF_ENDPOINT=https://hf-mirror.com</code></pre>
<h4 id="ai_features-3-out-of-memory-oom">3. Out of Memory (OOM)</h4>
<p><strong>Symptoms</strong>: System freezes, crashes during embedding</p>
<p><strong>Solutions</strong>:</p>
<ul>
<li>Reduce batch size: <code>MJR_AM_VECTOR_BATCH_SIZE=8</code></li>
<li>Set vector concurrency to one worker: <code>MJR_VECTOR_CONCURRENCY=1</code></li>
<li>Disable automatic vector indexing during scans: <code>MJR_AM_VECTOR_INDEX_ON_SCAN=0</code></li>
<li>Disable Florence captions during vector indexing: <code>MJR_AM_VECTOR_CAPTION_ON_INDEX=0</code></li>
<li>Enable model cleanup after AI actions: <code>MJR_AM_VECTOR_UNLOAD_AFTER_USE=1</code></li>
<li>Close other applications</li>
<li>Use CPU-only mode to free GPU RAM</li>
<li>Restart ComfyUI to clear memory</li>
</ul>
<h4 id="ai_features-4-slow-performance">4. Slow Performance</h4>
<p><strong>Symptoms</strong>: Searches take &gt;5 seconds, UI lag</p>
<p><strong>Solutions</strong>:</p>
<ul>
<li>Reduce library size (split into multiple scopes)</li>
<li>Use GPU acceleration if available</li>
<li>Increase system RAM</li>
<li>Disable verbose logging: <code>MJR_AM_AI_VERBOSE_LOGS=0</code></li>
</ul>
<h4 id="ai_features-5-poor-search-results">5. Poor Search Results</h4>
<p><strong>Symptoms</strong>: Irrelevant results for queries</p>
<p><strong>Solutions</strong>:</p>
<ul>
<li>Ensure vectors are backfilled: Index Status → Backfill vectors</li>
<li>Use more descriptive queries</li>
<li>Check model loaded correctly in console</li>
<li>Verify embedding dimension matches model</li>
</ul>
<h4 id="ai_features-6-caption-generation-fails">6. Caption Generation Fails</h4>
<p><strong>Symptoms</strong>: "Generate" button shows error, no caption</p>
<p><strong>Solutions</strong>:</p>
<ul>
<li>Check Florence-2 model loaded (console logs)</li>
<li>Verify asset is an image (not video)</li>
<li>Increase timeout: <code>MJR_AM_DB_TIMEOUT=120</code></li>
<li>Check disk space for model cache</li>
</ul>
<h3 id="ai_features-diagnostic-commands">Diagnostic Commands</h3>
<pre><code class="language-bash"># Check model cache
ls -la ~/.cache/huggingface/hub/
# Verify Python packages
pip list | grep -E "sentence-transformers|transformers|faiss"
# Test model loading
python -c "from sentence_transformers import SentenceTransformer; m = SentenceTransformer('google/siglip-so400m-patch14-384'); print('OK')"
# Check vector index status
curl http://localhost:8188/mjr/am/vector/stats</code></pre>
<h3 id="ai_features-log-analysis">Log Analysis</h3>
<p>Enable verbose logging for debugging:</p>
<pre><code class="language-bash">export MJR_AM_AI_VERBOSE_LOGS=1</code></pre>
<p>Look for these log patterns:</p>
<pre><code># 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</code></pre>
<hr />
<h2 id="ai_features-api-reference">API Reference</h2>
<h3 id="ai_features-semantic-search">Semantic Search</h3>
<pre><code class="language-http">GET /mjr/am/vector/search?q={query}&amp;top_k={count}&amp;scope={scope}</code></pre>
<p><strong>Parameters</strong>:</p>
<ul>
<li><code>q</code> (required): Natural language query</li>
<li><code>top_k</code> (optional): Max results (default: 20, max: 200)</li>
<li><code>scope</code> (optional): <code>output</code>, <code>input</code>, <code>custom</code>, <code>all</code></li>
</ul>
<p><strong>Example</strong>:</p>
<pre><code class="language-bash">curl "http://localhost:8188/mjr/am/vector/search?q=sunset%20beach&amp;top_k=10"</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": [
{
"id": 12345,
"asset_id": 12345,
"filepath": "/path/to/image.png",
"filename": "image.png",
"kind": "image",
"_vectorScore": 0.8923
}
]
}</code></pre>
<h3 id="ai_features-find-similar">Find Similar</h3>
<pre><code class="language-http">GET /mjr/am/vector/similar/{asset_id}?top_k={count}</code></pre>
<p><strong>Parameters</strong>:</p>
<ul>
<li><code>asset_id</code> (required): Reference asset ID</li>
<li><code>top_k</code> (optional): Max results (default: 20)</li>
</ul>
<p><strong>Example</strong>:</p>
<pre><code class="language-bash">curl "http://localhost:8188/mjr/am/vector/similar/12345?top_k=10"</code></pre>
<h3 id="ai_features-prompt-alignment">Prompt Alignment</h3>
<pre><code class="language-http">GET /mjr/am/vector/alignment/{asset_id}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": 0.7523
}</code></pre>
<h3 id="ai_features-generate-caption">Generate Caption</h3>
<pre><code class="language-http">POST /mjr/am/vector/caption/{asset_id}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": "An anime-style illustration of..."
}</code></pre>
<h3 id="ai_features-auto-tags">Auto-Tags</h3>
<pre><code class="language-http">GET /mjr/am/vector/auto-tags/{asset_id}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": ["anime", "portrait", "fantasy"]
}</code></pre>
<h3 id="ai_features-suggest-collections">Suggest Collections</h3>
<pre><code class="language-http">POST /mjr/am/vector/suggest-collections
Content-Type: application/json
{"k": 8}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"suggestions": [
{
"name": "Cyberpunk Collection",
"query": "cyberpunk neon city night",
"estimated_size": 45,
"tags": ["cyberpunk", "sci-fi", "night"]
}
]
}
}</code></pre>
<h3 id="ai_features-vector-stats">Vector Stats</h3>
<pre><code class="language-http">GET /mjr/am/vector/stats</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"total": 1234,
"avg_score": 0.6523,
"dim": 1152,
"enabled": true
}
}</code></pre>
<h3 id="ai_features-re-index-asset">Re-index Asset</h3>
<pre><code class="language-http">POST /mjr/am/vector/index/{asset_id}</code></pre>
<p>Force re-computation of embedding for a single asset.</p>
<h3 id="ai_features-unload-majoor-ai-models">Unload Majoor AI Models</h3>
<pre><code class="language-http">POST /mjr/am/settings/vector-search/unload</code></pre>
<p>Drops Majoor vector/caption model references, invalidates the in-memory vector searcher, asks ComfyUI to unload loaded models when available, and calls torch CUDA cache cleanup. The endpoint is write-protected and returns <code>COMFY_BUSY</code> while ComfyUI is actively executing.</p>
<hr />
<h2 id="ai_features-privacy-security">Privacy &amp; Security</h2>
<p>For a dedicated user-facing explanation focused on privacy, offline behavior, and token meaning, see <a href="#privacy_offline">PRIVACY_OFFLINE.md</a>.</p>
<h3 id="ai_features-data-privacy">Data Privacy</h3>
<ul>
<li><strong>Local Processing</strong>: All AI inference runs locally on your machine</li>
<li><strong>No Cloud Upload</strong>: Images and prompts never leave your computer</li>
<li><strong>Local Cache</strong>: Models cached in <code>~/.cache/huggingface/hub/</code></li>
<li><strong>No Telemetry</strong>: No usage data sent to developers</li>
</ul>
<p>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.</p>
<h3 id="ai_features-network-access">Network Access</h3>
<p>Initial model download requires internet access:</p>
<ul>
<li>Downloads from HuggingFace CDN</li>
<li>One-time download per model</li>
<li>Subsequent uses work offline</li>
<li>Optional HuggingFace token only affects HuggingFace Hub downloads and rate limits</li>
</ul>
<h3 id="ai_features-token-clarification">Token Clarification</h3>
<p>Majoor exposes two different token concepts that are easy to confuse:</p>
<ul>
<li><strong>HuggingFace token</strong>: optional, used for downloading model files from HuggingFace Hub with better rate limits. It is not a hosted AI inference key.</li>
<li><strong>Majoor API token</strong>: 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.</li>
</ul>
<h3 id="ai_features-offline-use">Offline Use</h3>
<p>Offline use is supported once the required models are already cached locally.</p>
<ul>
<li>If AI models are already present in the HuggingFace cache, AI features can run without internet access.</li>
<li>If models are not cached yet, the first model bootstrap requires network access.</li>
<li>Non-AI Majoor features do not depend on HuggingFace model downloads.</li>
</ul>
<h3 id="ai_features-security-considerations">Security Considerations</h3>
<ol>
<li><strong>Model Integrity</strong>: Models downloaded from official HuggingFace repos</li>
<li><strong>Model Code Surface</strong>: AI inference is local, but some model loading still depends on upstream HuggingFace/Transformers model packages and compatibility loaders</li>
<li><strong>Sandboxing</strong>: Models run in same process as ComfyUI</li>
<li><strong>Resource Limits</strong>: Built-in rate limiting prevents abuse</li>
</ol>
<h3 id="ai_features-environment-isolation">Environment Isolation</h3>
<p>For enhanced security, run in isolated environment:</p>
<pre><code class="language-bash"># Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt -r requirements-vector.txt</code></pre>
<hr />
<h2 id="ai_features-appendix-model-cards">Appendix: Model Cards</h2>
<h3 id="ai_features-siglip2-so400m">SigLIP2 SO400M</h3>
<ul>
<li><strong>Publisher</strong>: Google</li>
<li><strong>Architecture</strong>: Vision-Language Transformer</li>
<li><strong>Training</strong>: Contrastive image-text pairs</li>
<li><strong>License</strong>: Apache 2.0</li>
<li><strong>HuggingFace</strong>: https://huggingface.co/google/siglip-so400m-patch14-384</li>
</ul>
<h3 id="ai_features-x-clip-base">X-CLIP Base</h3>
<ul>
<li><strong>Publisher</strong>: Microsoft</li>
<li><strong>Architecture</strong>: Video-Language Transformer</li>
<li><strong>Training</strong>: Video-text pairs</li>
<li><strong>License</strong>: MIT</li>
<li><strong>HuggingFace</strong>: https://huggingface.co/microsoft/xclip-base-patch32</li>
</ul>
<h3 id="ai_features-florence-2-base">Florence-2 Base</h3>
<ul>
<li><strong>Publisher</strong>: Microsoft</li>
<li><strong>Architecture</strong>: Vision-Language Model</li>
<li><strong>Training</strong>: Image-caption pairs</li>
<li><strong>License</strong>: MIT</li>
<li><strong>HuggingFace</strong>: https://huggingface.co/microsoft/Florence-2-base</li>
</ul>
<hr />
<h2 id="ai_features-changelog">Changelog</h2>
<h3 id="ai_features-v240-march-2026">v2.4.0 (March 2026)</h3>
<ul>
<li>Added Florence-2 caption generation</li>
<li>Improved prompt alignment scoring with multi-signal fusion</li>
<li>Added French-to-English query translation</li>
<li>Enhanced auto-tag vocabulary (20+ tags)</li>
<li>Smart Collections with k-means clustering</li>
</ul>
<h3 id="ai_features-v230-february-2026">v2.3.0 (February 2026)</h3>
<ul>
<li>Initial AI features release</li>
<li>Semantic search with SigLIP2</li>
<li>Find Similar functionality</li>
<li>Basic auto-tagging</li>
</ul>
<hr />
<h2 id="ai_features-support-resources">Support &amp; Resources</h2>
<h3 id="ai_features-documentation">Documentation</h3>
<ul>
<li><a href="#documentation_index">Main Documentation Index</a></li>
<li><a href="#api_reference">API Reference</a></li>
<li><a href="#settings_configuration">Configuration Guide</a></li>
</ul>
<h3 id="ai_features-community">Community</h3>
<ul>
<li><a href="https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/issues" target="_blank" rel="noopener noreferrer">GitHub Issues</a></li>
<li><a href="https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/discussions" target="_blank" rel="noopener noreferrer">GitHub Discussions</a></li>
</ul>
<h3 id="ai_features-model-documentation">Model Documentation</h3>
<ul>
<li><a href="https://sbert.net/" target="_blank" rel="noopener noreferrer">Sentence Transformers Docs</a></li>
<li><a href="https://huggingface.co/docs/transformers" target="_blank" rel="noopener noreferrer">HuggingFace Transformers</a></li>
<li><a href="https://faiss.ai/" target="_blank" rel="noopener noreferrer">Faiss Documentation</a></li>
</ul>
<hr />
<p><em>This documentation is part of the Majoor Assets Manager project.</em> <em>Copyright © 2026 Ewald ALOEBOETOE (MajoorWaldi)</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/AI_FEATURES.md</code>
</div>
</div>
</section>
<section class="panel section" id="api_reference">
<div class="panel-h"><h2>API_REFERENCE.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="api_reference-majoor-api-reference">Majoor API Reference</h1>
<p><strong>Base Path</strong>: <code>/mjr/am</code> <strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 7, 2026</p>
<hr />
<h2 id="api_reference-compatibility">Compatibility</h2>
<h3 id="api_reference-versioning">Versioning</h3>
<ul>
<li><strong>Current API</strong>: <code>/mjr/am/*</code> (unversioned, stable)</li>
<li><strong>Version Alias</strong>: <code>/mjr/am/v1/<em></code> → redirects to <code>/mjr/am/</em></code> (308 Permanent Redirect)</li>
<li><strong>Legacy Alias</strong>: <code>/majoor/version</code> → redirects to <code>/mjr/am/version</code></li>
</ul>
<h3 id="api_reference-authentication">Authentication</h3>
<p>Most endpoints require authentication when ComfyUI auth is enabled:</p>
<ul>
<li><strong>Read operations</strong>: May work without auth (depends on configuration)</li>
<li><strong>Write operations</strong>: Require authentication or API token</li>
<li><strong>CSRF Protection</strong>: All state-changing endpoints require <code>X-Requested-With: XMLHttpRequest</code> or <code>X-CSRF-Token</code></li>
</ul>
<h3 id="api_reference-api-token">API Token</h3>
<p>For remote access or when <code>MAJOOR_API_TOKEN</code> is configured:</p>
<pre><code>X-MJR-Token: &lt;your-token&gt;
# OR
Authorization: Bearer &lt;your-token&gt;</code></pre>
<hr />
<h2 id="api_reference-table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#api_reference-health-diagnostics">Health &amp;amp; Diagnostics</a></li>
<li><a href="#api_reference-settings-configuration">Settings &amp;amp; Configuration</a></li>
<li><a href="#api_reference-search-discovery">Search &amp;amp; Discovery</a></li>
<li><a href="#api_reference-asset-operations">Asset Operations</a></li>
<li><a href="#api_reference-indexing-scanning">Indexing &amp;amp; Scanning</a></li>
<li><a href="#api_reference-collections-custom-roots">Collections &amp;amp; Custom Roots</a></li>
<li><a href="#api_reference-viewer-metadata">Viewer &amp;amp; Metadata</a></li>
<li><a href="#api_reference-download-export">Download &amp;amp; Export</a></li>
<li><a href="#api_reference-database-maintenance">Database Maintenance</a></li>
<li><a href="#api_reference-utilities">Utilities</a></li>
</ul>
<hr />
<h2 id="api_reference-health-diagnostics">Health &amp; Diagnostics</h2>
<h3 id="api_reference-health-summary">Health Summary</h3>
<pre><code class="language-http">GET /mjr/am/health</code></pre>
<p><strong>Response</strong>: Extension health summary including version, status, and basic metrics.</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"version": "2.4.9",
"status": "healthy",
"indexed_count": 1234,
"scopes_available": ["output", "input", "custom", "collections"]
}
}</code></pre>
<hr />
<h3 id="api_reference-health-counters">Health Counters</h3>
<pre><code class="language-http">GET /mjr/am/health/counters</code></pre>
<p><strong>Response</strong>: Indexed counters for all scopes.</p>
<pre><code class="language-json">{
"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 }
}
}</code></pre>
<hr />
<h3 id="api_reference-database-health">Database Health</h3>
<pre><code class="language-http">GET /mjr/am/health/db</code></pre>
<p><strong>Response</strong>: Database diagnostics including schema version, integrity status, and statistics.</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"schema_version": 7,
"integrity_check": "ok",
"page_count": 1024,
"freelist_count": 128,
"cache_size": 2000
}
}</code></pre>
<hr />
<h3 id="api_reference-runtime-configuration">Runtime Configuration</h3>
<pre><code class="language-http">GET /mjr/am/config</code></pre>
<p><strong>Response</strong>: Current runtime configuration snapshot.</p>
<pre><code class="language-json">{
"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
}
}</code></pre>
<hr />
<h3 id="api_reference-version-information">Version Information</h3>
<pre><code class="language-http">GET /mjr/am/version</code></pre>
<p><strong>Response</strong>: Extension version and build information.</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"version": "2.4.9",
"branch": "main",
"build_date": "2026-03-29",
"python_version": "3.11.0",
"comfyui_version": "0.13.0"
}
}</code></pre>
<hr />
<h3 id="api_reference-tool-status">Tool Status</h3>
<pre><code class="language-http">GET /mjr/am/tools/status</code></pre>
<p><strong>Response</strong>: Availability status of external tools (ExifTool, FFprobe).</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"exiftool": { "available": true, "version": "12.40" },
"ffprobe": { "available": true, "version": "6.0" },
"backend": "both"
}
}</code></pre>
<hr />
<h2 id="api_reference-settings-configuration">Settings &amp; Configuration</h2>
<h3 id="api_reference-probe-backend-settings">Probe Backend Settings</h3>
<pre><code class="language-http">GET /mjr/am/settings/probe-backend
POST /mjr/am/settings/probe-backend</code></pre>
<p><strong>GET</strong>: Read current probe mode. <strong>POST</strong>: Set probe mode.</p>
<p><strong>Request Body</strong> (POST):</p>
<pre><code class="language-json">{
"mode": "auto" // "auto", "exiftool", "ffprobe", "both"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"mode": "auto",
"available": ["exiftool", "ffprobe"]
}
}</code></pre>
<hr />
<h3 id="api_reference-output-directory-override">Output Directory Override</h3>
<pre><code class="language-http">GET /mjr/am/settings/output-directory
POST /mjr/am/settings/output-directory</code></pre>
<p><strong>GET</strong>: Read output directory override. <strong>POST</strong>: Set output directory override.</p>
<p><strong>Request Body</strong> (POST):</p>
<pre><code class="language-json">{
"path": "/path/to/custom/output"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"path": "/path/to/custom/output",
"exists": true,
"writable": true
}
}</code></pre>
<h3 id="api_reference-index-directory-override">Index Directory Override</h3>
<pre><code class="language-http">GET /mjr/am/settings/index-directory
POST /mjr/am/settings/index-directory</code></pre>
<p><strong>GET</strong>: Read the current index directory path.</p>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"index_directory": "/path/to/_mjr_index"
}
}</code></pre>
<p><strong>POST</strong>: Set a new index directory path.</p>
<blockquote>Requires write access (loopback or valid <code>X-MJR-Token</code>).</blockquote>
<p><strong>Request Body</strong> (POST):</p>
<pre><code class="language-json">{
"index_directory": "/path/to/local/mjr_index"
}</code></pre>
<p>Send an empty string to clear the override and revert to the default (<code>&amp;lt;output&amp;gt;/_mjr_index/</code>):</p>
<pre><code class="language-json">{
"index_directory": ""
}</code></pre>
<p><strong>Response</strong> (POST):</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"index_directory": "/path/to/local/mjr_index",
"requires_restart": true
}
}</code></pre>
<p>The new path is persisted to the <code>.mjr_index_directory_override</code> sidecar file and the in-process environment variables. The change takes effect after ComfyUI is restarted.</p>
<p><strong>Error codes</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>INVALID_INPUT</code></td><td>Path exists but is a file, not a directory</td></tr>
<tr><td><code>INVALID_INPUT</code></td><td>Path was given as a non-empty string but the parent directory does not exist</td></tr>
<tr><td><code>DB_ERROR</code></td><td>Sidecar file could not be written</td></tr>
</tbody></table></div>
<hr />
<h3 id="api_reference-metadata-fallback-toggles">Metadata Fallback Toggles</h3>
<pre><code class="language-http">GET /mjr/am/settings/metadata-fallback
POST /mjr/am/settings/metadata-fallback</code></pre>
<p><strong>GET</strong>: Read fallback toggles for metadata extraction. <strong>POST</strong>: Set fallback toggles.</p>
<p><strong>Request Body</strong> (POST):</p>
<pre><code class="language-json">{
"image": true, // Use internal parser if ExifTool fails
"media": true // Use hachoir if ffprobe unavailable
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"image": true,
"media": true
}
}</code></pre>
<hr />
<h2 id="api_reference-search-discovery">Search &amp; Discovery</h2>
<h3 id="api_reference-search-with-filters">Search with Filters</h3>
<pre><code class="language-http">GET /mjr/am/search</code></pre>
<p><strong>Query Parameters</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>q</code></td><td>string</td><td>Search query (full-text)</td></tr>
<tr><td><code>scope</code></td><td>string</td><td>Scope: <code>output</code>, <code>input</code>, <code>custom</code>, <code>collections</code></td></tr>
<tr><td><code>root_id</code></td><td>string</td><td>Custom root ID (for custom scope)</td></tr>
<tr><td><code>collection_id</code></td><td>string</td><td>Collection ID (for collections scope)</td></tr>
<tr><td><code>kind</code></td><td>string</td><td>File kind: <code>image</code>, <code>video</code>, <code>audio</code>, <code>model3d</code>, <code>all</code></td></tr>
<tr><td><code>min_rating</code></td><td>integer</td><td>Minimum rating (0-5)</td></tr>
<tr><td><code>workflow</code></td><td>boolean</td><td>Filter by workflow presence</td></tr>
<tr><td><code>date_from</code></td><td>string</td><td>Start date (ISO 8601)</td></tr>
<tr><td><code>date_to</code></td><td>string</td><td>End date (ISO 8601)</td></tr>
<tr><td><code>size_from</code></td><td>integer</td><td>Minimum file size (bytes)</td></tr>
<tr><td><code>size_to</code></td><td>integer</td><td>Maximum file size (bytes)</td></tr>
<tr><td><code>width_from</code></td><td>integer</td><td>Minimum image width</td></tr>
<tr><td><code>width_to</code></td><td>integer</td><td>Maximum image width</td></tr>
<tr><td><code>height_from</code></td><td>integer</td><td>Minimum image height</td></tr>
<tr><td><code>height_to</code></td><td>integer</td><td>Maximum image height</td></tr>
<tr><td><code>sort</code></td><td>string</td><td>Sort field: <code>relevance</code>, <code>name</code>, <code>date</code>, <code>size</code>, <code>rating</code></td></tr>
<tr><td><code>order</code></td><td>string</td><td>Sort order: <code>asc</code>, <code>desc</code></td></tr>
<tr><td><code>page</code></td><td>integer</td><td>Page number (1-based)</td></tr>
<tr><td><code>page_size</code></td><td>integer</td><td>Items per page (default: 50)</td></tr>
<tr><td><code>hide_png_siblings</code></td><td>boolean</td><td>Hide PNGs when video exists</td></tr>
</tbody></table></div>
<p><strong>Example</strong>:</p>
<pre><code class="language-http">GET /mjr/am/search?q=fantasy&amp;scope=output&amp;kind=image&amp;min_rating=4&amp;sort=date&amp;order=desc&amp;page=1&amp;page_size=50</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"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
}
}
]
}
}</code></pre>
<hr />
<h3 id="api_reference-list-assets-paginated">List Assets (Paginated)</h3>
<pre><code class="language-http">GET /mjr/am/list</code></pre>
<p><strong>Query Parameters</strong>: Same as search, but without <code>q</code> parameter.</p>
<p><strong>Purpose</strong>: List assets without full-text search (faster for browsing).</p>
<hr />
<h3 id="api_reference-asset-details">Asset Details</h3>
<pre><code class="language-http">GET /mjr/am/asset/{asset_id}</code></pre>
<p><strong>Response</strong>: Full asset details including metadata.</p>
<pre><code class="language-json">{
"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"
}
}
}</code></pre>
<hr />
<h3 id="api_reference-metadata-lookup">Metadata Lookup</h3>
<pre><code class="language-http">GET /mjr/am/metadata</code></pre>
<p><strong>Query Parameters</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>string</td><td>Scope type: <code>output</code>, <code>input</code>, <code>custom</code></td></tr>
<tr><td><code>filename</code></td><td>string</td><td>Filename</td></tr>
<tr><td><code>subfolder</code></td><td>string</td><td>Subfolder path (optional)</td></tr>
<tr><td><code>root_id</code></td><td>string</td><td>Custom root ID (for custom scope)</td></tr>
</tbody></table></div>
<p><strong>Example</strong>:</p>
<pre><code class="language-http">GET /mjr/am/metadata?type=output&amp;filename=image.png&amp;subfolder=2026-02-28</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"filename": "image.png",
"path": "/path/to/output/2026-02-28/image.png",
"metadata": { ... }
}
}</code></pre>
<hr />
<h2 id="api_reference-asset-operations">Asset Operations</h2>
<h3 id="api_reference-update-rating">Update Rating</h3>
<pre><code class="language-http">POST /mjr/am/asset/rating</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123",
"rating": 5 // 0-5
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"rating": 5,
"synced_to_file": true
}
}</code></pre>
<hr />
<h3 id="api_reference-update-tags">Update Tags</h3>
<pre><code class="language-http">POST /mjr/am/asset/tags</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123",
"tags": ["fantasy", "character", "sdxl"]
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"tags": ["fantasy", "character", "sdxl"],
"synced_to_file": true
}
}</code></pre>
<hr />
<h3 id="api_reference-rename-asset">Rename Asset</h3>
<pre><code class="language-http">POST /mjr/am/asset/rename</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123",
"new_filename": "new_name.png"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"old_path": "/path/to/old_name.png",
"new_path": "/path/to/new_name.png"
}
}</code></pre>
<p><strong>Note</strong>: Requires <code>MAJOOR_ALLOW_RENAME=1</code> if Safe Mode is enabled.</p>
<hr />
<h3 id="api_reference-delete-single-asset">Delete Single Asset</h3>
<pre><code class="language-http">POST /mjr/am/asset/delete</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"deleted": true,
"path": "/path/to/deleted_file.png"
}
}</code></pre>
<p><strong>Note</strong>: Requires <code>MAJOOR_ALLOW_DELETE=1</code> if Safe Mode is enabled.</p>
<hr />
<h3 id="api_reference-bulk-delete-assets">Bulk Delete Assets</h3>
<pre><code class="language-http">POST /mjr/am/assets/delete</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_ids": ["asset_123", "asset_124", "asset_125"]
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"deleted_count": 3,
"deleted_ids": ["asset_123", "asset_124", "asset_125"],
"failed": []
}
}</code></pre>
<p><strong>Note</strong>: Requires <code>MAJOOR_ALLOW_DELETE=1</code> if Safe Mode is enabled.</p>
<hr />
<h3 id="api_reference-open-in-folder">Open in Folder</h3>
<pre><code class="language-http">POST /mjr/am/open-in-folder</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123"
}</code></pre>
<p>or:</p>
<pre><code class="language-json">{
"filepath": "/absolute/path/to/file.png"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"path": "/path/to/output",
"opened": true
}
}</code></pre>
<p><strong>Note</strong>: Requires <code>MAJOOR_ALLOW_OPEN_IN_FOLDER=1</code> if Safe Mode is enabled. This route only asks the OS file manager to reveal an existing local file; it does not read, write, delete, or download file contents. Because of that, <code>filepath</code> targets are not restricted to indexed or configured asset roots for this action.</p>
<hr />
<h3 id="api_reference-stage-to-input">Stage to Input</h3>
<pre><code class="language-http">POST /mjr/am/stage-to-input</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_id": "asset_123"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"asset_id": "asset_123",
"staged_path": "/path/to/input/staged_file.png",
"staged": true
}
}</code></pre>
<p><strong>Purpose</strong>: Copy/link asset to ComfyUI input directory for workflow use.</p>
<hr />
<h2 id="api_reference-indexing-scanning">Indexing &amp; Scanning</h2>
<h3 id="api_reference-scan-scoperoot">Scan Scope/Root</h3>
<pre><code class="language-http">POST /mjr/am/scan</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"scope": "output", // "output", "input", "custom"
"root_id": null, // Custom root ID (for custom scope)
"incremental": true, // Incremental scan (default)
"force": false // Force full rescan
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"scan_id": "scan_123",
"scope": "output",
"status": "started",
"estimated_duration_ms": 5000
}
}</code></pre>
<p><strong>Note</strong>: Scan runs in background. Status available via health endpoints.</p>
<hr />
<h3 id="api_reference-index-explicit-files">Index Explicit Files</h3>
<pre><code class="language-http">POST /mjr/am/index-files</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"files": [
{
"filename": "image.png",
"subfolder": "2026-02-28",
"type": "output"
}
],
"origin": "generation" // "generation", "manual", "import"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"indexed_count": 1,
"failed": []
}
}</code></pre>
<p><strong>Purpose</strong>: Index specific files (used for real-time generation tracking).</p>
<hr />
<h3 id="api_reference-reset-index">Reset Index</h3>
<pre><code class="language-http">POST /mjr/am/index/reset</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"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
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"scope": "output",
"reset": true,
"rescan_triggered": true
}
}</code></pre>
<hr />
<h2 id="api_reference-collections-custom-roots">Collections &amp; Custom Roots</h2>
<h3 id="api_reference-list-collections">List Collections</h3>
<pre><code class="language-http">GET /mjr/am/collections</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"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"
}
]
}
}</code></pre>
<hr />
<h3 id="api_reference-createupdate-collection">Create/Update Collection</h3>
<pre><code class="language-http">POST /mjr/am/collections/create
POST /mjr/am/collections/update</code></pre>
<p><strong>Request Body</strong> (Create):</p>
<pre><code class="language-json">{
"name": "Best Characters",
"items": [
{
"asset_id": "asset_123",
"scope": "output"
}
]
}</code></pre>
<p><strong>Request Body</strong> (Update):</p>
<pre><code class="language-json">{
"collection_id": "collection_123",
"name": "Updated Name",
"items": [...],
"add_items": [...], // Items to add
"remove_items": [...] // Items to remove
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"collection_id": "collection_123",
"name": "Updated Name",
"item_count": 30
}
}</code></pre>
<hr />
<h3 id="api_reference-delete-collection">Delete Collection</h3>
<pre><code class="language-http">POST /mjr/am/collections/delete</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"collection_id": "collection_123"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"collection_id": "collection_123",
"deleted": true
}
}</code></pre>
<p><strong>Note</strong>: Does not delete assets, only the collection definition.</p>
<hr />
<h3 id="api_reference-list-custom-roots">List Custom Roots</h3>
<pre><code class="language-http">GET /mjr/am/custom-roots</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"roots": [
{
"id": "root_123",
"path": "/path/to/custom/dir",
"name": "Custom Directory",
"added_at": "2026-02-28T10:00:00Z"
}
]
}
}</code></pre>
<hr />
<h3 id="api_reference-add-custom-root">Add Custom Root</h3>
<pre><code class="language-http">POST /mjr/am/custom-roots/add</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"path": "/path/to/custom/dir",
"name": "Custom Directory"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"root_id": "root_123",
"path": "/path/to/custom/dir",
"added": true
}
}</code></pre>
<hr />
<h3 id="api_reference-remove-custom-root">Remove Custom Root</h3>
<pre><code class="language-http">POST /mjr/am/custom-roots/remove</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"root_id": "root_123"
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"root_id": "root_123",
"removed": true
}
}</code></pre>
<hr />
<h2 id="api_reference-viewer-metadata">Viewer &amp; Metadata</h2>
<h3 id="api_reference-viewer-info">Viewer Info</h3>
<pre><code class="language-http">GET /mjr/am/viewer/info</code></pre>
<p><strong>Query Parameters</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>asset_id</code></td><td>string</td><td>Asset ID</td></tr>
</tbody></table></div>
<p><strong>Example</strong>:</p>
<pre><code class="language-http">GET /mjr/am/viewer/info?asset_id=asset_123</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"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": { ... }
}
}
}</code></pre>
<hr />
<h2 id="api_reference-download-export">Download &amp; Export</h2>
<h3 id="api_reference-download-asset">Download Asset</h3>
<pre><code class="language-http">GET /mjr/am/download</code></pre>
<p><strong>Query Parameters</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>asset_id</code></td><td>string</td><td>Asset ID</td></tr>
<tr><td><code>filename</code></td><td>string</td><td>Filename (for direct download)</td></tr>
<tr><td><code>type</code></td><td>string</td><td>Scope type</td></tr>
<tr><td><code>subfolder</code></td><td>string</td><td>Subfolder path</td></tr>
</tbody></table></div>
<p><strong>Example</strong>:</p>
<pre><code class="language-http">GET /mjr/am/download?asset_id=asset_123</code></pre>
<p><strong>Response</strong>: File stream with appropriate Content-Type and Content-Disposition headers.</p>
<hr />
<h3 id="api_reference-create-batch-zip">Create Batch ZIP</h3>
<pre><code class="language-http">POST /mjr/am/batch-zip</code></pre>
<p><strong>Request Body</strong>:</p>
<pre><code class="language-json">{
"asset_ids": ["asset_123", "asset_124", "asset_125"]
}</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"token": "zip_token_abc123",
"expires_at": "2026-02-28T11:00:00Z",
"download_url": "/mjr/am/batch-zip/zip_token_abc123"
}
}</code></pre>
<hr />
<h3 id="api_reference-download-batch-zip">Download Batch ZIP</h3>
<pre><code class="language-http">GET /mjr/am/batch-zip/{token}</code></pre>
<p><strong>Response</strong>: ZIP file stream containing all requested assets.</p>
<hr />
<h2 id="api_reference-database-maintenance">Database Maintenance</h2>
<h3 id="api_reference-optimize-database">Optimize Database</h3>
<pre><code class="language-http">POST /mjr/am/db/optimize</code></pre>
<p><strong>Purpose</strong>: Run <code>PRAGMA optimize</code> and <code>ANALYZE</code> for performance.</p>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"optimized": true,
"duration_ms": 150
}
}</code></pre>
<hr />
<h3 id="api_reference-cleanup-case-duplicates">Cleanup Case Duplicates</h3>
<pre><code class="language-http">POST /mjr/am/db/cleanup-case-duplicates</code></pre>
<p><strong>Purpose</strong>: Remove historical path-case duplicates (Windows-specific).</p>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"duplicates_removed": 5,
"kept_canonical": 100
}
}</code></pre>
<hr />
<h3 id="api_reference-force-delete-database">Force-Delete Database</h3>
<pre><code class="language-http">POST /mjr/am/db/force-delete</code></pre>
<p><strong>Purpose</strong>: Emergency recovery for corrupted databases.</p>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"deleted": true,
"reinitialized": true,
"rescan_triggered": true
}
}</code></pre>
<p><strong>Note</strong>: This endpoint bypasses normal DB-dependent security checks for emergency recovery.</p>
<hr />
<h2 id="api_reference-utilities">Utilities</h2>
<h3 id="api_reference-date-histogram">Date Histogram</h3>
<pre><code class="language-http">GET /mjr/am/date-histogram</code></pre>
<p><strong>Query Parameters</strong>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>month</code></td><td>string</td><td>Month in YYYY-MM format</td></tr>
<tr><td><code>scope</code></td><td>string</td><td>Scope type</td></tr>
</tbody></table></div>
<p><strong>Example</strong>:</p>
<pre><code class="language-http">GET /mjr/am/date-histogram?month=2026-02&amp;scope=output</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"month": "2026-02",
"days": [
{ "date": "2026-02-01", "count": 50 },
{ "date": "2026-02-02", "count": 75 },
...
]
}
}</code></pre>
<p><strong>Purpose</strong>: Calendar histogram for date-based filtering UI.</p>
<hr />
<h3 id="api_reference-duplicate-alerts">Duplicate Alerts</h3>
<pre><code class="language-http">GET /mjr/am/duplicates/alerts</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"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
}
}</code></pre>
<hr />
<h3 id="api_reference-workflow-library">Workflow Library</h3>
<p>The Workflow tab uses the standard list endpoint with <code>scope=workflow</code> plus dedicated workflow routes for JSON content, organization, metadata, and thumbnails.</p>
<pre><code class="language-http">GET /mjr/am/list?scope=workflow&amp;q=*&amp;limit=100&amp;offset=0&amp;sort=workflow_default</code></pre>
<p><strong>Purpose</strong>: List saved workflow JSON files as grid cards.</p>
<p><strong>Response fields include</strong>:</p>
<ul>
<li><code>assets</code>: workflow cards</li>
<li><code>total</code>: total matched workflows</li>
<li><code>roots</code>: scanned workflow roots</li>
<li><code>scope</code>: <code>workflow</code></li>
</ul>
<pre><code class="language-http">GET /mjr/am/workflows/content?filepath=C%3A%5Cpath%5Cworkflow.json</code></pre>
<p><strong>Purpose</strong>: Read workflow JSON and prompt metadata for loading or Graph Map inspection.</p>
<pre><code class="language-http">POST /mjr/am/workflows/save
POST /mjr/am/workflows/duplicate
POST /mjr/am/workflows/move
POST /mjr/am/workflows/delete
POST /mjr/am/workflows/mark-loaded
POST /mjr/am/workflows/favorite
POST /mjr/am/workflows/tags
POST /mjr/am/workflows/info</code></pre>
<p><strong>Purpose</strong>: Manage workflow files and workflow library metadata. State-changing workflow routes require the normal Majoor write guards, CSRF/auth checks, audit logging, and workflow write rate limiting.</p>
<pre><code class="language-http">GET /mjr/am/workflows/thumbnail-candidates?filepath=...&amp;limit=12
POST /mjr/am/workflows/thumbnail/set
GET /mjr/am/workflows/thumbnail?filepath=...
GET /mjr/am/workflows/graph-map-thumbnail?filepath=...
GET /mjr/am/workflows/model-families
POST /mjr/am/workflows/open-root</code></pre>
<p><strong>Purpose</strong>: Support workflow thumbnails, generated Graph Map previews, workflow model-family filtering, and opening the saved workflow root in the OS file manager.</p>
<pre><code class="language-http">GET /mjr/am/settings/workflow-roots
POST /mjr/am/settings/workflow-roots</code></pre>
<p><strong>Purpose</strong>: Read and save the Workflow tab root folders. The POST body accepts <code>workflow_roots</code> as a string or list of strings.</p>
<p>See <a href="#workflows">WORKFLOWS.md</a> for the user-facing Workflow tab behavior.</p>
<hr />
<h3 id="api_reference-releases-information">Releases Information</h3>
<pre><code class="language-http">GET /mjr/am/releases</code></pre>
<p><strong>Response</strong>:</p>
<pre><code class="language-json">{
"ok": true,
"data": {
"current_version": "2.4.9",
"branch": "main",
"latest_release": {
"version": "2.4.9",
"date": "2026-04-10",
"download_url": "https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/releases/tag/v2.4.9"
},
"branches_available": ["main", "dev"],
"tags_available": ["v2.4.9", "v2.4.4", "v2.4.3", "v2.4.2"]
}
}</code></pre>
<hr />
<h2 id="api_reference-security-notes">Security Notes</h2>
<h3 id="api_reference-csrf-protection">CSRF Protection</h3>
<p>All state-changing endpoints (POST, PUT, PATCH, DELETE) require:</p>
<ul>
<li><code>X-Requested-With: XMLHttpRequest</code> header, OR</li>
<li><code>X-CSRF-Token</code> header with valid token</li>
</ul>
<h3 id="api_reference-authentication">Authentication</h3>
<p>When ComfyUI auth is enabled:</p>
<ul>
<li>Write operations require authenticated user</li>
<li>API token can be used for remote access</li>
<li>Token sent via <code>X-MJR-Token</code> or <code>Authorization: Bearer</code></li>
</ul>
<h3 id="api_reference-safe-mode">Safe Mode</h3>
<p>When Safe Mode is enabled (default):</p>
<ul>
<li>Delete operations require <code>MAJOOR_ALLOW_DELETE=1</code></li>
<li>Rename operations require <code>MAJOOR_ALLOW_RENAME=1</code></li>
<li>Open in folder requires <code>MAJOOR_ALLOW_OPEN_IN_FOLDER=1</code>; it may reveal any existing local path supplied to the route, but it does not grant file read/write access</li>
</ul>
<h3 id="api_reference-rate-limiting">Rate Limiting</h3>
<p>Expensive endpoints are rate-limited:</p>
<ul>
<li>Search: 10 requests/minute per client</li>
<li>Scan: 5 requests/minute per client</li>
<li>Batch ZIP: 3 requests/minute per client</li>
<li>Metadata: 20 requests/minute per client</li>
</ul>
<p>Client identity based on IP address (or <code>X-Forwarded-For</code> from trusted proxies).</p>
<hr />
<h2 id="api_reference-error-responses">Error Responses</h2>
<h3 id="api_reference-standard-error-format">Standard Error Format</h3>
<pre><code class="language-json">{
"ok": false,
"error": {
"code": "?_CODE",
"message": "Human-readable error message",
"details": { ... } // Optional additional context
}
}</code></pre>
<h3 id="api_reference-common-error-codes">Common Error Codes</h3>
<div class="table-wrap"><table>
<thead><tr><th>Code</th><th>HTTP Status</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>AUTH_REQUIRED</code></td><td>401</td><td>Authentication required</td></tr>
<tr><td><code>FORBIDDEN</code></td><td>403</td><td>Operation not allowed (Safe Mode)</td></tr>
<tr><td><code>NOT_FOUND</code></td><td>404</td><td>Resource not found</td></tr>
<tr><td><code>INVALID_REQUEST</code></td><td>400</td><td>Invalid request body/parameters</td></tr>
<tr><td><code>RATE_LIMITED</code></td><td>429</td><td>Too many requests</td></tr>
<tr><td><code>DB_ERROR</code></td><td>500</td><td>Database error</td></tr>
<tr><td><code>FILE_SYSTEM_ERROR</code></td><td>500</td><td>File system error</td></tr>
<tr><td><code>TOOL_UNAVAILABLE</code></td><td>503</td><td>External tool not available</td></tr>
</tbody></table></div>
<hr />
<h2 id="api_reference-websocket-events">WebSocket Events</h2>
<h3 id="api_reference-real-time-updates">Real-Time Updates</h3>
<p>The extension emits ComfyUI API events for real-time updates:</p>
<ul>
<li><code>mjr-asset-added</code>: New asset indexed</li>
<li><code>mjr-asset-updated</code>: Asset metadata updated</li>
<li><code>mjr-scan-complete</code>: Scan operation completed</li>
<li><code>mjr-enrichment-status</code>: Metadata enrichment status</li>
<li><code>mjr-db-restore-status</code>: Database restore status</li>
</ul>
<p><strong>Example</strong> (Frontend):</p>
<pre><code class="language-javascript">api.addEventListener("mjr-asset-added", (event) =&gt; {
const asset = event.detail;
// Update UI with new asset
});</code></pre>
<hr />
<p><em>API Reference Version: 2.0</em> <em>Last Updated: April 5, 2026</em> <em>Compatible with Majoor Assets Manager v2.4.4+</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/API_REFERENCE.md</code>
</div>
</div>
</section>
<section class="panel section" id="architecture_map">
<div class="panel-h"><h2>ARCHITECTURE_MAP.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="architecture_map-architecture-map">Architecture Map</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 10, 2026</p>
<p>This map is the working reference for backend refactors. It is intentionally short and operational.</p>
<h2 id="architecture_map-package-responsibilities">Package Responsibilities</h2>
<ul>
<li><code>mjr_am_backend/routes</code></li>
<ul>
<li>HTTP surface only.</li>
<li>Parse request data, apply auth/CSRF/rate-limit guards, call services, map <code>Result</code> payloads.</li>
<li><code>route_catalog.py</code> — declarative route registry (<code>RouteRegistration</code> dataclass, CORE + OPTIONAL catalogs).</li>
</ul>
<li><code>mjr_am_backend/routes/core</code></li>
<ul>
<li>Shared route helpers for security, path rules, JSON parsing, responses, and service resolution.</li>
</ul>
<li><code>mjr_am_backend/routes/assets</code></li>
<ul>
<li>Thin compatibility facades re-exporting from <code>features/assets/</code>.</li>
</ul>
<li><code>mjr_am_backend/features</code></li>
<ul>
<li>Business logic and orchestration by domain: index, metadata, collections, browser, health, tags, viewer.</li>
</ul>
<li><code>mjr_am_backend/features/assets</code></li>
<ul>
<li>Asset domain sub-services:</li>
<ul>
<li><code>lookup_service</code> — asset lookups and queries</li>
<li><code>download_service</code> — download/streaming logic</li>
<li><code>rating_tags_service</code> — ratings, tags, collections</li>
<li><code>request_context_service</code> — request parsing and context extraction</li>
<li><code>path_resolution_service</code> — root/subfolder/path resolution</li>
<li><code>delete_service</code> — asset deletion</li>
<li><code>rename_service</code> — asset renaming</li>
<li><code>filename_validator</code> — filename validation rules</li>
<li><code>models</code> — shared data models</li>
<li><code>service</code> — thin compatibility facade</li>
</ul>
</ul>
<li><code>mjr_am_backend/adapters</code></li>
<ul>
<li>Boundary code for SQLite, filesystem watchers, external tools, and other integration points.</li>
</ul>
<li><code>mjr_am_backend/adapters/db</code></li>
<ul>
<li><code>sqlite_facade.py</code> — main entry point for DB access (~1900 L)</li>
<li><code>sqlite_connections.py</code> — connection pool, pragmas, acquire/release</li>
<li><code>sqlite_execution.py</code> — query execution, timeout, retry, batch</li>
<li><code>sqlite_lifecycle.py</code> — transactions, reset, WAL, Windows file-handle management</li>
<li><code>sqlite_recovery.py</code> — malformed recovery, FTS rebuild, schema repair</li>
<li><code>schema.py</code> (331 L) + <code>schema_sql.py</code>, <code>schema_fts.py</code>, <code>schema_vec.py</code> — DDL and migration</li>
<li><code>connection_pool.py</code>, <code>db_recovery.py</code>, <code>transaction_manager.py</code> — lower-level helpers</li>
</ul>
<li><code>mjr_am_shared</code></li>
<ul>
<li>Shared result types, error definitions, logging, time helpers, and cross-cutting utilities.</li>
</ul>
</ul>
<h2 id="architecture_map-frontend">Frontend</h2>
<ul>
<li><strong>Vue 3 + Pinia</strong> for all major UI surfaces (grid, sidebar, feed, context menus, settings).</li>
<li><code>ui/vue/</code> — Vue components and stores.</li>
<li><code>ui/features/</code> — feature-scoped JS modules.</li>
<li><code>ui/stores/</code> — Pinia stores.</li>
<li>Imperative runtime services remain for viewer, DnD, and ComfyUI integration (by design).</li>
</ul>
<h2 id="architecture_map-request-flow">Request Flow</h2>
<p>UI action -&gt; route handler -&gt; route/core helper -&gt; feature service -&gt; adapter -&gt; DB/filesystem/tool -&gt; <code>Result</code> response -&gt; UI mapping</p>
<p>Rules:</p>
<ul>
<li>Routes do not decide filesystem or security policy locally.</li>
<li>Services own business rules and orchestration.</li>
<li>Adapters isolate external side effects.</li>
<li><code>security.py</code> is the single policy entry point for auth, write access, safe mode, trusted proxies, and CSRF.</li>
</ul>
<h2 id="architecture_map-watcher-and-index-flow">Watcher And Index Flow</h2>
<p>Filesystem event -&gt; watcher scope/runtime -&gt; index orchestration -&gt; DB update -&gt; UI refresh event</p>
<p>Hot zones:</p>
<ul>
<li>watcher lifecycle and scope resolution</li>
<li>reset/rebuild orchestration</li>
<li>metadata probing and vector backfill</li>
<li>live viewer synchronization</li>
</ul>
<p>These zones should be refactored behind stable service facades before adding new behavior.</p>
<h2 id="architecture_map-security-rules">Security Rules</h2>
<ul>
<li>Public API base stays <code>/mjr/am/*</code>.</li>
<li>Backend returns structured <code>Result</code> payloads for expected errors.</li>
<li>Safe mode is enabled by default.</li>
<li>Remote write remains denied without a valid token.</li>
<li>Loopback write is allowed by default in the current compatibility model unless <code>MAJOOR_REQUIRE_AUTH=1</code> is set.</li>
</ul>
<h2 id="architecture_map-naming-and-internal-api-conventions">Naming And Internal API Conventions</h2>
<ul>
<li>Public internal APIs should be thin, stable facades under <code>features/<em></code> or <code>routes/core/</em></code>.</li>
<li>Route modules may import facades and helpers, but should not directly orchestrate storage internals across multiple subsystems.</li>
<li>New security-sensitive mutations must call shared security and path helpers instead of duplicating checks.</li>
</ul>
<h2 id="architecture_map-tags-storage">Tags Storage</h2>
<ul>
<li>User tags are stored in normalized tags and asset_tags tables.</li>
<li>mjr_am_backend/data/repositories/tags_repository.py owns tag persistence and asset/tag links.</li>
<li>asset_metadata.tags and asset_metadata.tags_text are pre-v19 legacy columns only; application readers and writers must use normalized tags.</li>
<li>FTS tag columns are populated from normalized tags by v18+ triggers.</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/ARCHITECTURE_MAP.md</code>
</div>
</div>
</section>
<section class="panel section" id="contributing">
<div class="panel-h"><h2>CONTRIBUTING.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="contributing-contributing-to-majoor-assets-manager">Contributing to Majoor Assets Manager</h1>
<p>Thank you for your interest in contributing to <strong>ComfyUI-Majoor-AssetsManager</strong>! This guide will help you set up your development environment, understand our coding standards, and submit quality contributions.</p>
<hr />
<h2 id="contributing-table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#contributing-development-setup">Development Setup</a></li>
<li><a href="#contributing-project-structure">Project Structure</a></li>
<li><a href="#contributing-coding-standards">Coding Standards</a></li>
<li><a href="#contributing-testing">Testing</a></li>
<li><a href="#contributing-submitting-contributions">Submitting Contributions</a></li>
<li><a href="#contributing-git-workflow">Git Workflow</a></li>
<li><a href="#contributing-code-quality-gate">Code Quality Gate</a></li>
<li><a href="#contributing-documentation">Documentation</a></li>
<li><a href="#contributing-getting-help">Getting Help</a></li>
</ul>
<hr />
<h2 id="contributing-development-setup">Development Setup</h2>
<h3 id="contributing-prerequisites">Prerequisites</h3>
<ul>
<li><strong>Python</strong>: 3.10 - 3.13 (3.14 not yet supported)</li>
<li><strong>Node.js</strong>: 18+ (LTS recommended)</li>
<li><strong>Git</strong>: 2.30+</li>
<li><strong>ComfyUI</strong>: &gt;= 0.13.0 (running instance for testing)</li>
</ul>
<h3 id="contributing-quick-start">Quick Start</h3>
<pre><code class="language-bash"># 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</code></pre>
<h3 id="contributing-development-workflow">Development Workflow</h3>
<pre><code class="language-bash"># 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</code></pre>
<hr />
<h2 id="contributing-project-structure">Project Structure</h2>
<pre><code>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)
├── ui/ # 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
├── 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</code></pre>
<h3 id="contributing-key-architecture-principles">Key Architecture Principles</h3>
<ol>
<li><strong>Backend</strong>: Routes → Features → Adapters (clean separation of concerns)</li>
<li><strong>Frontend</strong>: Vue 3 owns major UI surfaces; imperative runtime bridges are explicit and limited</li>
<li><strong>State</strong>: Pinia stores own UI state; localStorage for persistence</li>
<li><strong>Security</strong>: Safe mode by default; write/delete operations require explicit opt-in</li>
<li><strong>Error Handling</strong>: <code>Result[T]</code> pattern everywhere — no exceptions bubble to UI</li>
</ol>
<p>See <a href="#architecture_map">&lt;code&gt;docs/ARCHITECTURE_MAP.md&lt;/code&gt;</a> and <a href="docs/docs/adr">&lt;code&gt;docs/adr/&lt;/code&gt;</a> for detailed design decisions.</p>
<hr />
<h2 id="contributing-coding-standards">Coding Standards</h2>
<h3 id="contributing-python">Python</h3>
<ul>
<li><strong>Style</strong>: Follow <a href="https://black.readthedocs.io/" target="_blank" rel="noopener noreferrer">Black</a> formatting (line length: 100)</li>
<li><strong>Linting</strong>: <a href="https://docs.astral.sh/ruff/" target="_blank" rel="noopener noreferrer">Ruff</a> with rules in <code>pyproject.toml</code></li>
<li><strong>Type Checking</strong>: <a href="https://mypy.readthedocs.io/" target="_blank" rel="noopener noreferrer">MyPy</a> + <a href="https://github.com/microsoft/pyright" target="_blank" rel="noopener noreferrer">Pyright</a> (standard mode)</li>
<li><strong>Imports</strong>: Organized by Ruff <code>I</code> rule (isort-compatible)</li>
<li><strong>Error Handling</strong>: Use <code>Result[T]</code> from <code>mjr_am_shared/result.py</code> — never raise to UI</li>
<li><strong>Security</strong>: Never import <code>ComfyUI/server.py</code> (see <a href="docs/docs/adr/0002-comfyui-server-import-safety.md">ADR-002</a>)</li>
</ul>
<p><strong>Example</strong>:</p>
<pre><code class="language-python">from mjr_am_shared.result import Result, Ok, Err
def process_asset(asset_id: str) -&gt; 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}")</code></pre>
<h3 id="contributing-javascriptvue">JavaScript/Vue</h3>
<ul>
<li><strong>Style</strong>: ESLint + Prettier (config in <code>eslint.config.mjs</code>, <code>.prettierrc</code>)</li>
<li><strong>Framework</strong>: Vue 3 Composition API with <code>&amp;lt;script setup&amp;gt;</code></li>
<li><strong>State</strong>: Pinia stores for reactive state; no global mutable objects</li>
<li><strong>Testing</strong>: Vitest with Happy-DOM for component tests</li>
<li><strong>Naming</strong>: camelCase for variables/functions, PascalCase for Vue components</li>
</ul>
<p><strong>Example</strong>:</p>
<pre><code class="language-vue">&lt;script setup&gt;
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(() =&gt; panelStore.selectedIds.includes(props.asset.id));
&lt;/script&gt;</code></pre>
<h3 id="contributing-commits">Commits</h3>
<ul>
<li>Use <a href="https://www.conventionalcommits.org/" target="_blank" rel="noopener noreferrer">Conventional Commits</a> format:</li>
</ul>
<pre><code> feat: add multi-pin support in Floating Viewer
fix: prevent timeout leak in Card.js
docs: update API reference with vector endpoints</code></pre>
<ul>
<li>Keep commits focused and atomic (one logical change per commit)</li>
<li>Write clear, descriptive messages (explain "why", not "what")</li>
</ul>
<hr />
<h2 id="contributing-testing">Testing</h2>
<h3 id="contributing-backend-python">Backend (Python)</h3>
<pre><code class="language-bash"># 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</code></pre>
<p><strong>Test organization</strong>:</p>
<ul>
<li><code>tests/backend/</code> — Backend unit/integration tests</li>
<li><code>tests/features/</code> — Feature-specific tests (geninfo parser, metadata extraction)</li>
<li><code>tests/security/</code> — Security tests (rate limiting, auth)</li>
<li><code>tests/database/</code> — Database schema and migration tests</li>
</ul>
<h3 id="contributing-frontend-javascriptvue">Frontend (JavaScript/Vue)</h3>
<pre><code class="language-bash"># Run all tests
npm run test:js
# Run with coverage
npm run test:js:coverage
# Watch mode
npm run test:js:watch</code></pre>
<p><strong>Test organization</strong>:</p>
<ul>
<li><code>ui/tests/</code> — Vitest test files</li>
<li>Test files named <code>*.vitest.mjs</code></li>
<li>Cover: composables, utilities, security-critical modules</li>
</ul>
<h3 id="contributing-coverage-thresholds">Coverage Thresholds</h3>
<div class="table-wrap"><table>
<thead><tr><th>Metric</th><th>Python</th><th>JavaScript</th></tr></thead>
<tbody>
<tr><td>Lines</td><td>68%</td><td>30%</td></tr>
<tr><td>Branches</td><td>Tracked (--cov-branch)</td><td>20%</td></tr>
<tr><td>Functions</td><td>70%</td><td>30%</td></tr>
</tbody></table></div>
<p><strong>Note</strong>: These are minimum thresholds. Security-critical code should have 100% coverage.</p>
<hr />
<h2 id="contributing-submitting-contributions">Submitting Contributions</h2>
<h3 id="contributing-before-you-start">Before You Start</h3>
<ol>
<li><strong>Check existing issues</strong>: See if someone else is working on your idea</li>
<li><strong>Open an issue</strong>: For features, bugs, or significant changes</li>
<li><strong>Discuss approach</strong>: For complex changes, propose design in issue comments</li>
</ol>
<h3 id="contributing-pull-request-process">Pull Request Process</h3>
<ol>
<li><strong>Fork the repository</strong> and create your branch from <code>main</code></li>
<li><strong>Make your changes</strong> following coding standards</li>
<li><strong>Add tests</strong> for new features or bug fixes</li>
<li><strong>Run quality gate</strong>: <code>npm run quality</code> (must pass)</li>
<li><strong>Update documentation</strong> if behavior changes</li>
<li><strong>Submit PR</strong> with clear description:</li>
<ul>
<li>What changed and why</li>
<li>How to test the changes</li>
<li>Any breaking changes or migration steps</li>
</ul>
</ol>
<h3 id="contributing-pr-checklist">PR Checklist</h3>
<ul>
<li>[ ] Code follows project style guide</li>
<li>[ ] Self-review completed</li>
<li>[ ] Tests added/updated</li>
<li>[ ] Quality gate passes (<code>npm run quality</code>)</li>
<li>[ ] Documentation updated</li>
<li>[ ] Commit messages are clear and conventional</li>
<li>[ ] No merge conflicts with target branch</li>
</ul>
<h3 id="contributing-review-process">Review Process</h3>
<p>Mainters will review your PR and may:</p>
<ul>
<li>Request changes (address feedback before merging)</li>
<li>Approve and merge</li>
<li>Ask for clarification on implementation choices</li>
</ul>
<hr />
<h2 id="contributing-git-workflow">Git Workflow</h2>
<h3 id="contributing-branch-strategy">Branch Strategy</h3>
<ul>
<li><strong><code>main</code></strong>: Stable release branch (always deployable)</li>
<li><strong>Feature branches</strong>: <code>feat/feature-name</code> or <code>fix/bug-name</code></li>
<li><strong>Release tags</strong>: <code>v2.4.9</code>, <code>v2.5.0</code>, etc. (semantic versioning)</li>
</ul>
<h3 id="contributing-release-cadence">Release Cadence</h3>
<ul>
<li><strong>Development window</strong>: 2 weeks for feature work and compatibility updates.</li>
<li><strong>Freeze window</strong>: 2 weeks for regression fixes, documentation, dependency</li>
<p>checks, and ComfyUI frontend compatibility validation.</p>
<li><strong>Nightly builds</strong>: Used for early validation and should not be treated as</li>
<p>stable releases.</p>
<li><strong>Release candidates</strong>: Cut from <code>main</code> after the freeze window when the</li>
<p>quality gate, frontend build, and smoke tests pass.</p>
<li><strong>Hotfixes</strong>: Allowed outside the cadence for security, data loss, startup</li>
<p>failures, or ComfyUI compatibility breaks.</p>
</ul>
<h3 id="contributing-typical-workflow">Typical Workflow</h3>
<pre><code class="language-bash"># 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</code></pre>
<h3 id="contributing-pre-push-quality-gate">Pre-Push Quality Gate</h3>
<p>The pre-push hook automatically runs:</p>
<pre><code class="language-bash">python scripts/run_changed_quality_gate.py</code></pre>
<p>This checks changed files only (faster than full quality gate).</p>
<hr />
<h2 id="contributing-code-quality-gate">Code Quality Gate</h2>
<p>The quality gate enforces:</p>
<h3 id="contributing-python-checks">Python Checks</h3>
<ul>
<li>✅ UTF-8 text files without BOM</li>
<li>✅ Ruff linting (changed files during migration window)</li>
<li>✅ MyPy type checking</li>
<li>✅ Bandit security linting</li>
<li>✅ pip-audit dependency auditing</li>
<li>✅ Xenon/Radon complexity checks</li>
<li>✅ Backend tests pass</li>
</ul>
<h3 id="contributing-frontend-checks">Frontend Checks</h3>
<ul>
<li>✅ ESLint passes</li>
<li>✅ Prettier formatting</li>
<li>✅ Vitest tests pass</li>
<li>✅ npm audit passes</li>
</ul>
<h3 id="contributing-running-the-gate">Running the Gate</h3>
<pre><code class="language-bash"># 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</code></pre>
<p><strong>Important</strong>: The quality gate <strong>must pass</strong> before PRs are merged.</p>
<hr />
<h2 id="contributing-documentation">Documentation</h2>
<h3 id="contributing-when-to-update-docs">When to Update Docs</h3>
<p>Update documentation when:</p>
<ul>
<li>Adding new features or changing behavior</li>
<li>Modifying API endpoints or configuration</li>
<li>Introducing breaking changes</li>
<li>Fixing bugs that affect user-facing behavior</li>
</ul>
<h3 id="contributing-documentation-standards">Documentation Standards</h3>
<ul>
<li><strong>Language</strong>: English (all user-facing docs)</li>
<li><strong>Format</strong>: Markdown (<code>.md</code> files)</li>
<li><strong>Location</strong>: <code>docs/</code> directory</li>
<li><strong>Index</strong>: Update <a href="#documentation_index">&lt;code&gt;docs/DOCUMENTATION_INDEX.md&lt;/code&gt;</a> for new docs</li>
</ul>
<h3 id="contributing-types-of-documentation">Types of Documentation</h3>
<div class="table-wrap"><table>
<thead><tr><th>Document Type</th><th>Purpose</th><th>Location</th></tr></thead>
<tbody>
<tr><td><strong>User Guides</strong></td><td>How to use features</td><td><code>docs/*.md</code></td></tr>
<tr><td><strong>API Reference</strong></td><td>Backend endpoints</td><td><code>docs/API_REFERENCE.md</code></td></tr>
<tr><td><strong>Architecture</strong></td><td>Design decisions</td><td><code>docs/adr/</code></td></tr>
<tr><td><strong>Configuration</strong></td><td>Settings &amp; env vars</td><td><code>docs/SETTINGS_CONFIGURATION.md</code></td></tr>
<tr><td><strong>Security</strong></td><td>Threat model, hardening</td><td><code>docs/SECURITY_ENV_VARS.md</code>, <code>docs/THREAT_MODEL.md</code></td></tr>
</tbody></table></div>
<h3 id="contributing-architecture-decision-records-adrs">Architecture Decision Records (ADRs)</h3>
<p>For significant architectural decisions:</p>
<ol>
<li>Create <code>docs/adr/NNNN-short-title.md</code></li>
<li>Follow ADR template (context, decision, consequences)</li>
<li>Reference in <code>docs/adr/README.md</code></li>
</ol>
<p>See existing ADRs for examples.</p>
<hr />
<h2 id="contributing-getting-help">Getting Help</h2>
<h3 id="contributing-resources">Resources</h3>
<ul>
<li><strong>Documentation</strong>: <a href="#documentation_index">&lt;code&gt;docs/DOCUMENTATION_INDEX.md&lt;/code&gt;</a></li>
<li><strong>API Reference</strong>: <a href="#api_reference">&lt;code&gt;docs/API_REFERENCE.md&lt;/code&gt;</a></li>
<li><strong>Architecture</strong>: <a href="#architecture_map">&lt;code&gt;docs/ARCHITECTURE_MAP.md&lt;/code&gt;</a></li>
<li><strong>Changelog</strong>: <a href="docs/CHANGELOG.md">&lt;code&gt;CHANGELOG.md&lt;/code&gt;</a></li>
</ul>
<h3 id="contributing-contact">Contact</h3>
<ul>
<li><strong>GitHub Issues</strong>: <a href="https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/issues" target="_blank" rel="noopener noreferrer">Report bugs or request features</a></li>
<li><strong>GitHub Discussions</strong>: <a href="https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/discussions" target="_blank" rel="noopener noreferrer">Ask questions or share ideas</a></li>
</ul>
<h3 id="contributing-common-questions">Common Questions</h3>
<p><strong>Q: How do I test changes locally?</strong></p>
<pre><code class="language-bash"># Rebuild frontend
npm run build
# Restart ComfyUI (extension loads from dist/)
# Test in browser, iterate quickly</code></pre>
<p><strong>Q: Can I add a new dependency?</strong></p>
<ul>
<li><strong>Runtime</strong>: Must be justified and approved (keep minimal)</li>
<li><strong>Dev</strong>: Easier to approve (testing, linting tools)</li>
<li><strong>Optional</strong>: AI/vector deps in <code>requirements-vector.txt</code></li>
</ul>
<p><strong>Q: What if the quality gate fails on CI but passes locally?</strong></p>
<ul>
<li>Check Python/Node version differences</li>
<li>Ensure all files are committed (CI checks entire repo)</li>
<li>Run <code>npm run quality</code> from clean state</li>
</ul>
<hr />
<h2 id="contributing-recognition">Recognition</h2>
<p>Contributors are recognized in:</p>
<ul>
<li>GitHub contributors page</li>
<li>Release notes (for significant contributions)</li>
<li>This file (optional, add your name below)</li>
</ul>
<h3 id="contributing-contributors">Contributors</h3>
<ul>
<li><strong>MajoorWaldi</strong> — Original creator and lead maintainer</li>
<li>_[Your name here — submit a PR to add yourself!]_</li>
</ul>
<hr />
<h2 id="contributing-license">License</h2>
<p>By contributing, you agree that your contributions will be licensed under the project's <a href="docs/LICENSE">LICENSE</a>.</p>
<p>Thank you for helping make Majoor Assets Manager better! 🎉</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/CONTRIBUTING.md</code>
</div>
</div>
</section>
<section class="panel section" id="custom_nodes">
<div class="panel-h"><h2>CUSTOM_NODES.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="custom_nodes-custom-nodes-reference">Custom Nodes Reference</h1>
<p>Majoor Assets Manager provides two ComfyUI nodes that embed <strong>generation timing metadata</strong> directly inside saved files. The asset manager's indexing pipeline then reads this metadata automatically.</p>
<p><strong>Source</strong>: <a href="nodes.py">&lt;code&gt;nodes.py&lt;/code&gt;</a></p>
<hr />
<h2 id="custom_nodes-majoor-save-image">〽️ Majoor Save Image 💾</h2>
<p><strong>Node name</strong>: <code>MajoorSaveImage</code> <strong>Category</strong>: Majoor <strong>Display name</strong>: 〽️ Majoor Save Image 💾</p>
<p>Drop-in replacement for ComfyUI's built-in <code>SaveImage</code> node. Saves PNG files to the standard output directory with <code>generation_time_ms</code> persisted in the PNG text chunks.</p>
<h3 id="custom_nodes-inputs">Inputs</h3>
<div class="table-wrap"><table>
<thead><tr><th>Input</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>images</code></td><td>IMAGE</td><td>✅</td><td>—</td><td>The image batch to save</td></tr>
<tr><td><code>filename_prefix</code></td><td>STRING</td><td>✅</td><td><code>Majoor</code></td><td>Filename prefix. Supports ComfyUI formatting placeholders (<code>%date%</code>, <code>%batch_num%</code>, etc.)</td></tr>
<tr><td><code>generation_time_ms</code></td><td>INT</td><td>❌</td><td><code>-1</code></td><td>Generation time in milliseconds. Set to <code>-1</code> for automatic detection from the prompt lifecycle</td></tr>
</tbody></table></div>
<h3 id="custom_nodes-hidden-inputs">Hidden Inputs</h3>
<div class="table-wrap"><table>
<thead><tr><th>Input</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>prompt</code></td><td>PROMPT</td><td>Full ComfyUI prompt graph (auto-provided)</td></tr>
<tr><td><code>extra_pnginfo</code></td><td>EXTRA_PNGINFO</td><td>Additional PNG metadata (workflow, etc.)</td></tr>
</tbody></table></div>
<h3 id="custom_nodes-metadata-written">Metadata Written</h3>
<p>Each saved PNG contains the following text chunks:</p>
<div class="table-wrap"><table>
<thead><tr><th>Key</th><th>Content</th></tr></thead>
<tbody>
<tr><td><code>prompt</code></td><td>Full prompt graph as JSON</td></tr>
<tr><td><code>workflow</code></td><td>Full workflow as JSON (via <code>extra_pnginfo</code>)</td></tr>
<tr><td><code>generation_time_ms</code></td><td>Elapsed time since prompt start, in milliseconds</td></tr>
<tr><td><code>CreationTime</code></td><td>ISO 8601 timestamp (<code>YYYY-MM-DD HH:MM:SS</code>)</td></tr>
</tbody></table></div>
<h3 id="custom_nodes-output">Output</h3>
<p>Returns a UI result with the list of saved images (filename, subfolder, type) for ComfyUI's preview system.</p>
<h3 id="custom_nodes-example-usage">Example Usage</h3>
<ol>
<li>Connect any image output to the <code>images</code> input</li>
<li>Optionally set a custom <code>filename_prefix</code></li>
<li>Leave <code>generation_time_ms</code> at <code>-1</code> for automatic timing</li>
<li>The node saves PNGs to <code>ComfyUI/output/</code> with full metadata</li>
</ol>
<hr />
<h2 id="custom_nodes-majoor-save-video">〽️ Majoor Save Video 🎬</h2>
<p><strong>Node name</strong>: <code>MajoorSaveVideo</code> <strong>Category</strong>: Majoor <strong>Display name</strong>: 〽️ Majoor Save Video 🎬</p>
<p>Saves a <strong>VIDEO</strong> input or a batch of <strong>IMAGE</strong> frames as a video file. Uses <strong>PyAV</strong> for MP4 encoding (same approach as ComfyUI's native <code>SaveVideo</code> node) and <strong>Pillow</strong> for GIF/WebP.</p>
<h3 id="custom_nodes-inputs">Inputs</h3>
<div class="table-wrap"><table>
<thead><tr><th>Input</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>filename_prefix</code></td><td>STRING</td><td>✅</td><td><code>MajoorVideo</code></td><td>Filename prefix</td></tr>
<tr><td><code>format</code></td><td>COMBO</td><td>✅</td><td><code>mp4 (h264)</code></td><td>Output format: <code>mp4 (h264)</code>, <code>gif</code>, <code>webp</code></td></tr>
<tr><td><code>images</code></td><td>IMAGE</td><td>❌</td><td>—</td><td>Batch of frames to encode as video</td></tr>
<tr><td><code>video</code></td><td>VIDEO</td><td>❌</td><td>—</td><td>A VIDEO input (from LoadVideo, CreateVideo, etc.)</td></tr>
<tr><td><code>frame_rate</code></td><td>FLOAT</td><td>❌</td><td><code>24.0</code></td><td>Frames per second (1–120). Ignored when <code>video</code> input carries its own frame rate</td></tr>
<tr><td><code>loop_count</code></td><td>INT</td><td>❌</td><td><code>0</code></td><td>Loop count for GIF/WebP. 0 = infinite loop</td></tr>
<tr><td><code>generation_time_ms</code></td><td>INT</td><td>❌</td><td><code>-1</code></td><td>Generation time in ms. <code>-1</code> = auto-detect</td></tr>
<tr><td><code>audio</code></td><td>AUDIO</td><td>❌</td><td>—</td><td>Audio track to mux into the MP4 container</td></tr>
<tr><td><code>crf</code></td><td>INT</td><td>❌</td><td><code>19</code></td><td>Constant Rate Factor (0–63). Lower = higher quality, larger file</td></tr>
<tr><td><code>save_first_frame</code></td><td>BOOLEAN</td><td>❌</td><td><code>true</code></td><td>Save a PNG sidecar of the first frame with full metadata</td></tr>
</tbody></table></div>
<h3 id="custom_nodes-hidden-inputs">Hidden Inputs</h3>
<div class="table-wrap"><table>
<thead><tr><th>Input</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>prompt</code></td><td>PROMPT</td><td>Full ComfyUI prompt graph</td></tr>
<tr><td><code>extra_pnginfo</code></td><td>EXTRA_PNGINFO</td><td>Additional metadata (workflow, etc.)</td></tr>
</tbody></table></div>
<h3 id="custom_nodes-input-resolution">Input Resolution</h3>
<p>At least one of <code>images</code> or <code>video</code> must be connected:</p>
<ul>
<li><strong><code>video</code> input</strong> (priority): frame tensor, frame rate, and audio are extracted via <code>video.get_components()</code>. The <code>frame_rate</code> widget is ignored.</li>
<li><strong><code>images</code> input</strong> (fallback): frames are taken from the IMAGE batch, and <code>frame_rate</code> / <code>audio</code> widgets are used.</li>
<li><strong>Neither connected</strong>: the node produces no output.</li>
</ul>
<h3 id="custom_nodes-metadata-written">Metadata Written</h3>
<h4 id="custom_nodes-mp4-h264">MP4 (h264)</h4>
<p>Metadata is embedded directly in the MP4 container using PyAV with <code>movflags=use_metadata_tags</code>:</p>
<div class="table-wrap"><table>
<thead><tr><th>Key</th><th>Content</th></tr></thead>
<tbody>
<tr><td><code>prompt</code></td><td>Full prompt graph as JSON</td></tr>
<tr><td><code>workflow</code></td><td>Full workflow as JSON</td></tr>
<tr><td><code>generation_time_ms</code></td><td>Elapsed time since prompt start, in milliseconds</td></tr>
<tr><td><code>CreationTime</code></td><td>ISO 8601 timestamp</td></tr>
</tbody></table></div>
<p>These tags are readable by FFProbe (<code>ffprobe -show_format_tags</code>) and ExifTool.</p>
<h4 id="custom_nodes-gif-webp">GIF / WebP</h4>
<p>Animated GIF and WebP formats do not support arbitrary metadata. When <code>save_first_frame</code> is enabled (default), a <strong>PNG sidecar</strong> is saved alongside the animation with full metadata in its text chunks. The asset manager indexes this sidecar automatically.</p>
<h3 id="custom_nodes-video-encoding-details">Video Encoding Details</h3>
<ul>
<li><strong>Codec</strong>: libx264, pixel format yuv420p</li>
<li><strong>Quality</strong>: controlled by <code>crf</code> (default 19)</li>
<li><strong>Audio</strong>: AAC codec, supports mono/stereo/5.1 layouts</li>
<li><strong>Frame rate</strong>: stored as an exact fraction for precision</li>
</ul>
<h3 id="custom_nodes-output">Output</h3>
<p>Returns a UI result with the saved video file(s) for ComfyUI's preview system.</p>
<h3 id="custom_nodes-example-usage">Example Usage</h3>
<p><strong>From IMAGE batch:</strong></p>
<ol>
<li>Connect a batch of images to <code>images</code></li>
<li>Set <code>format</code> to <code>mp4 (h264)</code></li>
<li>Adjust <code>frame_rate</code> and <code>crf</code> as needed</li>
<li>The node saves an MP4 to <code>ComfyUI/output/</code></li>
</ol>
<p><strong>From VIDEO input:</strong></p>
<ol>
<li>Connect a VIDEO output to <code>video</code></li>
<li>The node uses the video's native frame rate and audio</li>
<li>Set <code>format</code> and <code>crf</code> to control encoding</li>
</ol>
<hr />
<h2 id="custom_nodes-auto-detection-of-generation_time_ms">Auto-Detection of <code>generation_time_ms</code></h2>
<p>Both nodes support automatic generation time measurement. When <code>generation_time_ms</code> is set to <code>-1</code> (default), the node computes the elapsed time by reading the prompt start time from Majoor's <code>runtime_activity</code> module.</p>
<h3 id="custom_nodes-how-it-works">How It Works</h3>
<ol>
<li>When ComfyUI starts executing a prompt, <code>runtime_activity</code> records <code>time.monotonic()</code> as <code>last_started_at</code></li>
<li>When the save node runs, it computes <code>(time.monotonic() - last_started_at) * 1000</code> to get milliseconds</li>
<li>This value is written into the file metadata</li>
</ol>
<h3 id="custom_nodes-manual-override">Manual Override</h3>
<p>Connect an INT value to <code>generation_time_ms</code> (any value ≥ 0) to override automatic detection. This is useful for:</p>
<ul>
<li>Measuring only part of a workflow</li>
<li>Importing timing from external sources</li>
<li>Testing and debugging</li>
</ul>
<hr />
<h2 id="custom_nodes-majoor-geninfo-override">〽️ Majoor GenInfo Override 🧩</h2>
<p><strong>Node name</strong>: <code>MajoorGenInfoOverride</code> <strong>Category</strong>: Majoor <strong>Display name</strong>: 〽️ Majoor GenInfo Override 🧩</p>
<p>Adds custom/manual GenInfo fields that can be consumed by Majoor Save nodes so the final saved asset includes your override data in embedded metadata.</p>
<h3 id="custom_nodes-typical-use-case">Typical Use Case</h3>
<p>Use this node when you want to:</p>
<ul>
<li>override detected values shown in GenInfo,</li>
<li>inject custom fields for downstream indexing/search,</li>
<li>attach additional manual context before saving with Majoor Save Image or Majoor Save Video.</li>
</ul>
<h3 id="custom_nodes-integration-notes">Integration Notes</h3>
<ul>
<li>Place the node upstream of your Majoor Save node path.</li>
<li>Save nodes serialize the merged metadata into file metadata (PNG text chunks or MP4 tags where applicable).</li>
<li>Indexed assets then expose those values in Majoor Assets Manager GenInfo and metadata search flows.</li>
</ul>
<hr />
<h2 id="custom_nodes-extraction-pipeline">Extraction Pipeline</h2>
<p>When assets are indexed by Majoor Assets Manager, <code>generation_time_ms</code> is extracted through the following chain:</p>
<h3 id="custom_nodes-png-files">PNG Files</h3>
<ol>
<li><code>read_png_text_chunks()</code> reads the <code>generation_time_ms</code> text chunk → <code>PNG:Generation_time_ms</code></li>
<li><code>_merge_png_exif()</code> merges it into the EXIF data dict</li>
<li><code>_apply_rating_tags_and_generation_time()</code> extracts the integer value → <code>metadata["generation_time_ms"]</code></li>
<li><code>_best_effort_generation_time_ms()</code> finds it at the top level and returns it for DB storage</li>
</ol>
<h3 id="custom_nodes-mp4-files">MP4 Files</h3>
<ol>
<li>FFProbe reads the container format tags (including <code>generation_time_ms</code>)</li>
<li><code>apply_video_ffprobe_fields()</code> extracts it from <code>format.tags.generation_time_ms</code></li>
<li><code>_best_effort_generation_time_ms()</code> finds it and returns it for DB storage</li>
</ol>
<h3 id="custom_nodes-fallback-behavior">Fallback Behavior</h3>
<p>When the Majoor save nodes are <strong>not</strong> used (e.g., standard SaveImage or third-party nodes), the asset manager falls back to:</p>
<ul>
<li>EXIF <code>DateTimeOriginal</code> / <code>CreateDate</code> for <code>generation_time</code> (date-based, not duration)</li>
<li>Prompt graph analysis for workflow metadata</li>
<li>No <code>generation_time_ms</code> is stored (column remains NULL)</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/CUSTOM_NODES.md</code>
</div>
</div>
</section>
<section class="panel section" id="db_maintenance">
<div class="panel-h"><h2>DB_MAINTENANCE.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="db_maintenance-database-maintenance">Database Maintenance</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 7, 2026</p>
<p>Majoor Assets Manager stores its index in an SQLite database. By default this is at <code>&amp;lt;output&amp;gt;/_mjr_index/assets.sqlite</code>, 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.</p>
<p><strong>Recent highlights</strong>: Improved corruption detection, automatic health monitoring, and safer rebuild flows.</p>
<h2 id="db_maintenance-configuring-the-index-directory">Configuring the Index Directory</h2>
<p>By default the index lives next to your assets at <code>&amp;lt;output&amp;gt;/_mjr_index/</code>. You can move it to any local path without touching your asset files.</p>
<h3 id="db_maintenance-why-you-might-want-to-relocate">Why you might want to relocate</h3>
<ul>
<li><strong>Network drives (NAS/SMB/CIFS)</strong>: 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.</li>
<li><strong>Separate disk for performance</strong>: Keep assets on a large slow HDD and the index/DB on a fast SSD.</li>
<li><strong>Shared ComfyUI install, separate indexes</strong>: Multiple users on the same machine can each point to their own index directory.</li>
</ul>
<h3 id="db_maintenance-how-to-configure">How to configure</h3>
<p><strong>Option 1 — UI (recommended, survives reinstalls):</strong></p>
<ol>
<li>Open Settings → <strong>Majoor Assets Manager</strong> → <strong>Advanced</strong> and search for <code>path</code> if needed.</li>
<li>Locate <strong>Index Database Directory</strong>.</li>
<li>Enter the full path to the desired directory (it will be created if it does not exist).</li>
<li>Save. A toast confirms the change and reminds you to restart ComfyUI.</li>
<li>Restart ComfyUI. A fresh scan runs on startup.</li>
</ol>
<p><a href="docs/images/index-directory-override-ui.svg" target="_blank" rel="noopener noreferrer">[Index directory override in Majoor settings]</a></p>
<p>The same section also exposes <strong>Generation Output Directory</strong> when you need to override the ComfyUI output folder independently from the database location.</p>
<p>The UI persists the path in a sidecar file <code>.mjr_index_directory_override</code> next to the extension root. This file takes precedence over the default on every startup.</p>
<p><strong>Option 2 — Environment variable:</strong></p>
<p>Set either <code>MJR_AM_INDEX_DIRECTORY</code> or <code>MAJOOR_INDEX_DIRECTORY</code> in the environment that launches ComfyUI:</p>
<pre><code class="language-batch">:: Windows — batch launcher
set MJR_AM_INDEX_DIRECTORY=C:\mjr_index
python main.py</code></pre>
<pre><code class="language-bash"># Linux/macOS — shell launcher
export MJR_AM_INDEX_DIRECTORY=/var/local/mjr_index
python main.py</code></pre>
<p>Env var overrides the sidecar file and the default. Restart ComfyUI after changing it.</p>
<p><strong>Priority order</strong> (highest wins):</p>
<ol>
<li><code>MJR_AM_INDEX_DIRECTORY</code> or <code>MAJOOR_INDEX_DIRECTORY</code> environment variable</li>
<li><code>.mjr_index_directory_override</code> sidecar file (written by the UI)</li>
<li>Default: <code>&amp;lt;output_directory&amp;gt;/_mjr_index/</code></li>
</ol>
<h3 id="db_maintenance-what-the-index-contains">What the index contains</h3>
<p>The index directory holds:</p>
<div class="table-wrap"><table>
<thead><tr><th>File</th><th>Contents</th></tr></thead>
<tbody>
<tr><td><code>assets.sqlite</code></td><td>Main index: metadata, ratings, tags, FTS search index, scan journal</td></tr>
<tr><td><code>assets.sqlite-wal</code> / <code>-shm</code></td><td>SQLite WAL and shared memory (transient, recreated automatically)</td></tr>
<tr><td><code>vectors.sqlite</code></td><td>Optional AI embeddings (vector search)</td></tr>
<tr><td><code>collections/</code></td><td>Collection JSON files (preserved across Delete DB)</td></tr>
<tr><td><code>custom_roots.json</code></td><td>Custom roots configuration (preserved across Delete DB)</td></tr>
</tbody></table></div>
<h3 id="db_maintenance-after-changing-the-index-directory">After changing the index directory</h3>
<ol>
<li>Restart ComfyUI to apply the new path.</li>
<li>A fresh scan starts automatically.</li>
<li>Ratings, tags, and AI vectors from the old database are <strong>not</strong> automatically migrated.</li>
<ul>
<li>To migrate: stop ComfyUI, copy the <code>.sqlite</code> files to the new directory, then restart.</li>
</ul>
<li>The old <code>_mjr_index</code> directory is not deleted automatically.</li>
</ol>
<hr />
<h2 id="db_maintenance-buttons-in-the-status-panel">Buttons in the Status Panel</h2>
<p>Open the Assets Manager panel and expand the <strong>Index Status</strong> section. Maintenance action buttons appear at the bottom:</p>
<div class="table-wrap"><table>
<thead><tr><th>Button</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td><strong>Backfill vectors</strong></td><td>Computes missing AI vector embeddings for existing assets.</td></tr>
<tr><td><strong>Memory purge</strong></td><td>Unloads Majoor AI models, asks ComfyUI to unload loaded models, and clears torch cache when the queue is idle.</td></tr>
<tr><td><strong>Reset index</strong></td><td>Clears cached data inside the existing database and triggers a background rescan. Requires the database to be readable.</td></tr>
<tr><td><strong>Delete DB</strong></td><td>Force-deletes the database files from disk (bypassing all DB-dependent checks) and rebuilds from scratch. Works even when the database is corrupted.</td></tr>
</tbody></table></div>
<h2 id="db_maintenance-reset-index">Reset Index</h2>
<p>The <strong>Reset index</strong> button calls <code>POST /mjr/am/scan/reset</code> with flags to clear assets, metadata, FTS index, and scan journal, then triggers a full rescan.</p>
<p>If the database is corrupted, the reset will fail because security-preference queries cannot execute on a malformed database. When this happens:</p>
<ul>
<li>The status dot turns red.</li>
<li>A toast appears: <em>"Reset failed -- database is corrupted. Use the Delete DB button to force-delete and rebuild."</em></li>
</ul>
<h2 id="db_maintenance-delete-db-emergency-recovery">Delete DB (Emergency Recovery)</h2>
<p>The <strong>Delete DB</strong> button calls <code>POST /mjr/am/db/force-delete</code>. It is designed to work even when the database is completely unreadable.</p>
<h3 id="db_maintenance-how-it-works">How it works</h3>
<ol>
<li><strong>CSRF check only</strong> -- no database-dependent security queries are run.</li>
<li><strong>Adapter reset (fast path)</strong> -- tries <code>db.areset()</code> first. If this succeeds the database is wiped through the adapter and a rescan starts immediately.</li>
<li><strong>Manual file deletion (fallback)</strong> -- if the adapter reset fails (typical for severe corruption):</li>
<ul>
<li>Calls <code>db.close()</code> to release connections.</li>
<li>Runs <code>gc.collect()</code> twice with a short delay to release Windows file handles.</li>
<li>Deletes <code>assets.sqlite</code>, <code>assets.sqlite-wal</code>, <code>assets.sqlite-shm</code>, and <code>assets.sqlite-journal</code> with up to 6 retries per file.</li>
</ul>
<li><strong>Re-initialization</strong> -- creates a fresh database with all tables, indexes, and triggers.</li>
<li><strong>Background rescan</strong> -- triggers a full non-incremental scan of the output directory.</li>
</ol>
<h3 id="db_maintenance-confirmation-dialog">Confirmation dialog</h3>
<p>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.</p>
<h3 id="db_maintenance-what-is-lost">What is lost</h3>
<ul>
<li>Star ratings</li>
<li>Custom tags</li>
<li>Cached metadata (prompts, models, generation parameters)</li>
<li>Scan journal / history</li>
</ul>
<h3 id="db_maintenance-what-is-preserved">What is preserved</h3>
<ul>
<li>Original image/video/audio files (never touched)</li>
<li>Collections (stored as separate JSON files in <code>_mjr_index/collections/</code>)</li>
<li>Custom roots configuration (<code>_mjr_index/custom_roots.json</code>)</li>
<li>All ComfyUI settings (stored in <code>localStorage</code>)</li>
</ul>
<h2 id="db_maintenance-automatic-corruption-detection">Automatic Corruption Detection</h2>
<p>The status polling loop (every few seconds) checks health counters via <code>GET /mjr/am/health/counters</code>. If the response error contains keywords like "malformed", "corrupt", or "disk image":</p>
<ul>
<li>The status dot turns red.</li>
<li>The status text shows <strong>"Database is corrupted"</strong> with a hint to use the Delete DB button.</li>
<li>A one-time toast notification appears with the same guidance.</li>
</ul>
<p>The toast fires only once per session to avoid spamming. It resets after a successful Delete DB operation.</p>
<h2 id="db_maintenance-database-optimize">Database Optimize</h2>
<p>An additional endpoint <code>POST /mjr/am/db/optimize</code> runs <code>PRAGMA optimize</code> and <code>ANALYZE</code> 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.</p>
<h2 id="db_maintenance-manual-recovery">Manual Recovery</h2>
<p>If the Delete DB button reports that files could not be deleted (another process holds a lock):</p>
<ol>
<li>Stop ComfyUI completely.</li>
<li>Navigate to <code>&amp;lt;output&amp;gt;/_mjr_index/</code>.</li>
<li>Delete <code>assets.sqlite</code> and any sibling files (<code>-wal</code>, <code>-shm</code>, <code>-journal</code>).</li>
<li>Restart ComfyUI.</li>
<li>The database will be recreated automatically on startup and a scan will populate it.</li>
</ol>
<h2 id="db_maintenance-related-files">Related Files</h2>
<div class="table-wrap"><table>
<thead><tr><th>File</th><th>Role</th></tr></thead>
<tbody>
<tr><td><code>mjr_am_backend/routes/handlers/db_maintenance.py</code></td><td><code>/db/optimize</code> and <code>/db/force-delete</code> endpoints</td></tr>
<tr><td><code>mjr_am_backend/adapters/db/sqlite.py</code></td><td>DB adapter with malformed detection and online recovery</td></tr>
<tr><td><code>ui/features/status/StatusDot.js</code></td><td>Frontend status polling, corruption detection, Reset/Delete buttons</td></tr>
<tr><td><code>ui/api/client.js</code></td><td><code>forceDeleteDb()</code> API call</td></tr>
</tbody></table></div>
<h2 id="db_maintenance-environment-variables">Environment Variables</h2>
<div class="table-wrap"><table>
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>MJR_AM_INDEX_DIRECTORY</code> / <code>MAJOOR_INDEX_DIRECTORY</code></td><td><code>&amp;lt;output&amp;gt;/_mjr_index/</code></td><td>Override the index database directory</td></tr>
<tr><td><code>MAJOOR_DB_TIMEOUT</code></td><td><code>30.0</code></td><td>SQLite busy timeout (seconds)</td></tr>
<tr><td><code>MAJOOR_DB_MAX_CONNECTIONS</code></td><td><code>8</code></td><td>Maximum concurrent DB connections</td></tr>
<tr><td><code>MAJOOR_DB_QUERY_TIMEOUT</code></td><td><code>60.0</code></td><td>Per-query timeout (seconds)</td></tr>
</tbody></table></div></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/DB_MAINTENANCE.md</code>
</div>
</div>
</section>
<section class="panel section" id="dependency_policy">
<div class="panel-h"><h2>DEPENDENCY_POLICY.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="dependency_policy-dependency-policy">Dependency Policy</h1>
<p>Date: 2026-04-09</p>
<h2 id="dependency_policy-goal">Goal</h2>
<p>Keep one explicit dependency contract per use case and avoid drift between packaging metadata, manual installs, and CI tooling.</p>
<h2 id="dependency_policy-source-of-truth">Source Of Truth</h2>
<ul>
<li><code>requirements.txt</code> is the primary dependency source of truth for the project.</li>
<li><code>requirements.txt</code> is also the default runtime install contract for the extension.</li>
<li><code>requirements-vector.txt</code> extends runtime with optional AI/vector dependencies.</li>
<li><code>requirements-dev.txt</code> is the contributor tooling layer for tests, linting, typing, and security checks.</li>
<li><code>pyproject.toml</code> mirrors published metadata and optional dependency groups for packaging, but does not replace <code>requirements.txt</code> as the source of truth used by users and CI.</li>
</ul>
<h2 id="dependency_policy-dependency-roles">Dependency Roles</h2>
<h3 id="dependency_policy-runtime">Runtime</h3>
<p>Put a dependency in <code>requirements.txt</code> only if the extension needs it for normal backend operation.</p>
<p>Examples:</p>
<ul>
<li><code>aiohttp</code></li>
<li><code>aiosqlite</code></li>
<li><code>pillow</code></li>
<li><code>av</code></li>
<li><code>send2trash</code></li>
</ul>
<h3 id="dependency_policy-optional-ai-vector">Optional AI / Vector</h3>
<p>Put a dependency in <code>requirements-vector.txt</code> only if it is required for optional AI, vector search, captioning, clustering, or related enrichment features.</p>
<p>This file must include <code>-r requirements.txt</code> so it always layers on top of the runtime baseline.</p>
<h3 id="dependency_policy-dev-contributor-tooling">Dev / Contributor Tooling</h3>
<p>Put a dependency in <code>requirements-dev.txt</code> only if it is used to develop, test, lint, type-check, or audit the project locally or in CI.</p>
<p>Examples:</p>
<ul>
<li><code>pytest</code></li>
<li><code>ruff</code></li>
<li><code>mypy</code></li>
<li><code>bandit</code></li>
<li><code>pip-audit</code></li>
</ul>
<p>Dev tools must not be added to <code>requirements.txt</code> just because CI uses them.</p>
<h2 id="dependency_policy-update-rules">Update Rules</h2>
<p>When adding or changing a dependency:</p>
<ol>
<li>Decide whether it is <code>runtime</code>, <code>vector</code>, or <code>dev</code>.</li>
<li>Update <code>requirements.txt</code> first when the dependency affects the main project baseline.</li>
<li>Update the matching extension file when the dependency is optional or contributor-only.</li>
<li>Mirror the change in <code>pyproject.toml</code> when it affects published metadata.</li>
<li>Update docs if the install story or contributor workflow changed.</li>
<li>Run the relevant tests or quality checks for the impacted area.</li>
</ol>
<h2 id="dependency_policy-ci-and-quality-gate-expectations">CI And Quality Gate Expectations</h2>
<ul>
<li>Runtime security auditing targets <code>requirements.txt</code>.</li>
<li>Contributor tooling is installed from <code>requirements-dev.txt</code> for local development and quality workflows.</li>
<li>Optional AI/vector installs remain opt-in and must not become implicit in the default setup.</li>
</ul>
<h2 id="dependency_policy-non-goals">Non-Goals</h2>
<ul>
<li>No hidden dependency source in ad hoc scripts.</li>
<li>No “install everything always” default for end users.</li>
<li>No divergence between README instructions and the actual files used by installs.</li>
</ul>
<h2 id="dependency_policy-quick-commands">Quick Commands</h2>
<pre><code class="language-bash"># 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.txt</code></pre></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/DEPENDENCY_POLICY.md</code>
</div>
</div>
</section>
<section class="panel section" id="desktop_popup_workaround">
<div class="panel-h"><h2>DESKTOP_POPUP_WORKAROUND.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="desktop_popup_workaround-comfyui-desktop-popup-workaround">ComfyUI Desktop Popup Workaround</h1>
<p><strong>Version</strong>: 2.4.9</p>
<p>Use this only if you run the official <strong>ComfyUI Desktop / Electron</strong> build and want the <strong>Majoor Floating Viewer</strong> to open in a real detachable window that can be moved to another monitor.</p>
<p>The Majoor plugin already tries to open a real popup first on Desktop. Some Desktop builds still block <code>window.open("about:blank")</code> in the Electron host and redirect it to the OS instead. The workaround below patches an internal packaged Desktop file, so treat it as advanced and unsupported:</p>
<ul>
<li>Back up the file before editing it.</li>
<li>Expect the file path or method name to change between Desktop releases.</li>
<li>Reapply only if a Desktop update removes the change.</li>
<li>Browser-based ComfyUI does not require this patch.</li>
</ul>
<p>Typical file to patch in an extracted Desktop app:</p>
<pre><code class="language-text">.vite/build/main.cjs</code></pre>
<p>Find the <code>#shouldOpenInPopup(url2)</code> method and make sure it allows <code>about:blank</code>, <code>127.0.0.1</code>, and <code>localhost</code>:</p>
<pre><code class="language-js"> #shouldOpenInPopup(url2) {
return url2 === "about:blank"
|| url2.startsWith("http://127.0.0.1:")
|| url2.startsWith("http://localhost:")
|| url2.startsWith("https://dreamboothy.firebaseapp.com/")
|| url2.startsWith("https://checkout.comfy.org/")
|| url2.startsWith("https://accounts.google.com/")
|| url2.startsWith("https://github.com/login/oauth/");
}</code></pre>
<p>Ensure the window-open handler still allows popup creation for approved URLs:</p>
<pre><code class="language-js"> this.window.webContents.setWindowOpenHandler(({ url: url2 }) =&gt; {
if (this.#shouldOpenInPopup(url2)) {
return {
action: "allow",
overrideBrowserWindowOptions: {
webPreferences: { preload: void 0 },
},
};
}
electron.shell.openExternal(url2);
return { action: "deny" };
});</code></pre>
<p>After patching the Desktop host:</p>
<ol>
<li>Repack the Desktop app archive if needed.</li>
<li>Restart ComfyUI Desktop completely.</li>
<li>Reopen Majoor Assets Manager.</li>
<li>Use the MFV pop-out button. It should now open a real detachable window that can be moved to another screen.</li>
</ol>
<p>If your Desktop build already allows these popup URLs, no host change is required.</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/DESKTOP_POPUP_WORKAROUND.md</code>
</div>
</div>
</section>
<section class="panel section" id="drag_drop">
<div class="panel-h"><h2>DRAG_DROP.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="drag_drop-majoor-assets-manager-drag-drop-guide">Majoor Assets Manager - Drag &amp; Drop Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: May 18, 2026</p>
<h2 id="drag_drop-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: Audio file staging support, improved multi-select ZIP creation, S+drag clean export, and better node compatibility detection.</p>
<h2 id="drag_drop-drag-to-comfyui-canvas">Drag to ComfyUI Canvas</h2>
<h3 id="drag_drop-basic-drag-operation">Basic Drag Operation</h3>
<ol>
<li>Select one or more assets in the Assets Manager</li>
<li>Click and hold on an asset card</li>
<li>Drag the asset onto the ComfyUI canvas</li>
<li>Release the mouse button to drop the asset</li>
</ol>
<h3 id="drag_drop-single-asset-drag">Single Asset Drag</h3>
<ul>
<li>Drag any single asset to the canvas</li>
<li>Dropping on a compatible loader node stages the asset and injects the staged path</li>
<li>Dropping media on empty canvas imports a valid embedded workflow when present; otherwise it creates the matching loader node</li>
<li>Hold <strong>L</strong> while dropping on empty canvas to skip embedded workflow import and create the matching media loader node directly. With a multi-selection, one loader node is created per selected asset.</li>
<li>The staged copy is placed under the ComfyUI input directory for immediate use</li>
</ul>
<h3 id="drag_drop-multiple-asset-drag">Multiple Asset Drag</h3>
<ul>
<li>Select multiple assets using Ctrl/Cmd+click or Shift+click</li>
<li>Drag any selected asset outside the browser to create a ZIP for OS/file-manager drops</li>
<li>Hold <strong>L</strong> while dropping a multi-selection on empty canvas to create one matching loader node per selected asset</li>
<li>Dropping a multi-selection on an existing node still injects only the card that started the drag into that node</li>
</ul>
<h3 id="drag_drop-node-compatibility">Node Compatibility</h3>
<p>The system intelligently identifies compatible nodes:</p>
<h4 id="drag_drop-image-input-nodes">Image Input Nodes</h4>
<ul>
<li><strong>LoadImage</strong>: Accepts image file paths</li>
<li><strong>LoadImageMask</strong>: For mask images</li>
</ul>
<h4 id="drag_drop-video-input-nodes">Video Input Nodes</h4>
<ul>
<li><strong>LoadVideo</strong>: For video file paths</li>
<li><strong>VideoLoad</strong>: For various video formats</li>
</ul>
<h4 id="drag_drop-other-media-input-nodes">Other Media Input Nodes</h4>
<ul>
<li>Audio loader nodes with audio path widgets</li>
<li>3D/model loader nodes with model, mesh, geometry, or scene path widgets</li>
</ul>
<h3 id="drag_drop-staging-mechanism">Staging Mechanism</h3>
<ul>
<li>Assets are copied or hard-linked into the ComfyUI input directory before node injection</li>
<li>Paths are injected into compatible input widgets after staging completes</li>
<li>Empty-canvas image drops can create a LoadImage node using the staged path</li>
<li>Staging writes an input-side file; the workflow itself changes only when a node is created or a widget value is updated</li>
</ul>
<h2 id="drag_drop-drag-to-operating-system">Drag to Operating System</h2>
<h3 id="drag_drop-single-file-drag">Single File Drag</h3>
<ol>
<li>Select an asset in the Assets Manager</li>
<li>Click and drag the asset outside the browser window</li>
<li>Drop onto your file explorer, desktop, or another application</li>
<li>The original file is transferred to the destination</li>
</ol>
<h3 id="drag_drop-multiple-file-drag">Multiple File Drag</h3>
<ol>
<li>Select multiple assets using Ctrl/Cmd+click or Shift+click</li>
<li>Drag any selected asset outside the browser window</li>
<li>A ZIP file is automatically created containing all selected assets</li>
<li>Drop the ZIP file onto your destination</li>
</ol>
<h3 id="drag_drop-clean-export-without-comfyui-metadata">Clean Export Without ComfyUI Metadata</h3>
<ul>
<li>Normal drag-out preserves the original file and does not show a metadata prompt.</li>
<li>Hold <strong>S</strong> while starting the drag to export clean copies with ComfyUI workflow/generation metadata stripped.</li>
<li>S+drag works for one or more selected assets. For multiple selected assets, a single clean ZIP is created for the OS drop.</li>
<li>If an asset has no supported embedded metadata to strip, it is exported unchanged.</li>
</ul>
<h3 id="drag_drop-zip-creation-process">ZIP Creation Process</h3>
<ul>
<li>ZIP files are created on-demand when dragging multiple items</li>
<li>Files are streamed directly from disk (no temporary copies)</li>
<li>ZIPs are flat (no directory structure maintained)</li>
<li>ZIP files are automatically cleaned up after transfer</li>
</ul>
<h3 id="drag_drop-protocol-support">Protocol Support</h3>
<p>The system supports multiple drag protocols:</p>
<h4 id="drag_drop-downloadurl-protocol">DownloadURL Protocol</h4>
<ul>
<li>Used for single file transfers</li>
<li>Direct file download to destination</li>
<li>Maintains original filename and extension</li>
</ul>
<h4 id="drag_drop-texturi-list-protocol">text/uri-list Protocol</h4>
<ul>
<li>Used for multiple file transfers</li>
<li>Properly formatted URI list for operating system</li>
<li>Compatible with most file managers and applications</li>
</ul>
<h2 id="drag_drop-drag-operations">Drag Operations</h2>
<h3 id="drag_drop-starting-a-drag">Starting a Drag</h3>
<ul>
<li>Click and hold on any asset card for a moment</li>
<li>A visual indicator appears showing the drag is active</li>
<li>The cursor changes to indicate drag operation</li>
<li>Selection remains active during drag</li>
</ul>
<h3 id="drag_drop-drag-indicators">Drag Indicators</h3>
<ul>
<li><strong>Visual Outline</strong>: Selected assets show a highlighted border</li>
<li><strong>Cursor Change</strong>: Cursor changes to indicate drag capability</li>
<li><strong>Preview</strong>: Small preview of dragged asset(s) follows cursor</li>
<li><strong>Count Display</strong>: For multiple selections, shows number of items</li>
</ul>
<h3 id="drag_drop-drag-targets">Drag Targets</h3>
<p>The system recognizes different drag targets:</p>
<h4 id="drag_drop-valid-targets">Valid Targets</h4>
<ul>
<li>ComfyUI canvas area</li>
<li>Compatible input nodes</li>
<li>Browser address bar (downloads)</li>
<li>File explorer windows</li>
<li>Desktop</li>
<li>Email attachments</li>
<li>Other applications accepting files</li>
</ul>
<h4 id="drag_drop-invalid-targets">Invalid Targets</h4>
<ul>
<li>Text input fields (unless specifically designed for file drops)</li>
<li>Non-compatible areas of the interface</li>
<li>Applications that don't accept the file type</li>
</ul>
<h2 id="drag_drop-file-transfer-mechanisms">File Transfer Mechanisms</h2>
<h3 id="drag_drop-direct-file-transfer">Direct File Transfer</h3>
<ul>
<li>Single files are transferred directly</li>
<li>Maintains original file properties</li>
<li>Preserves embedded metadata</li>
<li>No quality loss during transfer</li>
</ul>
<h3 id="drag_drop-batch-zip-transfer">Batch ZIP Transfer</h3>
<ul>
<li>Multiple files are packaged into a ZIP archive</li>
<li>ZIP created dynamically during drag operation</li>
<li>No temporary files written to disk</li>
<li>Automatic cleanup after transfer completion</li>
</ul>
<h3 id="drag_drop-streaming-transfer">Streaming Transfer</h3>
<ul>
<li>Files are streamed directly from disk</li>
<li>No intermediate copies created</li>
<li>Memory efficient for large files</li>
<li>Maintains file integrity during transfer</li>
</ul>
<h2 id="drag_drop-filename-collision-indicator">Filename Collision Indicator</h2>
<h3 id="drag_drop-understanding-the-indicator">Understanding the Indicator</h3>
<ul>
<li>When multiple assets share the same filename, the extension badge shows <code>EXT+</code></li>
<li>For example: <code>image.png</code> becomes <code>PNG+</code> when duplicates exist</li>
<li>Helps identify potential conflicts during drag operations</li>
</ul>
<h3 id="drag_drop-handling-collisions">Handling Collisions</h3>
<ul>
<li>Collisions don't prevent drag operations</li>
<li>ZIP files handle duplicate names automatically</li>
<li>Individual file drops may require manual renaming</li>
<li>Consider renaming files to avoid confusion</li>
</ul>
<h2 id="drag_drop-drag-performance">Drag Performance</h2>
<h3 id="drag_drop-optimizations">Optimizations</h3>
<ul>
<li>Lightweight preview during drag</li>
<li>Asynchronous file operations</li>
<li>Memory-efficient streaming</li>
<li>Responsive interface during drag operations</li>
</ul>
<h3 id="drag_drop-performance-considerations">Performance Considerations</h3>
<ul>
<li>Large files may take time to initiate transfer</li>
<li>Multiple large files in ZIP may take longer to create</li>
<li>Network drives may affect performance</li>
<li>System resources impact drag responsiveness</li>
</ul>
<h2 id="drag_drop-advanced-drag-features">Advanced Drag Features</h2>
<h3 id="drag_drop-drag-preview">Drag Preview</h3>
<ul>
<li>Live preview of dragged assets</li>
<li>Shows thumbnail of the primary dragged item</li>
<li>Displays count for multiple selections</li>
<li>Updates in real-time during drag</li>
</ul>
<h3 id="drag_drop-drag-validation">Drag Validation</h3>
<ul>
<li>Validates target compatibility before allowing drop</li>
<li>Provides visual feedback for valid/invalid targets</li>
<li>Prevents invalid operations</li>
<li>Shows appropriate cursors for different states</li>
</ul>
<h3 id="drag_drop-drag-cancellation">Drag Cancellation</h3>
<ul>
<li>Cancel drag by releasing over invalid target</li>
<li>Escape key cancels active drag operation</li>
<li>Clicking elsewhere cancels drag</li>
<li>Drag automatically cancels if window loses focus</li>
</ul>
<h2 id="drag_drop-integration-with-other-features">Integration with Other Features</h2>
<h3 id="drag_drop-selection-integration">Selection Integration</h3>
<ul>
<li>Drag operations work with current selection</li>
<li>Multiple selections enable batch operations</li>
<li>Selection filters apply to drag operations</li>
<li>Search results can be dragged directly</li>
</ul>
<h3 id="drag_drop-collection-integration">Collection Integration</h3>
<ul>
<li>Assets from collections can be dragged normally</li>
<li>Entire collections can be exported via drag</li>
<li>Collection organization preserved in transfers</li>
<li>Cross-collection drags work seamlessly</li>
</ul>
<h3 id="drag_drop-ratingtag-integration">Rating/Tag Integration</h3>
<ul>
<li>Rated and tagged assets maintain metadata during drag</li>
<li>Tags and ratings preserved in file transfers (when supported)</li>
<li>Filtered views can be dragged directly</li>
<li>Quality indicators visible during drag</li>
</ul>
<h2 id="drag_drop-troubleshooting">Troubleshooting</h2>
<h3 id="drag_drop-common-drag-issues">Common Drag Issues</h3>
<h4 id="drag_drop-drag-not-initiating">Drag Not Initiating</h4>
<ul>
<li>Ensure you're clicking and holding long enough</li>
<li>Check that the asset is properly selected</li>
<li>Verify no JavaScript errors in console</li>
<li>Try refreshing the Assets Manager</li>
</ul>
<h4 id="drag_drop-drag-target-not-recognized">Drag Target Not Recognized</h4>
<ul>
<li>Ensure target application accepts file drops</li>
<li>Check if target area is actually droppable</li>
<li>For ComfyUI node drops, use loader/input nodes with file-path widgets</li>
<li>Preview/output nodes are not file-path drop targets</li>
<li>Try dragging to a different application</li>
</ul>
<h4 id="drag_drop-zip-creation-failure">ZIP Creation Failure</h4>
<ul>
<li>Check available disk space</li>
<li>Verify write permissions to temporary directory</li>
<li>Ensure no antivirus interference</li>
<li>Try dragging fewer files at once</li>
</ul>
<h4 id="drag_drop-file-transfer-problems">File Transfer Problems</h4>
<ul>
<li>Large files may timeout during transfer</li>
<li>Network drives may cause delays</li>
<li>Antivirus software may interfere</li>
<li>Check file permissions at destination</li>
</ul>
<h3 id="drag_drop-browser-specific-issues">Browser-Specific Issues</h3>
<h4 id="drag_drop-chromechromium">Chrome/Chromium</h4>
<ul>
<li>May require specific security settings for file downloads</li>
<li>Extensions might interfere with drag operations</li>
<li>Incognito mode may behave differently</li>
</ul>
<h4 id="drag_drop-firefox">Firefox</h4>
<ul>
<li>Security settings may restrict file operations</li>
<li>Enhanced Tracking Protection might interfere</li>
<li>Check privacy settings for file handling</li>
</ul>
<h4 id="drag_drop-safari">Safari</h4>
<ul>
<li>May require additional permissions for file access</li>
<li>Security restrictions could limit functionality</li>
<li>Check website permissions for file handling</li>
</ul>
<h3 id="drag_drop-operating-system-issues">Operating System Issues</h3>
<h4 id="drag_drop-windows">Windows</h4>
<ul>
<li>UAC (User Account Control) may interfere</li>
<li>Antivirus software may block operations</li>
<li>File permissions may restrict transfers</li>
</ul>
<h4 id="drag_drop-macos">macOS</h4>
<ul>
<li>Gatekeeper may restrict file operations</li>
<li>Privacy settings may limit access</li>
<li>Sandboxing could affect functionality</li>
</ul>
<h4 id="drag_drop-linux">Linux</h4>
<ul>
<li>File permissions are strictly enforced</li>
<li>Desktop environment may affect drag behavior</li>
<li>Security modules might restrict operations</li>
</ul>
<h2 id="drag_drop-security-considerations">Security Considerations</h2>
<h3 id="drag_drop-file-access-security">File Access Security</h3>
<ul>
<li>Drag operations respect file system permissions</li>
<li>No files can be accessed that the user can't read</li>
<li>Temporary files are created with restricted permissions</li>
<li>ZIP files are cleaned up automatically</li>
</ul>
<h3 id="drag_drop-cross-origin-security">Cross-Origin Security</h3>
<ul>
<li>Drag operations confined to same origin</li>
<li>No cross-site data leakage possible</li>
<li>File system access limited to allowed directories</li>
<li>Path validation prevents directory traversal</li>
</ul>
<h3 id="drag_drop-malicious-file-protection">Malicious File Protection</h3>
<ul>
<li>No automatic execution of dragged files</li>
<li>Files transferred in their original form</li>
<li>No modification during drag operations</li>
<li>Integrity preserved throughout transfer</li>
</ul>
<h2 id="drag_drop-performance-optimization">Performance Optimization</h2>
<h3 id="drag_drop-large-file-handling">Large File Handling</h3>
<ul>
<li>Stream large files instead of loading entirely</li>
<li>Progress indicators for long operations</li>
<li>Asynchronous operations to maintain interface responsiveness</li>
<li>Memory management for multiple large files</li>
</ul>
<h3 id="drag_drop-multiple-file-optimization">Multiple File Optimization</h3>
<ul>
<li>Efficient ZIP creation algorithms</li>
<li>Parallel processing where possible</li>
<li>Memory buffering for optimal performance</li>
<li>Cleanup operations scheduled appropriately</li>
</ul>
<h3 id="drag_drop-interface-responsiveness">Interface Responsiveness</h3>
<ul>
<li>Drag operations don't block UI thread</li>
<li>Smooth animations during drag</li>
<li>Immediate feedback for user actions</li>
<li>Graceful degradation for slower systems</li>
</ul>
<h2 id="drag_drop-best-practices">Best Practices</h2>
<h3 id="drag_drop-efficient-drag-operations">Efficient Drag Operations</h3>
<ul>
<li>Use multiple selection for batch operations</li>
<li>Organize assets in collections for easier access</li>
<li>Use search and filters to find assets quickly</li>
<li>Verify target compatibility before initiating drag</li>
</ul>
<h3 id="drag_drop-file-organization">File Organization</h3>
<ul>
<li>Maintain consistent naming conventions</li>
<li>Use collections to group related assets</li>
<li>Apply tags for easy retrieval</li>
<li>Regular cleanup of unused assets</li>
</ul>
<h3 id="drag_drop-workflow-integration">Workflow Integration</h3>
<ul>
<li>Use drag operations to accelerate workflow creation</li>
<li>Stage frequently used assets in collections</li>
<li>Leverage drag to canvas for rapid prototyping</li>
<li>Combine with other Assets Manager features</li>
</ul>
<h3 id="drag_drop-performance-tips">Performance Tips</h3>
<ul>
<li>Close unnecessary browser tabs during large operations</li>
<li>Ensure sufficient system resources</li>
<li>Use wired connections for network drives</li>
<li>Regular maintenance of index database</li>
</ul>
<h2 id="drag_drop-advanced-usage">Advanced Usage</h2>
<h3 id="drag_drop-scripted-operations">Scripted Operations</h3>
<ul>
<li>Combine drag operations with automation scripts</li>
<li>Use collections for automated workflows</li>
<li>Integrate with external tools via file operations</li>
<li>Batch processing using drag interfaces</li>
</ul>
<h3 id="drag_drop-power-user-techniques">Power User Techniques</h3>
<ul>
<li>Master keyboard shortcuts with drag operations</li>
<li>Use multiple monitors for efficient transfers</li>
<li>Leverage search and filters for precise selection</li>
<li>Combine collections with drag for complex operations</li>
</ul>
<hr />
<p><em>Drag &amp; Drop Guide Version: 1.0</em> <em>Last Updated: April 5, 2026</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/DRAG_DROP.md</code>
</div>
</div>
</section>
<section class="panel section" id="floating_viewer_workflow_sidebar">
<div class="panel-h"><h2>FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="floating_viewer_workflow_sidebar-floating-viewer-workflow-sidebar-run-implementation">Floating Viewer — Workflow Sidebar &amp; Run Implementation</h1>
<blockquote><strong>Purpose</strong>: The Floating Viewer (MFV) includes a lightweight sidebar panel that allows</blockquote>
<blockquote>modifying widgets of selected nodes on the canvas, <strong>plus a Run button</strong>,</blockquote>
<blockquote>without duplicating or competing with ComfyUI's native "Workflow Overview" panel.</blockquote>
<hr />
<h2 id="floating_viewer_workflow_sidebar-1-design-principles-delegate-dont-duplicate">1. Design Principles — "Delegate, Don't Duplicate"</h2>
<div class="table-wrap"><table>
<thead><tr><th>Rule</th><th>Detail</th></tr></thead>
<tbody>
<tr><td><strong>Read-only data access</strong></td><td>Reads <code>app.graph</code>, <code>app.canvas.selected_nodes</code> — no parallel model invented.</td></tr>
<tr><td><strong>Write via native widget</strong></td><td>Writes via <code>widget.value = x ; widget.callback?.(x)</code> — ComfyUI engine handles propagation.</td></tr>
<tr><td><strong>Execution via endpoint</strong></td><td>Run calls <code>POST /prompt</code> with payload from <code>app.graphToPrompt()</code> — queue is not reimplemented.</td></tr>
<tr><td><strong>No own persistence</strong></td><td>No store, no cache — each opening reads live graph state.</td></tr>
</tbody></table></div>
<hr />
<h2 id="floating_viewer_workflow_sidebar-2-module-architecture">2. Module Architecture</h2>
<pre><code>ui/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)</code></pre>
<h3 id="floating_viewer_workflow_sidebar-21-dependencies">2.1 Dependencies</h3>
<pre><code>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)</code></pre>
<p>None of these files import from <code>comfyui-frontend</code> directly. Everything goes through the existing bridge: <code>ui/app/comfyApiBridge.js</code>.</p>
<hr />
<h2 id="floating_viewer_workflow_sidebar-3-module-by-module">3. Module by Module</h2>
<hr />
<h3 id="floating_viewer_workflow_sidebar-31-workflowsidebarjs-sidebar-panel">3.1 <code>WorkflowSidebar.js</code> — Sidebar Panel</h3>
<p><strong>Role</strong>: Sliding container (slide-in) that opens on the right side of the Floating Viewer.</p>
<pre><code>┌─────────────────────────────────────────────────────┐
│ Majoor Floating Viewer [⚙] [▶ 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...] │ │
│ │ └────────────────────────────┘ │
└────────────────────┴────────────────────────────────┘</code></pre>
<h4 id="floating_viewer_workflow_sidebar-public-api">Public API</h4>
<pre><code class="language-js">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()
}</code></pre>
<h4 id="floating_viewer_workflow_sidebar-how-to-read-selected-nodes">How to read selected nodes</h4>
<p>The pattern already exists in <code>NodeStreamController.js</code>:</p>
<pre><code class="language-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 [];
}</code></pre>
<h4 id="floating_viewer_workflow_sidebar-how-to-listen-to-selection-changes">How to listen to selection changes</h4>
<p>The pattern exists in <code>LiveStreamTracker.js</code>:</p>
<pre><code class="language-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
};</code></pre>
<blockquote><strong>Important</strong>: These hooks must be properly attached/detached when opening/closing</blockquote>
<blockquote>the sidebar to avoid leaks. We use the same chained pattern as <code>LiveStreamTracker</code>.</blockquote>
<hr />
<h3 id="floating_viewer_workflow_sidebar-32-nodewidgetrendererjs-node-rendering">3.2 <code>NodeWidgetRenderer.js</code> — Node Rendering</h3>
<p><strong>Role</strong>: For a given <code>LGraphNode</code>, iterates over <code>node.widgets</code> and generates a corresponding HTML form.</p>
<h4 id="floating_viewer_workflow_sidebar-information-available-on-a-comfyui-widget">Information available on a ComfyUI widget</h4>
<pre><code class="language-js">node.widgets.forEach(widget =&gt; {
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
});</code></pre>
<h4 id="floating_viewer_workflow_sidebar-generated-dom-structure">Generated DOM structure</h4>
<pre><code class="language-html">&lt;section class="mjr-ws-node" data-node-id="3"&gt;
&lt;div class="mjr-ws-node-header"&gt;
&lt;span class="mjr-ws-node-title"&gt;KSampler&lt;/span&gt;
&lt;button class="mjr-icon-btn mjr-ws-locate" title="Locate on canvas"&gt;
&lt;i class="pi pi-map-marker"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="mjr-ws-node-body"&gt;
&lt;!-- widgets rendered by widgetAdapters --&gt;
&lt;/div&gt;
&lt;/section&gt;</code></pre>
<h4 id="floating_viewer_workflow_sidebar-locating-a-node-on-the-canvas">Locating a node on the canvas</h4>
<pre><code class="language-js">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 &amp;&amp; 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);
}
}</code></pre>
<hr />
<h3 id="floating_viewer_workflow_sidebar-33-widgetadaptersjs-widget-html-input-conversion">3.3 <code>widgetAdapters.js</code> — Widget → HTML Input Conversion</h3>
<p>File of pure functions, no state. Each adapter returns an <code>HTMLElement</code>.</p>
<div class="table-wrap"><table>
<thead><tr><th><code>widget.type</code></th><th>HTML Input</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>"number"</code></td><td><code>&amp;lt;input type="number" min max step&amp;gt;</code></td><td>Uses <code>widget.options.{min,max,step}</code></td></tr>
<tr><td><code>"combo"</code></td><td><code>&amp;lt;select&amp;gt;</code> with <code>&amp;lt;option&amp;gt;</code></td><td>Uses <code>widget.options.values</code></td></tr>
<tr><td><code>"text"</code></td><td><code>&amp;lt;textarea&amp;gt;</code></td><td>Auto-resize</td></tr>
<tr><td><code>"toggle"</code></td><td><code>&amp;lt;input type="checkbox"&amp;gt;</code></td><td></td></tr>
<tr><td><code>"IMAGEUPLOAD"</code></td><td><em>(ignored)</em></td><td>Too complex, not handled</td></tr>
<tr><td>other / unknown</td><td><code>&amp;lt;input type="text" readonly&amp;gt;</code></td><td>Display only</td></tr>
</tbody></table></div>
<h4 id="floating_viewer_workflow_sidebar-writing-to-a-comfyui-widget">Writing to a ComfyUI widget</h4>
<pre><code class="language-js">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;
}</code></pre>
<blockquote>This pattern is identical to the one already used in <code>DragDrop.js</code> for dropping</blockquote>
<blockquote>media on nodes — nothing is reinvented.</blockquote>
<hr />
<h3 id="floating_viewer_workflow_sidebar-34-sidebarrunbuttonjs-run-button">3.4 <code>sidebarRunButton.js</code> — Run Button</h3>
<p><strong>Role</strong>: Single button that triggers <code>POST /prompt</code> using ComfyUI API.</p>
<h4 id="floating_viewer_workflow_sidebar-option-a-via-app-object-recommended">Option A — Via <code>app</code> object (recommended)</h4>
<pre><code class="language-js">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 &amp;&amp; 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() };
}</code></pre>
<h4 id="floating_viewer_workflow_sidebar-rendering">Rendering</h4>
<pre><code class="language-html">&lt;!-- In toolbar, next to ⚙ settings button --&gt;
&lt;button class="mjr-icon-btn mjr-mfv-run-btn" title="Queue Prompt (Run)"&gt;
&lt;i class="pi pi-play"&gt;&lt;/i&gt;
&lt;/button&gt;</code></pre>
<p>Visual state:</p>
<ul>
<li>Idle → green <code>pi-play</code> icon</li>
<li>Running → <code>pi-spin pi-spinner</code> icon + disabled</li>
<li>Error → red flash 1s then back to idle</li>
</ul>
<hr />
<h2 id="floating_viewer_workflow_sidebar-4-integration-in-floatingviewer">4. Integration in FloatingViewer</h2>
<h3 id="floating_viewer_workflow_sidebar-41-modification-of-_buildtoolbar">4.1 Modification of <code>_buildToolbar()</code></h3>
<p>Add <strong>2 buttons</strong> to existing toolbar:</p>
<pre><code class="language-js">// 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", () =&gt; this._sidebar?.toggle());
bar.appendChild(settingsBtn);
// --- Run Button ---
const runBtn = createRunButton(); // from sidebarRunButton.js
bar.appendChild(runBtn);</code></pre>
<h3 id="floating_viewer_workflow_sidebar-42-modification-of-render">4.2 Modification of <code>render()</code></h3>
<pre><code class="language-js">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: () =&gt; this._updateSettingsBtnState(false),
});
el.appendChild(this._sidebar.el);
return el;
}</code></pre>
<h3 id="floating_viewer_workflow_sidebar-43-selection-hook-in-floatingviewermanagerjs">4.3 Selection Hook in <code>floatingViewerManager.js</code></h3>
<pre><code class="language-js">// In open():
_bindNodeSelectionListener();
// In close():
_unbindNodeSelectionListener();</code></pre>
<hr />
<h2 id="floating_viewer_workflow_sidebar-5-css-added-classes">5. CSS — Added Classes</h2>
<pre><code class="language-css">/* ── 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); }</code></pre>
<hr />
<h2 id="floating_viewer_workflow_sidebar-6-events-and-data-flow">6. Events and Data Flow</h2>
<pre><code>┌─────────────┐ 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 │
└─────────────┘ └───────────────────┘</code></pre>
<hr />
<h2 id="floating_viewer_workflow_sidebar-7-what-we-do-not-do">7. What We Do NOT Do</h2>
<div class="table-wrap"><table>
<thead><tr><th>Prohibited</th><th>Reason</th></tr></thead>
<tbody>
<tr><td>Recreate a full node editor</td><td>We're not an IDE — just a quick adjustment panel</td></tr>
<tr><td>Handle node add/delete</td><td>ComfyUI canvas manages that</td></tr>
<tr><td>Duplicate Workflow Overview</td><td>Our sidebar is a <strong>contextual shortcut</strong> (selected nodes only)</td></tr>
<tr><td>Intercept execution queue</td><td>We POST and that's it — no own progress tracking</td></tr>
<tr><td>Store widget state</td><td>We read/write live graph — no local copy</td></tr>
<tr><td>Add ComfyUI frontend dependencies</td><td>Everything goes through <code>comfyApiBridge.js</code></td></tr>
</tbody></table></div>
<hr />
<h2 id="floating_viewer_workflow_sidebar-8-implementation-checklist">8. Implementation Checklist</h2>
<ul>
<li>[x] Create <code>ui/features/viewer/workflowSidebar/widgetAdapters.js</code></li>
<li>[x] Create <code>ui/features/viewer/workflowSidebar/NodeWidgetRenderer.js</code></li>
<li>[x] Create <code>ui/features/viewer/workflowSidebar/WorkflowSidebar.js</code></li>
<li>[x] Create <code>ui/features/viewer/workflowSidebar/sidebarRunButton.js</code></li>
<li>[x] Modify <code>FloatingViewer.js</code> — add ⚙ + ▶ buttons in <code>_buildToolbar()</code></li>
<li>[x] Modify <code>FloatingViewer.js</code> — instantiate <code>WorkflowSidebar</code> in <code>render()</code></li>
<li>[x] Modify <code>floatingViewerManager.js</code> — <code>onNodeSelected</code> / <code>onSelectionChange</code> hooks</li>
<li>[x] Add CSS classes in <code>theme-comfy.css</code></li>
<li>[x] Tests: <code>workflowSidebar.vitest.mjs</code> (DOM/adapters), <code>sidebarRunButton.vitest.mjs</code> (mock fetch)</li>
</ul>
<p><strong>Status</strong>: ✅ <strong>Complete</strong> — All items implemented and tested.</p>
<hr />
<h2 id="floating_viewer_workflow_sidebar-9-final-mfv-toolbar-visual-summary">9. Final MFV Toolbar Visual Summary</h2>
<pre><code>[ Mode ][ Pin ▾]│[ Live ][ Preview ][ NodeStream ]│[ GenInfo ][ PopOut ][ Download ]│[ ⚙ Settings ][ ▶ Run ]│[ ✕ ]
└── NEW ──┘</code></pre>
<hr />
<h2 id="floating_viewer_workflow_sidebar-10-sidebar-position-setting">10. Sidebar Position Setting</h2>
<p>Users can customize sidebar placement via Settings:</p>
<ul>
<li><strong>Right</strong> (default) — sidebar slides in from right</li>
<li><strong>Left</strong> — sidebar slides in from left</li>
<li><strong>Bottom</strong> — sidebar slides up from bottom</li>
</ul>
<p>The setting applies immediately without reload and persists across sessions.</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</code>
</div>
</div>
</section>
<section class="panel section" id="frontend_imperative_design">
<div class="panel-h"><h2>FRONTEND_IMPERATIVE_DESIGN.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="frontend_imperative_design-frontend-imperative-vs-vue-ownership">Frontend: Imperative vs. Vue ownership</h1>
<p><strong>Last updated</strong>: April 10, 2026</p>
<h2 id="frontend_imperative_design-summary">Summary</h2>
<p>The frontend uses <strong>Vue 3 + Pinia</strong> for all major UI surfaces. Certain runtime modules remain imperative <strong>by design</strong> because they orchestrate cross-cutting behavior that predates or spans multiple Vue component trees.</p>
<h2 id="frontend_imperative_design-imperative-by-design">Imperative by design</h2>
<p>These modules are intentionally kept outside Vue's reactivity model:</p>
<div class="table-wrap"><table>
<thead><tr><th>Module</th><th>Reason</th></tr></thead>
<tbody>
<tr><td><code>ui/app/bootstrap.js</code></td><td>App init sequence, ComfyUI lifecycle hooks</td></tr>
<tr><td><code>ui/app/comfyApiBridge.js</code></td><td>Bridge to ComfyUI's API (external dependency, not ours)</td></tr>
<tr><td><code>ui/app/events.js</code></td><td>Central event bus for cross-feature communication</td></tr>
<tr><td><code>ui/app/metrics.js</code></td><td>Performance telemetry (no UI surface)</td></tr>
<tr><td><code>ui/features/panel/panelRuntime.js</code></td><td>Panel lifecycle orchestration: init, teardown, events</td></tr>
<tr><td><code>ui/features/viewer/*</code></td><td>Viewer runtime: canvas, zoom, playback — too stateful and performance-sensitive for Vue reactivity</td></tr>
<tr><td><code>ui/features/dnd/*</code></td><td>Drag-and-drop runtime: low-level DOM events, interop with ComfyUI DnD</td></tr>
<tr><td><code>ui/features/runtime/*</code></td><td>ComfyUI runtime integration (generation flow, progress, queue)</td></tr>
</tbody></table></div>
<h2 id="frontend_imperative_design-provisionally-imperative-may-migrate-later">Provisionally imperative (may migrate later)</h2>
<div class="table-wrap"><table>
<thead><tr><th>Module</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>ui/stores/panelStateBridge.js</code></td><td>Bridge between Vue panel store and legacy non-Vue consumers. Remove when all consumers are Vue.</td></tr>
<tr><td><code>ui/app/dialogs.js</code>, <code>ui/app/toast.js</code></td><td>UI utilities currently used by both Vue and imperative code. Can move to Vue composables once all callers migrate.</td></tr>
</tbody></table></div>
<h2 id="frontend_imperative_design-fully-vue-owned">Fully Vue-owned</h2>
<p>All surfaces under <code>ui/vue/components/</code> and <code>ui/vue/composables/</code> are fully Vue-owned:</p>
<ul>
<li>Grid, sidebar, feed, settings panels, context menus</li>
<li>All Pinia stores under <code>ui/stores/</code></li>
</ul>
<h2 id="frontend_imperative_design-decision-criteria">Decision criteria</h2>
<p>A module should stay imperative when:</p>
<ol>
<li>It manages a lifecycle spanning multiple Vue app mounts/unmounts</li>
<li>It bridges to external APIs not under our control (ComfyUI)</li>
<li>It requires low-level DOM/canvas control where Vue overhead is measurable</li>
<li>Its mutation patterns don't map cleanly to reactive state (e.g. streaming events, Web Workers)</li>
</ol></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/FRONTEND_IMPERATIVE_DESIGN.md</code>
</div>
</div>
</section>
<section class="panel section" id="frontend_lifecycle_conventions">
<div class="panel-h"><h2>FRONTEND_LIFECYCLE_CONVENTIONS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="frontend_lifecycle_conventions-frontend-lifecycle-conventions">Frontend Lifecycle Conventions</h1>
<p><strong>Last updated</strong>: April 10, 2026</p>
<h2 id="frontend_lifecycle_conventions-vue-component-lifecycle">Vue component lifecycle</h2>
<ul>
<li>Vue apps are created via <code>ui/vue/createVueApp.js</code> with Pinia store injection.</li>
<li>Components use <code>onMounted</code> / <code>onUnmounted</code> for DOM setup/teardown.</li>
<li>Event listeners added in <code>onMounted</code> <strong>must</strong> be removed in <code>onUnmounted</code>.</li>
<li>Pinia stores are the single source of truth for UI state.</li>
</ul>
<h2 id="frontend_lifecycle_conventions-imperative-runtime-lifecycle">Imperative runtime lifecycle</h2>
<ul>
<li><code>panelRuntime.js</code> manages the top-level panel init/destroy cycle.</li>
<li>Feature modules (viewer, DnD, grid) expose <code>init()</code> / <code>destroy()</code> or <code>start()</code> / <code>stop()</code>.</li>
<li>The bootstrap sequence is:</li>
<ol>
<li><code>bootstrap.js</code> initializes the app context</li>
<li><code>panelRuntime.js</code> sets up the panel shell</li>
<li>Vue apps mount into panel slots</li>
<li>Feature runtimes activate via events or direct calls</li>
</ol>
</ul>
<h2 id="frontend_lifecycle_conventions-teardown-rules">Teardown rules</h2>
<ol>
<li>Every <code>addEventListener</code> must have a matching <code>removeEventListener</code> on destroy.</li>
<li>Every <code>setInterval</code> / <code>setTimeout</code> must be cleared on destroy.</li>
<li>Pinia store subscriptions are auto-cleaned when the Vue app unmounts.</li>
<li>Custom event buses (<code>events.js</code>) listeners must be unsubscribed explicitly.</li>
</ol>
<h2 id="frontend_lifecycle_conventions-bridge-conventions">Bridge conventions</h2>
<ul>
<li><code>comfyApiBridge.js</code>: wraps ComfyUI API calls. Never call ComfyUI API directly from Vue components.</li>
<li><code>panelStateBridge.js</code>: syncs Pinia panel store with legacy imperative consumers. Will be removed when all consumers migrate to Vue.</li>
<li>New bridges should be avoided. Prefer Pinia stores or composables for new features.</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/FRONTEND_LIFECYCLE_CONVENTIONS.md</code>
</div>
</div>
</section>
<section class="panel section" id="graph_map">
<div class="panel-h"><h2>GRAPH_MAP.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="graph_map-graph-map-guide">Graph Map Guide</h1>
<p><strong>Last Updated</strong>: May 10, 2026</p>
<h2 id="graph_map-overview">Overview</h2>
<p>Graph Map is the workflow-aware navigation view inside the Majoor Floating Viewer.</p>
<p>It is designed for one fast job: keep the saved workflow readable while you inspect the asset that came out of it.</p>
<p>Graph Map combines three things in one place:</p>
<ul>
<li>a large workflow map with readable node labels</li>
<li>a selected-node detail panel with copy/import actions</li>
<li>a small asset preview that keeps the current media visible while the panel refreshes</li>
</ul>
<p><a href="docs/images/graph-map-overview.png" target="_blank" rel="noopener noreferrer">[Graph Map overview]</a></p>
<h2 id="graph_map-why-it-exists">Why It Exists</h2>
<p>When a workflow gets large, the usual minimap is useful for shape but not for understanding.</p>
<p>Graph Map adds the missing context:</p>
<ul>
<li>real node labels instead of anonymous boxes</li>
<li>subgraph names that stay human-readable instead of raw UUID or hash identifiers</li>
<li>a direct bridge between the saved workflow and the asset currently shown in the viewer</li>
</ul>
<p>This is especially useful for video workflows, larger prompt-routing graphs, and pipelines that rely on nested subgraphs.</p>
<h2 id="graph_map-how-to-open-it">How To Open It</h2>
<ol>
<li>Open an asset in the Majoor Floating Viewer.</li>
<li>Switch the viewer to <strong>Graph Map</strong> mode.</li>
<li>If you also open the <strong>Node Parameters</strong> sidebar, use the <strong>Graph Map</strong> tab to keep the small preview and node details visible together.</li>
</ol>
<p>Graph Map only appears when the selected asset contains readable workflow data.</p>
<h2 id="graph_map-what-you-see">What You See</h2>
<h3 id="graph_map-1-workflow-overview">1. Workflow overview</h3>
<p>The large map shows the saved workflow structure with node labels, links, and the current selection highlight.</p>
<ul>
<li>loader, sampler, and output nodes remain easy to spot</li>
<li>subgraphs are labeled with their real names when that metadata is available</li>
<li>the selected node is outlined so you can keep your position while panning or zooming</li>
</ul>
<h3 id="graph_map-2-selected-node-detail-panel">2. Selected node detail panel</h3>
<p>When you click a node in Graph Map, the detail panel shows the node title, its type, and the most useful simple parameters.</p>
<p>Available actions include:</p>
<ul>
<li><strong>Copy node</strong></li>
<li><strong>Import node</strong></li>
<li><strong>Import workflow</strong></li>
<li><strong>Transfer params to selected canvas node</strong></li>
</ul>
<p>You can also click individual parameter rows to copy a single value.</p>
<p><a href="docs/images/graph-map-node-detail.png" target="_blank" rel="noopener noreferrer">[Graph Map node detail]</a></p>
<h3 id="graph_map-3-asset-preview">3. Asset preview</h3>
<p>The small preview keeps the active asset visible while you browse the workflow.</p>
<p>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.</p>
<h2 id="graph_map-navigation-and-interaction">Navigation And Interaction</h2>
<h3 id="graph_map-canvas-controls">Canvas controls</h3>
<ul>
<li><strong>Click a node</strong> to select it</li>
<li><strong>Mouse wheel</strong> to zoom in or out</li>
<li><strong>Click-drag</strong> to pan around the map</li>
</ul>
<h3 id="graph_map-node-detail-workflow">Node detail workflow</h3>
<ul>
<li>select the node that generated or refined the output you care about</li>
<li>inspect the visible parameters</li>
<li>copy a single value or the full node JSON</li>
<li>import the node or the whole workflow back into the current canvas when needed</li>
</ul>
<h2 id="graph_map-subgraph-labels">Subgraph Labels</h2>
<p>Graph Map now prefers readable subgraph names over opaque identifiers.</p>
<p>In practice that means:</p>
<ul>
<li>named subgraphs are shown with their visible workflow name</li>
<li>raw UUID or long hash node types are no longer the main label when a better name exists</li>
<li>the same readable naming is reused in the map and the node detail panel</li>
</ul>
<p>This matters most in larger reusable workflow blocks such as detailers, post-process passes, regional prompt groups, or custom routed subgraphs.</p>
<h2 id="graph_map-best-use-cases">Best Use Cases</h2>
<ul>
<li>understand which block produced the current result</li>
<li>jump quickly between sampler, prompt, and save/output nodes</li>
<li>inspect a subgraph without opening the full ComfyUI canvas layout</li>
<li>compare outputs while keeping the originating node parameters close by</li>
<li>reuse values from a saved asset back into the current canvas node</li>
</ul>
<h2 id="graph_map-limitations">Limitations</h2>
<ul>
<li>Graph Map depends on workflow metadata being present in the asset.</li>
<li>If an asset has no embedded workflow, the panel cannot reconstruct the graph.</li>
<li>The detail panel intentionally focuses on simple, copyable values; not every complex widget or linked input is shown as a plain field.</li>
</ul>
<h2 id="graph_map-related-docs">Related Docs</h2>
<ul>
<li><a href="#viewer_feature_tutorial">VIEWER_FEATURE_TUTORIAL.md</a></li>
<li><a href="#floating_viewer_workflow_sidebar">FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</a></li>
<li><a href="#hotkeys_shortcuts">HOTKEYS_SHORTCUTS.md</a></li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/GRAPH_MAP.md</code>
</div>
</div>
</section>
<section class="panel section" id="hotkeys_shortcuts">
<div class="panel-h"><h2>HOTKEYS_SHORTCUTS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="hotkeys_shortcuts-majoor-assets-manager-hotkeys-keyboard-shortcuts-guide">Majoor Assets Manager - Hotkeys &amp; Keyboard Shortcuts Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: May 18, 2026</p>
<h2 id="hotkeys_shortcuts-overview">Overview</h2>
<p>This guide covers all active keyboard shortcuts for the Majoor Assets Manager, including the new <strong>Majoor Floating Viewer (MFV)</strong>.</p>
<hr />
<h2 id="hotkeys_shortcuts-table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#hotkeys_shortcuts-global-panel-hotkeys">Global / Panel Hotkeys</a></li>
<li><a href="#hotkeys_shortcuts-grid-view-hotkeys">Grid View Hotkeys</a></li>
<li><a href="#hotkeys_shortcuts-standard-viewer-hotkeys">Standard Viewer Hotkeys</a></li>
<li><a href="#hotkeys_shortcuts-majoor-floating-viewer-mfv-hotkeys">Majoor Floating Viewer (MFV) Hotkeys</a></li>
<li><a href="#hotkeys_shortcuts-video-playback-hotkeys">Video Playback Hotkeys</a></li>
<li><a href="#hotkeys_shortcuts-drag-drop-shortcuts">Drag &amp;amp; Drop Shortcuts</a></li>
<li><a href="#hotkeys_shortcuts-mouse-shortcuts">Mouse Shortcuts</a></li>
</ul>
<hr />
<h2 id="hotkeys_shortcuts-global-panel-hotkeys">Global / Panel Hotkeys</h2>
<p>These shortcuts work globally in the Assets Manager panel.</p>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th><th>Scope</th></tr></thead>
<tbody>
<tr><td><strong>Ctrl+S</strong> / <strong>Cmd+S</strong></td><td>Trigger Index Scan</td><td>Global (Panel Focused)</td></tr>
<tr><td><strong>Ctrl+F</strong> / <strong>Ctrl+K</strong></td><td>Focus Search Input</td><td>Global</td></tr>
<tr><td><strong>Ctrl+H</strong></td><td>Clear Search Input</td><td>Global</td></tr>
<tr><td><strong>D</strong></td><td>Toggle Sidebar (Details)</td><td>Grid / Panel</td></tr>
<tr><td><strong>V</strong></td><td>Toggle Floating Viewer</td><td>Grid / Panel</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-quick-filter-controls">Quick Filter Controls</h3>
<p>These controls are available as direct icon buttons in the panel summary area:</p>
<div class="table-wrap"><table>
<thead><tr><th>Control</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Image icon button</strong></td><td>Toggle Image-only quick filter</td></tr>
<tr><td><strong>Video icon button</strong></td><td>Toggle Video-only quick filter</td></tr>
<tr><td><strong>Audio icon button</strong></td><td>Toggle Audio-only quick filter</td></tr>
<tr><td><strong>3D icon button</strong></td><td>Toggle 3D-only quick filter</td></tr>
</tbody></table></div>
<hr />
<h2 id="hotkeys_shortcuts-grid-view-hotkeys">Grid View Hotkeys</h2>
<p>These shortcuts apply when the asset grid has focus.</p>
<h3 id="hotkeys_shortcuts-navigation-selection">Navigation &amp; Selection</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Arrow Keys</strong> (↑↓←→)</td><td>Navigate selection</td></tr>
<tr><td><strong>Enter</strong> / <strong>Space</strong></td><td>Open Viewer</td></tr>
<tr><td><strong>V</strong></td><td>Open selected asset in Floating Viewer</td></tr>
<tr><td><strong>Ctrl+A</strong> / <strong>Cmd+A</strong></td><td>Select All</td></tr>
<tr><td><strong>Ctrl+D</strong> / <strong>Cmd+D</strong></td><td>Deselect All</td></tr>
<tr><td><strong>Ctrl+Shift+C</strong> / <strong>Cmd+Shift+C</strong></td><td>Copy selected asset path</td></tr>
<tr><td><strong>Ctrl+Shift+E</strong> / <strong>Cmd+Shift+E</strong></td><td>Open selected asset in Explorer/Finder</td></tr>
<tr><td><strong>Ctrl+Click</strong> / <strong>Cmd+Click</strong></td><td>Toggle Selection</td></tr>
<tr><td><strong>Shift+Click</strong></td><td>Range Selection</td></tr>
<tr><td><strong>Home</strong></td><td>Go to first asset</td></tr>
<tr><td><strong>End</strong></td><td>Go to last asset</td></tr>
<tr><td><strong>Page Up</strong></td><td>Scroll up one page</td></tr>
<tr><td><strong>Page Down</strong></td><td>Scroll down one page</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-organization-rating">Organization &amp; Rating</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>0</strong></td><td>Reset Rating (0 stars)</td></tr>
<tr><td><strong>1</strong> - <strong>5</strong></td><td>Set Rating (1-5 stars)</td></tr>
<tr><td><strong>T</strong></td><td>Edit Tags</td></tr>
<tr><td><strong>B</strong></td><td>Add to Collection (Bookmark)</td></tr>
<tr><td><strong>Shift+B</strong></td><td>Remove from Collection</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-file-operations">File Operations</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>F2</strong></td><td>Rename File</td></tr>
<tr><td><strong>Delete</strong></td><td>Delete File (with confirmation)</td></tr>
<tr><td><strong>S</strong></td><td>Download File</td></tr>
<tr><td><strong>Ctrl+Shift+C</strong> / <strong>Cmd+Shift+C</strong></td><td>Copy File Path</td></tr>
<tr><td><strong>Ctrl+Shift+E</strong> / <strong>Cmd+Shift+E</strong></td><td>Open in Explorer/Finder</td></tr>
</tbody></table></div>
<hr />
<h2 id="hotkeys_shortcuts-standard-viewer-hotkeys">Standard Viewer Hotkeys</h2>
<p>These shortcuts apply when the <strong>Standard Viewer</strong> overlay is open (double-click on asset).</p>
<h3 id="hotkeys_shortcuts-general">General</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Esc</strong></td><td>Close Viewer</td></tr>
<tr><td><strong>F</strong></td><td>Toggle Fullscreen</td></tr>
<tr><td><strong>D</strong></td><td>Toggle Info Panel (Generation Data)</td></tr>
<tr><td><strong>Space</strong></td><td>Play/Pause Video</td></tr>
<tr><td><strong>S</strong></td><td>Download Original from context menu</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-navigation">Navigation</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><strong>Left Arrow</strong></td><td>Previous Asset</td><td>Default behavior</td></tr>
<tr><td><strong>Right Arrow</strong></td><td>Next Asset</td><td>Default behavior</td></tr>
<tr><td><strong>Left Arrow</strong></td><td>Step Frame (-1)</td><td><strong>Only when Video Player bar is focused</strong></td></tr>
<tr><td><strong>Right Arrow</strong></td><td>Step Frame (+1)</td><td><strong>Only when Video Player bar is focused</strong></td></tr>
<tr><td><strong>Mouse Wheel</strong></td><td>Zoom In/Out</td><td></td></tr>
<tr><td><strong>Click+Drag</strong></td><td>Pan Image</td><td>When zoomed in</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-tools-analysis">Tools &amp; Analysis</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><strong>I</strong></td><td>Set In Point (video) / Toggle Pixel Probe (image)</td><td>Context-sensitive</td></tr>
<tr><td><strong>O</strong></td><td>Set Out Point</td><td>Video only</td></tr>
<tr><td><strong>C</strong></td><td>Copy Probed Color (Hex)</td><td></td></tr>
<tr><td><strong>L</strong></td><td>Toggle Loupe</td><td></td></tr>
<tr><td><strong>Z</strong></td><td>Toggle Zebra (Exposure)</td><td></td></tr>
<tr><td><strong>G</strong></td><td>Cycle Grid Overlays</td><td></td></tr>
<tr><td><strong>Alt+1</strong></td><td>Toggle 1:1 Pixel View</td><td></td></tr>
<tr><td><strong>+</strong> / <strong>-</strong></td><td>Zoom In / Out</td><td></td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-enhancement-tools">Enhancement Tools</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>E</strong></td><td>Toggle Exposure (EV) Control</td></tr>
<tr><td><strong>M</strong></td><td>Toggle Gamma Correction</td></tr>
<tr><td><strong>1</strong> / <strong>2</strong> / <strong>3</strong></td><td>Isolate R/G/B Channel</td></tr>
<tr><td><strong>0</strong></td><td>Reset to RGB (all channels)</td></tr>
<tr><td><strong>A</strong></td><td>Toggle Alpha Channel View</td></tr>
<tr><td><strong>Y</strong></td><td>Toggle Luma (Y) View</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-analysis-overlays">Analysis Overlays</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Shift+Z</strong></td><td>Toggle False Color</td></tr>
<tr><td><strong>Shift+H</strong></td><td>Toggle Histogram</td></tr>
<tr><td><strong>Shift+W</strong></td><td>Toggle Waveform</td></tr>
<tr><td><strong>Shift+V</strong></td><td>Toggle Vectorscope</td></tr>
</tbody></table></div>
<hr />
<h2 id="hotkeys_shortcuts-majoor-floating-viewer-mfv-hotkeys">Majoor Floating Viewer (MFV) Hotkeys</h2>
<p>These shortcuts apply when the <strong>Majoor Floating Viewer</strong> panel is open.</p>
<h3 id="hotkeys_shortcuts-general-controls">General Controls</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Esc</strong></td><td>Close Floating Viewer</td></tr>
<tr><td><strong>V</strong></td><td>Open or close Floating Viewer</td></tr>
<tr><td><strong>C</strong></td><td>Cycle compare modes: A/B, Side-by-side, Off (Simple mode)</td></tr>
<tr><td><strong>K</strong></td><td>Toggle KSampler denoising preview on or off</td></tr>
<tr><td><strong>L</strong></td><td>Toggle Live Stream final-output following on or off</td></tr>
<tr><td><strong>N</strong></td><td>Toggle selected-node Node Stream on or off</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-panel-and-media-interaction">Panel And Media Interaction</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Mouse Wheel</strong></td><td>Zoom In/Out</td></tr>
<tr><td><strong>Click+Drag</strong></td><td>Pan Image (when zoomed)</td></tr>
<tr><td><strong>Drag Header</strong></td><td>Move the floating panel</td></tr>
<tr><td><strong>Drag Edges</strong></td><td>Resize the floating panel</td></tr>
</tbody></table></div>
<p>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.</p>
<h3 id="hotkeys_shortcuts-video-controls-mfv">Video Controls (MFV)</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Space</strong></td><td>Play/Pause the focused MFV player</td></tr>
<tr><td><strong>Left Arrow</strong></td><td>Step backward one frame</td></tr>
<tr><td><strong>Right Arrow</strong></td><td>Step forward one frame</td></tr>
</tbody></table></div>
<blockquote>Click once on the inline player surface to give it focus before using the playback shortcuts above.</blockquote>
<hr />
<h2 id="hotkeys_shortcuts-video-playback-hotkeys">Video Playback Hotkeys</h2>
<p>These shortcuts apply when playing video content in the viewer.</p>
<h3 id="hotkeys_shortcuts-playback-control">Playback Control</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Space</strong></td><td>Play/Pause</td></tr>
<tr><td><strong>Click on video</strong></td><td>Play/Pause</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-frame-navigation">Frame Navigation</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><strong>Left Arrow</strong></td><td>Previous Frame</td><td>When player bar is focused</td></tr>
<tr><td><strong>Right Arrow</strong></td><td>Next Frame</td><td>When player bar is focused</td></tr>
<tr><td><strong>Home</strong></td><td>Go to In Point</td><td></td></tr>
<tr><td><strong>End</strong></td><td>Go to Out Point</td><td></td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-inout-points-edit-marks">In/Out Points (Edit Marks)</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>I</strong></td><td>Set In Point at current frame</td></tr>
<tr><td><strong>O</strong></td><td>Set Out Point at current frame</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-speed-control">Speed Control</h3>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>[</strong></td><td>Decrease Playback Speed (-0.25x)</td></tr>
<tr><td><strong>]</strong></td><td>Increase Playback Speed (+0.25x)</td></tr>
<tr><td><strong>\\</strong></td><td>Reset Speed to Normal (1x)</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-audio">Audio</h3>
<p>Video audio is automatically unmuted after first user interaction (play, seek, or frame step).</p>
<hr />
<h2 id="hotkeys_shortcuts-drag-drop-shortcuts">Drag &amp; Drop Shortcuts</h2>
<div class="table-wrap"><table>
<thead><tr><th>Gesture</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>S+Drag to OS</strong></td><td>Export without ComfyUI workflow/generation metadata</td></tr>
<tr><td><strong>L+Drop on canvas</strong></td><td>Load selected asset(s) as media loader node(s) instead of importing embedded workflow</td></tr>
</tbody></table></div>
<hr />
<h2 id="hotkeys_shortcuts-mouse-shortcuts">Mouse Shortcuts</h2>
<h3 id="hotkeys_shortcuts-grid-view">Grid View</h3>
<div class="table-wrap"><table>
<thead><tr><th>Action</th><th>Result</th></tr></thead>
<tbody>
<tr><td><strong>Click</strong></td><td>Select asset</td></tr>
<tr><td><strong>Double-Click</strong></td><td>Open in Viewer</td></tr>
<tr><td><strong>Ctrl+Click</strong> / <strong>Cmd+Click</strong></td><td>Toggle selection</td></tr>
<tr><td><strong>Shift+Click</strong></td><td>Range selection</td></tr>
<tr><td><strong>Right-Click</strong></td><td>Open context menu</td></tr>
<tr><td><strong>Drag</strong></td><td>Initiate drag &amp; drop</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-viewer">Viewer</h3>
<div class="table-wrap"><table>
<thead><tr><th>Action</th><th>Result</th></tr></thead>
<tbody>
<tr><td><strong>Mouse Wheel</strong></td><td>Zoom in/out</td></tr>
<tr><td><strong>Click+Drag</strong> (zoomed)</td><td>Pan image</td></tr>
<tr><td><strong>Double-Click</strong></td><td>Toggle 1:1 zoom</td></tr>
<tr><td><strong>Right-Click</strong></td><td>Open context menu</td></tr>
<tr><td><strong>Middle-Click</strong></td><td>Reset zoom and pan</td></tr>
</tbody></table></div>
<h3 id="hotkeys_shortcuts-floating-viewer">Floating Viewer</h3>
<div class="table-wrap"><table>
<thead><tr><th>Action</th><th>Result</th></tr></thead>
<tbody>
<tr><td><strong>Drag Header</strong></td><td>Move panel</td></tr>
<tr><td><strong>Drag Edges</strong></td><td>Resize panel</td></tr>
<tr><td><strong>Click Outside</strong></td><td>Close open MFV popovers</td></tr>
<tr><td><strong>Mouse Wheel</strong></td><td>Zoom in/out</td></tr>
<tr><td><strong>Click+Drag</strong> (zoomed)</td><td>Pan image</td></tr>
</tbody></table></div>
<hr />
<h2 id="hotkeys_shortcuts-quick-reference-card">Quick Reference Card</h2>
<h3 id="hotkeys_shortcuts-most-used-shortcuts">Most Used Shortcuts</h3>
<pre><code>Grid Navigation: Arrow keys
Open Viewer: Enter / Double-click
Rate: 0-5 keys
Search: Ctrl+F / Ctrl+K
Scan: Ctrl+S
Download: S
Tags: T
Collection: B
Floating Viewer: V
Close Viewer: Esc
Zoom: Mouse wheel
Pan: Click+drag</code></pre>
<h3 id="hotkeys_shortcuts-viewer-essentials">Viewer Essentials</h3>
<pre><code>Fullscreen: F
Info Panel: D
Pixel Probe: I
Loupe: L
Zebra: Z
Grid: G
1:1 Zoom: Alt+1</code></pre>
<h3 id="hotkeys_shortcuts-mfv-essentials">MFV Essentials</h3>
<pre><code>Toggle MFV: V
Compare Mode: C
Live Stream: L
KSampler Preview: K
Node Stream: N
Play/Pause: Space</code></pre>
<hr />
<h2 id="hotkeys_shortcuts-customization">Customization</h2>
<h3 id="hotkeys_shortcuts-remapping-shortcuts">Remapping Shortcuts</h3>
<p>Currently, shortcuts are hardcoded. Future versions may support customization via:</p>
<ul>
<li>Browser extensions</li>
<li>ComfyUI keymap settings</li>
<li>Configuration file</li>
</ul>
<h3 id="hotkeys_shortcuts-conflicts-with-comfyui">Conflicts with ComfyUI</h3>
<p>The following shortcuts may conflict with ComfyUI globals:</p>
<ul>
<li><strong>Ctrl+S</strong>: Also triggers ComfyUI workflow save</li>
<li><strong>S</strong>: Download waits for key release so S+drag can still be used for clean export without triggering a download</li>
<li><strong>Delete</strong>: Also deletes ComfyUI nodes</li>
<li><strong>Ctrl+Z</strong>: ComfyUI undo (not overridden)</li>
</ul>
<p>When the Assets Manager panel has focus, its shortcuts take precedence.</p>
<hr />
<h2 id="hotkeys_shortcuts-accessibility">Accessibility</h2>
<h3 id="hotkeys_shortcuts-keyboard-only-navigation">Keyboard-Only Navigation</h3>
<p>All features are accessible via keyboard:</p>
<ul>
<li>Tab through UI elements</li>
<li>Enter/Space to activate</li>
<li>Arrow keys to navigate</li>
<li>Esc to close dialogs</li>
</ul>
<h3 id="hotkeys_shortcuts-screen-reader-support">Screen Reader Support</h3>
<ul>
<li>ARIA labels on interactive elements</li>
<li>Status announcements for operations</li>
<li>Alt text on thumbnails</li>
</ul>
<hr />
<p>_Hotkeys &amp; Shortcuts Guide Version: 2.4.9_ _Last Updated: May 18, 2026_ _Compatible with Majoor Assets Manager v2.4.4+_</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/HOTKEYS_SHORTCUTS.md</code>
</div>
</div>
</section>
<section class="panel section" id="installation">
<div class="panel-h"><h2>INSTALLATION.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="installation-majoor-assets-manager-installation-guide">Majoor Assets Manager - Installation Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: May 18, 2026</p>
<h2 id="installation-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: 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).</p>
<h2 id="installation-prerequisites">Prerequisites</h2>
<h3 id="installation-system-requirements">System Requirements</h3>
<ul>
<li>ComfyUI installation (≥ 0.13.0 recommended)</li>
<li>Python 3.10, 3.11, or 3.12 (3.13 compatible)</li>
<li>At least 500MB free disk space for the extension and dependencies</li>
<li>Administrator privileges (for installation and optional tool installations)</li>
</ul>
<h3 id="installation-supported-platforms">Supported Platforms</h3>
<ul>
<li><strong>Windows</strong>: 10/11</li>
<li><strong>macOS</strong>: 10.15 or higher</li>
<li><strong>Linux</strong>: Ubuntu 22.04+, Debian 12+, Fedora, or equivalent</li>
</ul>
<h2 id="installation-quick-installation-recommended">Quick Installation (Recommended)</h2>
<h3 id="installation-using-comfyui-manager">Using ComfyUI Manager</h3>
<ol>
<li>Open ComfyUI Manager in your browser</li>
<li>Find "Majoor Assets Manager" in the extensions list</li>
<li>Click "Install" next to the extension</li>
<li>Wait for the installation to complete</li>
<li>Restart ComfyUI completely</li>
<li>The extension should now be available in the Assets Manager tab</li>
</ol>
<h2 id="installation-manual-installation">Manual Installation</h2>
<h3 id="installation-step-1-clone-the-repository">Step 1: Clone the Repository</h3>
<p>Open your terminal/command prompt and navigate to your ComfyUI custom_nodes directory:</p>
<pre><code class="language-bash">cd /path/to/your/ComfyUI/custom_nodes</code></pre>
<p>Clone the repository:</p>
<pre><code class="language-bash">git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager ComfyUI-Majoor-AssetsManager</code></pre>
<h3 id="installation-step-2-install-python-dependencies">Step 2: Install Python Dependencies</h3>
<p>Navigate to the extension directory and install the required packages:</p>
<pre><code class="language-bash">cd ComfyUI-Majoor-AssetsManager
pip install -r requirements.txt</code></pre>
<p>If you want the optional AI/vector features as well, install the extra vector stack too:</p>
<pre><code class="language-bash">pip install -r requirements.txt -r requirements-vector.txt</code></pre>
<p>If you are contributing to the project itself, install the local dev/test tooling separately:</p>
<pre><code class="language-bash">pip install -r requirements-dev.txt</code></pre>
<p>Dependency ownership and update rules are documented in <code>docs/DEPENDENCY_POLICY.md</code>.</p>
<blockquote><strong>Faster installs with <code>uv</code></strong>: If you have <a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener noreferrer">uv</a> installed, you can use it as a drop-in replacement for significantly faster dependency resolution:</blockquote>
<blockquote>```bash</blockquote>
<blockquote>uv pip install -r requirements.txt</blockquote>
<blockquote>```</blockquote>
<blockquote>Install <code>uv</code> with <code>pip install uv</code> or see the <a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener noreferrer">uv documentation</a> for other methods.</blockquote>
<blockquote>For AI/vector features with <code>uv</code>:</blockquote>
<blockquote>```bash</blockquote>
<blockquote>uv pip install -r requirements.txt -r requirements-vector.txt</blockquote>
<blockquote>```</blockquote>
<blockquote></blockquote>
<blockquote>For contributor tooling with <code>uv</code>:</blockquote>
<blockquote>```bash</blockquote>
<blockquote>uv pip install -r requirements-dev.txt</blockquote>
<blockquote>```</blockquote>
<h3 id="installation-step-3-restart-comfyui">Step 3: Restart ComfyUI</h3>
<p>Stop your ComfyUI server completely and restart it to load the new extension.</p>
<h2 id="installation-optional-dependencies-highly-recommended">Optional Dependencies (Highly Recommended)</h2>
<p>For full functionality including metadata extraction and file tagging, install these external tools:</p>
<h3 id="installation-windows-installation">Windows Installation</h3>
<h4 id="installation-option-a-using-scoop-package-manager">Option A: Using Scoop Package Manager</h4>
<ol>
<li>Install Scoop if you don't have it:</li>
</ol>
<pre><code class="language-powershell"> Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
irm get.scoop.sh | iex</code></pre>
<ol>
<li>Install the required tools:</li>
</ol>
<pre><code class="language-powershell"> scoop install ffmpeg exiftool</code></pre>
<h4 id="installation-option-b-using-chocolatey-package-manager">Option B: Using Chocolatey Package Manager</h4>
<ol>
<li>Install Chocolatey if you don't have it:</li>
</ol>
<pre><code class="language-powershell"> 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'))</code></pre>
<ol>
<li>Install the required tools:</li>
</ol>
<pre><code class="language-powershell"> choco install -y ffmpeg exiftool</code></pre>
<h4 id="installation-option-c-using-winget">Option C: Using WinGet</h4>
<pre><code class="language-powershell">winget install -e --id Gyan.FFmpeg
winget install -e --id OliverBetz.ExifTool</code></pre>
<h4 id="installation-option-d-manual-installation">Option D: Manual Installation</h4>
<ol>
<li><strong>FFmpeg</strong>: Download from https://www.gyan.dev/ffmpeg/builds/</li>
<ul>
<li>Extract to a folder (e.g., <code>C:\ffmpeg</code>)</li>
<li>Add <code>C:\ffmpeg\bin</code> to your system PATH</li>
</ul>
</ol>
<ol>
<li><strong>ExifTool</strong>: Download from https://exiftool.org/</li>
<ul>
<li>Download <code>exiftool-#.##.zip</code></li>
<li>Extract to a folder (e.g., <code>C:\exiftool</code>)</li>
<li>Add <code>C:\exiftool</code> to your system PATH</li>
</ul>
</ol>
<h3 id="installation-macos-installation">macOS Installation</h3>
<h4 id="installation-using-homebrew-recommended">Using Homebrew (Recommended)</h4>
<ol>
<li>Install Homebrew if you don't have it:</li>
</ol>
<pre><code class="language-bash"> /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"</code></pre>
<ol>
<li>Install the required tools:</li>
</ol>
<pre><code class="language-bash"> brew install ffmpeg exiftool</code></pre>
<h3 id="installation-linux-installation">Linux Installation</h3>
<h4 id="installation-ubuntudebian">Ubuntu/Debian</h4>
<pre><code class="language-bash">sudo apt update
sudo apt install -y ffmpeg libimage-exiftool-perl</code></pre>
<h4 id="installation-fedorarhel">Fedora/RHEL</h4>
<pre><code class="language-bash">sudo dnf install -y ffmpeg perl-Image-ExifTool</code></pre>
<h4 id="installation-arch-linux">Arch Linux</h4>
<pre><code class="language-bash">sudo pacman -S ffmpeg exiftool</code></pre>
<h2 id="installation-verification-steps">Verification Steps</h2>
<p>After installation, verify everything is working:</p>
<h3 id="installation-step-1-check-extension-loading">Step 1: Check Extension Loading</h3>
<ol>
<li>Start ComfyUI</li>
<li>Look for the Assets Manager tab in the interface</li>
<li>Check the console/logs for any error messages during startup</li>
</ol>
<h3 id="installation-step-2-verify-external-tools-if-installed">Step 2: Verify External Tools (if installed)</h3>
<p>Open a terminal/command prompt and run:</p>
<pre><code class="language-bash">exiftool -ver
ffprobe -version</code></pre>
<p>Both commands should return version information without errors.</p>
<h3 id="installation-step-3-test-basic-functionality">Step 3: Test Basic Functionality</h3>
<ol>
<li>Open the Assets Manager in ComfyUI</li>
<li>Switch between different scopes (Outputs, Inputs, Custom, Collections)</li>
<li>Perform a simple search to verify indexing is working</li>
<li>Try opening the viewer for an asset</li>
</ol>
<h2 id="installation-configuration">Configuration</h2>
<h3 id="installation-environment-variables">Environment Variables</h3>
<p>You can configure the extension using environment variables. Add these to your shell profile or set them before starting ComfyUI:</p>
<pre><code class="language-bash"># 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 &lt;output&gt;/_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"
</code></pre>
<h3 id="installation-ui-output-directory-override">UI Output Directory Override</h3>
<p>If you prefer not to change shell startup scripts, you can override the generation output folder directly from the ComfyUI settings UI:</p>
<ol>
<li>Open Settings → <strong>Majoor Assets Manager</strong> → <strong>Advanced</strong> and search for <code>path</code> if needed.</li>
<li>Edit <strong>Generation Output Directory</strong>.</li>
<li>Save the new directory path. Leave the field empty to fall back to the current backend default.</li>
</ol>
<p><a href="docs/images/output-directory-override-ui.svg" target="_blank" rel="noopener noreferrer">[Output directory override in Majoor settings]</a></p>
<h3 id="installation-windows-batch-file-example">Windows Batch File Example</h3>
<p>Create a batch file to set environment variables and start ComfyUI:</p>
<pre><code class="language-batch">@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</code></pre>
<h3 id="installation-remote-access-write-permissions">Remote Access Write Permissions</h3>
<p>If you open ComfyUI from another machine, Majoor blocks write operations by default unless you explicitly allow them.</p>
<p>Recommended setup: define <code>MAJOOR_API_TOKEN</code> on the machine that runs ComfyUI, then send the same token from the remote client.</p>
<p>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.</p>
<p>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.</p>
<p>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:</p>
<ul>
<li><code>Require Token For All Writes</code></li>
<li><code>Recommended Remote LAN Setup</code> to auto-generate a token and apply the safest common LAN defaults in one click</li>
<li><code>Allow HTTP Token Transport</code> for trusted LAN-only HTTP setups</li>
<li><code>Allow Remote Full Access</code> if you explicitly want no-token remote writes</li>
<li><code>API Token</code> for the fixed shared token value</li>
</ul>
<h4 id="installation-fastest-settings-only-path-for-a-trusted-lan">Fastest Settings-only path for a trusted LAN</h4>
<ol>
<li>Open Majoor Settings in ComfyUI.</li>
<li>Turn on <code>Recommended Remote LAN Setup</code>.</li>
<li>Confirm that the current browser session is authorized.</li>
<li>Keep <code>Allow Remote Full Access</code> off unless you explicitly want no-token remote writes.</li>
</ol>
<p>That preset automatically:</p>
<ul>
<li>generates a strong API token if none exists yet</li>
<li>enables <code>Require Token For All Writes</code></li>
<li>keeps <code>Allow Remote Full Access</code> disabled</li>
<li>enables <code>Allow HTTP Token Transport</code> automatically when the current session is plain HTTP on a non-loopback LAN address</li>
<li>injects the token into the current browser session immediately so write actions work without a manual copy/paste step</li>
</ul>
<p>Visual confirmation inside Assets Manager:</p>
<ul>
<li>the runtime status widget now shows a <code>Write auth:</code> line such as <code>Write auth: active ...ABCD</code></li>
<li>if the browser session is missing authorization while the server still requires a token, the same widget reports that state directly</li>
</ul>
<p>If you need to authorize a different browser or device, either:</p>
<ul>
<li>enter a fixed shared value in <code>Security -&amp;gt; Majoor: API Token</code>, or</li>
<li>open that browser while signed in to ComfyUI and let Majoor bootstrap a session token there</li>
</ul>
<h4 id="installation-windows-batch-example">Windows batch example</h4>
<pre><code class="language-batch">@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</code></pre>
<h4 id="installation-windows-powershell-example">Windows PowerShell example</h4>
<pre><code class="language-powershell">$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</code></pre>
<h4 id="installation-linuxmacos-shell-example">Linux/macOS shell example</h4>
<pre><code class="language-bash">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</code></pre>
<h4 id="installation-unsafe-fallback">Unsafe fallback</h4>
<p>If you really want to allow remote writes without a token, set <code>MAJOOR_ALLOW_REMOTE_WRITE=1</code> before starting ComfyUI. This is less safe and should only be used on trusted networks.</p>
<pre><code class="language-bash">export MAJOOR_ALLOW_REMOTE_WRITE=1
python main.py --listen 0.0.0.0 --port 8188</code></pre>
<h4 id="installation-remote-client-side">Remote client side</h4>
<ul>
<li>In the ComfyUI UI, open the Majoor settings and set the same token under <code>Security -&amp;gt; Majoor: API Token</code>.</li>
<li>For direct API calls, send either <code>X-MJR-Token: &amp;lt;token&amp;gt;</code> or <code>Authorization: Bearer &amp;lt;token&amp;gt;</code>.</li>
<li>Restart ComfyUI after changing environment variables so the new values are picked up.</li>
<li>On first remote use, if ComfyUI authentication is enabled and no persistent token exists yet, Majoor can provision the initial session token automatically.</li>
</ul>
<h2 id="installation-troubleshooting">Troubleshooting</h2>
<h3 id="installation-extension-not-appearing">Extension Not Appearing</h3>
<ul>
<li>Verify the folder is named correctly: <code>ComfyUI-Majoor-AssetsManager</code></li>
<li>Check that all files were downloaded properly</li>
<li>Ensure ComfyUI was restarted after installation</li>
<li>Look for error messages in the ComfyUI console</li>
</ul>
<h3 id="installation-missing-dependencies-error">Missing Dependencies Error</h3>
<p>If you see messages about missing dependencies:</p>
<pre><code class="language-bash"># Navigate to the extension directory
cd ComfyUI-Majoor-AssetsManager
pip install -r requirements.txt</code></pre>
<p>If the error is related to AI/vector search features, install the optional vector stack too:</p>
<pre><code class="language-bash">pip install -r requirements.txt -r requirements-vector.txt</code></pre>
<h3 id="installation-external-tools-not-found">External Tools Not Found</h3>
<p>If metadata extraction isn't working:</p>
<ol>
<li>Verify tools are installed: <code>exiftool -ver</code> and <code>ffprobe -version</code></li>
<li>Check if tools are in your system PATH</li>
<li>Set environment variables to point to the executables directly</li>
</ol>
<h3 id="installation-permission-errors">Permission Errors</h3>
<ul>
<li>Ensure ComfyUI has read/write access to the output directory</li>
<li>On Windows, run as administrator if needed</li>
<li>On Unix systems, check file permissions with <code>ls -la</code></li>
</ul>
<h3 id="installation-slow-performance">Slow Performance</h3>
<ul>
<li>The first scan may take time for large directories</li>
<li>Subsequent scans will be faster due to incremental indexing</li>
<li>Consider excluding very large directories that don't contain relevant assets</li>
</ul>
<h2 id="installation-post-installation-setup">Post-Installation Setup</h2>
<h3 id="installation-first-time-usage">First-Time Usage</h3>
<ol>
<li>Open ComfyUI and navigate to the Assets Manager tab</li>
<li>The extension will automatically begin indexing your output directory</li>
<li>Wait for the initial scan to complete (progress shown in status bar)</li>
<li>Use the search bar to find assets</li>
<li>Right-click on assets to access context menus</li>
</ol>
<h3 id="installation-adding-custom-directories">Adding Custom Directories</h3>
<ol>
<li>Right-click in the Assets Manager interface</li>
<li>Select "Add Custom Root"</li>
<li>Enter the path to your custom directory</li>
<li>The directory will be added to the Custom scope</li>
</ol>
<h3 id="installation-creating-your-first-collection">Creating Your First Collection</h3>
<ol>
<li>Select one or more assets</li>
<li>Right-click and choose "Add to Collection"</li>
<li>Create a new collection or add to an existing one</li>
<li>Access your collections from the Collections tab</li>
</ol>
<h2 id="installation-updates">Updates</h2>
<h3 id="installation-using-comfyui-manager">Using ComfyUI Manager</h3>
<p>Updates will be available through the ComfyUI Manager interface when released.</p>
<h3 id="installation-manual-updates">Manual Updates</h3>
<pre><code class="language-bash">cd ComfyUI/custom_nodes/ComfyUI-Majoor-AssetsManager
git pull origin main
pip install -r requirements.txt --upgrade</code></pre>
<p>If you use AI/vector features, also upgrade the optional vector requirements:</p>
<pre><code class="language-bash">pip install -r requirements.txt -r requirements-vector.txt --upgrade</code></pre>
<p>Remember to restart ComfyUI after updating.</p>
<h2 id="installation-uninstallation">Uninstallation</h2>
<h3 id="installation-using-comfyui-manager">Using ComfyUI Manager</h3>
<p>Simply click "Uninstall" in the ComfyUI Manager interface.</p>
<h3 id="installation-manual-removal">Manual Removal</h3>
<ol>
<li>Delete the <code>ComfyUI-Majoor-AssetsManager</code> folder from <code>custom_nodes</code></li>
<li>Restart ComfyUI</li>
<li>The extension will be completely removed</li>
</ol>
<hr />
<p><em>Installation Guide Version: 1.1</em> <em>Last Updated: April 5, 2026</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/INSTALLATION.md</code>
</div>
</div>
</section>
<section class="panel section" id="mfv_guide">
<div class="panel-h"><h2>MFV_GUIDE.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="mfv_guide-majoor-floating-viewer-mfv-guide">Majoor Floating Viewer (MFV) Guide</h1>
<p><strong>Last Updated</strong>: May 10, 2026</p>
<h2 id="mfv_guide-overview">Overview</h2>
<p>The Majoor Floating Viewer is the fast workflow cockpit inside Majoor Assets Manager.</p>
<p>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.</p>
<p>This guide focuses only on MFV.</p>
<p><a href="docs/images/mfv-overview.png" target="_blank" rel="noopener noreferrer">[MFV overview]</a></p>
<h2 id="mfv_guide-what-mfv-covers">What MFV Covers</h2>
<p>MFV is not just a floating preview anymore. It brings together:</p>
<ul>
<li>compare modes</li>
<li>A/B/C/D pin slots</li>
<li>format and guide overlays</li>
<li>Live Stream</li>
<li>Node Stream</li>
<li>KSampler Preview</li>
<li>Node Parameters</li>
<li>Run and Stop actions</li>
<li>pop-out / detached window workflow</li>
</ul>
<h2 id="mfv_guide-opening-mfv">Opening MFV</h2>
<p>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.</p>
<p>Typical workflow:</p>
<ol>
<li>Select an asset in the grid.</li>
<li>Open the Floating Viewer.</li>
<li>Pick the mode or stream you want from the MFV toolbar.</li>
</ol>
<h2 id="mfv_guide-compare-modes">Compare Modes</h2>
<p>MFV supports several ways to compare results depending on how much context you need on screen.</p>
<h3 id="mfv_guide-simple-mode">Simple mode</h3>
<p>Use this when you want the cleanest single-asset view.</p>
<ul>
<li>one asset visible at a time</li>
<li>best for inspection and overlay controls</li>
<li>good default when you are reviewing video or a single final image</li>
</ul>
<h3 id="mfv_guide-ab-compare">A/B compare</h3>
<p>Use A/B compare when you want to flip between two candidates quickly.</p>
<ul>
<li>ideal for subtle sampler, CFG, or prompt changes</li>
<li>good for before/after review</li>
<li>especially useful when one slot is pinned and the other stays live</li>
</ul>
<h3 id="mfv_guide-side-by-side-compare">Side-by-side compare</h3>
<p>Use side-by-side when both assets must remain visible at the same time.</p>
<ul>
<li>stronger spatial comparison than A/B switching</li>
<li>useful for composition and crop review</li>
<li>good when synchronized zoom/pan matters</li>
</ul>
<h3 id="mfv_guide-grid-compare">Grid compare</h3>
<p>Use grid compare when you want to review several references together.</p>
<ul>
<li>suited to broader sweeps</li>
<li>pairs naturally with A/B/C/D pins</li>
<li>good for sampler or prompt variation sets</li>
</ul>
<p><a href="docs/images/mfv-compare-pins.png" target="_blank" rel="noopener noreferrer">[MFV compare and pins]</a></p>
<h2 id="mfv_guide-pin-mode-abcd">Pin Mode (A/B/C/D)</h2>
<p>The pin system lets you lock up to four references so they stay stable while other slots keep changing.</p>
<h3 id="mfv_guide-what-pins-are-for">What pins are for</h3>
<ul>
<li>keep your best result as a baseline</li>
<li>compare new generations against a fixed reference</li>
<li>hold four selected results in grid compare</li>
</ul>
<h3 id="mfv_guide-practical-pin-patterns">Practical pin patterns</h3>
<ul>
<li><strong>A pinned, B live</strong>: best pattern for iterative prompt or sampler work</li>
<li><strong>A/B pinned</strong>: useful for deliberate side-by-side review</li>
<li><strong>A/B/C/D pinned</strong>: useful for a final selection round in grid compare</li>
</ul>
<p>Pinned slots are meant to remain stable while unpinned content can keep following the current workflow output.</p>
<h2 id="mfv_guide-format-and-guides">Format And Guides</h2>
<p>MFV can also act as a framing and presentation surface.</p>
<h3 id="mfv_guide-format-overlays">Format overlays</h3>
<p>Use format controls when you want to preview how the media reads inside a target ratio or crop.</p>
<ul>
<li>useful for cinematic framing checks</li>
<li>useful when comparing several outputs against the same intended presentation shape</li>
</ul>
<h3 id="mfv_guide-guides">Guides</h3>
<p>Use guides when you want composition references directly in the viewer.</p>
<ul>
<li>rule-of-thirds style guidance</li>
<li>safe-area style guidance</li>
<li>fast visual help during compare sessions</li>
</ul>
<p>These controls matter most when MFV is used as a review surface, not just as a passive preview.</p>
<h2 id="mfv_guide-live-stream">Live Stream</h2>
<p>Live Stream is for final outputs.</p>
<ul>
<li>follows newly completed outputs after execution</li>
<li>best for monitoring what the workflow actually saves</li>
<li>keeps MFV aligned with the most recent finished result</li>
</ul>
<p>Use Live Stream when you care about the last completed output file, not the currently selected node.</p>
<h2 id="mfv_guide-node-stream">Node Stream</h2>
<p>Node Stream is for selected-node media.</p>
<ul>
<li>follows the node you click in ComfyUI when that node exposes frontend media</li>
<li>useful for preview nodes, save nodes, loader nodes, and compatible live-preview surfaces</li>
<li>good for debugging a specific point in the workflow</li>
</ul>
<p>Use Node Stream when you want to inspect the media attached to the selected node, not the final workflow output.</p>
<h2 id="mfv_guide-ksampler-preview">KSampler Preview</h2>
<p>KSampler Preview is for denoising-step frames during execution.</p>
<ul>
<li>shows the progression while the workflow is still running</li>
<li>useful when you want feedback before the final file exists</li>
<li>different from Live Stream and different from Node Stream</li>
</ul>
<p>In short:</p>
<ul>
<li><strong>Live Stream</strong> = final outputs</li>
<li><strong>Node Stream</strong> = selected node media</li>
<li><strong>KSampler Preview</strong> = execution preview frames</li>
</ul>
<p><a href="docs/images/mfv-streams.png" target="_blank" rel="noopener noreferrer">[MFV streams]</a></p>
<h2 id="mfv_guide-node-parameters">Node Parameters</h2>
<p>Node Parameters let you inspect and edit the workflow node that produced the current result without leaving MFV.</p>
<p>Typical values you will use there:</p>
<ul>
<li>prompt text</li>
<li>seed</li>
<li>steps</li>
<li>CFG</li>
<li>sampler</li>
<li>scheduler</li>
</ul>
<p>This makes MFV practical for short iteration loops where you want to tweak one or two values and launch another run immediately.</p>
<h2 id="mfv_guide-run-and-stop">Run And Stop</h2>
<p>MFV is not only a viewer surface. It also exposes execution actions.</p>
<h3 id="mfv_guide-run">Run</h3>
<p>Use Run when you want to queue the current workflow again directly from MFV.</p>
<p>Good use cases:</p>
<ul>
<li>prompt iteration</li>
<li>seed changes</li>
<li>sampler or scheduler changes</li>
<li>quick compare loops with pinned references</li>
</ul>
<h3 id="mfv_guide-stop">Stop</h3>
<p>Use Stop when you want to abort the current generation without leaving the viewer surface.</p>
<p>Good use cases:</p>
<ul>
<li>a bad direction is obvious early</li>
<li>KSampler Preview already tells you the run is not worth finishing</li>
<li>you want to reclaim time before launching the next variation</li>
</ul>
<h2 id="mfv_guide-pop-out">Pop-Out</h2>
<p>Pop-out detaches MFV from the main ComfyUI window so it can live as a separate viewer window.</p>
<p>This is useful when:</p>
<ul>
<li>you want MFV on a second screen</li>
<li>you want the canvas and the viewer visible side by side</li>
<li>you want to keep MFV open as a dedicated monitoring surface while working on the graph elsewhere</li>
</ul>
<p><a href="docs/images/mfv-controls-actions.png" target="_blank" rel="noopener noreferrer">[MFV controls and actions]</a></p>
<h2 id="mfv_guide-graph-map-as-a-natural-companion">Graph Map As A Natural Companion</h2>
<p>Graph Map is the workflow-context side of MFV.</p>
<p>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.</p>
<p><a href="docs/images/graph-map-overview.png" target="_blank" rel="noopener noreferrer">[Graph Map overview]</a></p>
<p>Graph Map complements MFV by adding:</p>
<ul>
<li>readable node and subgraph names instead of raw opaque identifiers</li>
<li>a selected-node detail panel with copy-ready parameters and actions</li>
<li>a workflow overview that stays close to the current asset preview</li>
</ul>
<p>Typical pairing:</p>
<ol>
<li>Review the latest result in MFV.</li>
<li>Open Graph Map to locate the relevant node or subgraph.</li>
<li>Inspect the selected node details.</li>
<li>Return to Node Parameters or Run inside MFV for the next iteration.</li>
</ol>
<p>For the full Graph Map walkthrough, including the selected-node detail panel, see <a href="#graph_map">GRAPH_MAP.md</a>.</p>
<h2 id="mfv_guide-recommended-workflows">Recommended Workflows</h2>
<h3 id="mfv_guide-fast-prompt-iteration">Fast prompt iteration</h3>
<ol>
<li>Pin your baseline in slot A.</li>
<li>Edit prompt or seed in Node Parameters.</li>
<li>Click Run.</li>
<li>Let the unpinned slot follow Live Stream.</li>
</ol>
<h3 id="mfv_guide-execution-monitoring">Execution monitoring</h3>
<ol>
<li>Turn on KSampler Preview.</li>
<li>Watch denoising steps during the run.</li>
<li>Keep Live Stream ready for the final output.</li>
</ol>
<h3 id="mfv_guide-node-focused-debugging">Node-focused debugging</h3>
<ol>
<li>Turn on Node Stream.</li>
<li>Click the relevant node in ComfyUI.</li>
<li>Inspect available frontend media from that node.</li>
</ol>
<h3 id="mfv_guide-two-screen-review">Two-screen review</h3>
<ol>
<li>Pop out MFV.</li>
<li>Keep ComfyUI on the main screen.</li>
<li>Use the detached viewer as a dedicated compare and monitoring surface.</li>
</ol>
<h2 id="mfv_guide-related-docs">Related Docs</h2>
<ul>
<li><a href="#graph_map">GRAPH_MAP.md</a></li>
<li><a href="#viewer_feature_tutorial">VIEWER_FEATURE_TUTORIAL.md</a></li>
<li><a href="#floating_viewer_workflow_sidebar">FLOATING_VIEWER_WORKFLOW_SIDEBAR.md</a></li>
<li><a href="#hotkeys_shortcuts">HOTKEYS_SHORTCUTS.md</a></li>
<li><a href="#settings_configuration">SETTINGS_CONFIGURATION.md</a></li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/MFV_GUIDE.md</code>
</div>
</div>
</section>
<section class="panel section" id="migration_ts_tracker">
<div class="panel-h"><h2>MIGRATION_TS_TRACKER.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="migration_ts_tracker-migration-js-typescript-tracker">Migration JS → TypeScript — Tracker</h1>
<blockquote>Mis à jour : 2026-05-26</blockquote>
<blockquote>Stratégie : renommer <code>.js</code> → <code>.ts</code> + ajouter annotations TypeScript progressivement par frontière logique.</blockquote>
<hr />
<h2 id="migration_ts_tracker-résumé-global">Résumé global</h2>
<div class="table-wrap"><table>
<thead><tr><th>Zone</th><th>Total fichiers</th><th>Migrés (<code>.ts</code>)</th><th>Restants (<code>.js</code>)</th></tr></thead>
<tbody>
<tr><td><code>utils/</code></td><td>14</td><td><strong>14 ✅</strong></td><td>0</td></tr>
<tr><td><code>api/</code></td><td>5</td><td><strong>5 ✅</strong></td><td>0</td></tr>
<tr><td><code>integration/</code></td><td>1</td><td><strong>1 ✅</strong></td><td>0</td></tr>
<tr><td><code>stores/</code></td><td>6</td><td><strong>6 ✅</strong></td><td>0</td></tr>
<tr><td><code>app/</code></td><td>30</td><td><strong>30 ✅</strong></td><td>0</td></tr>
<tr><td><code>vue/</code></td><td>23</td><td><strong>23 ✅</strong></td><td>0</td></tr>
<tr><td><code>components/</code></td><td>15</td><td><strong>15 ✅</strong></td><td>0</td></tr>
<tr><td><code>features/</code></td><td>141</td><td><strong>141 ✅</strong></td><td>0</td></tr>
<tr><td><code>entry.ts</code></td><td>1</td><td><strong>1 ✅</strong></td><td>0</td></tr>
<tr><td><strong>TOTAL</strong></td><td><strong>~236</strong></td><td><strong>~236 ✅</strong></td><td><strong>0</strong></td></tr>
</tbody></table></div>
<blockquote>Fichiers exclus (générés, ne pas toucher) :</blockquote>
<blockquote>- <code>ui/vue/majoor-ui.umd.js</code></blockquote>
<blockquote>- <code>ui/app/i18n.generated.js</code></blockquote>
<blockquote>- <code>dist/</code> (tout le dossier — produit par <code>npm run build</code>)</blockquote>
<hr />
<h2 id="migration_ts_tracker-détail-par-zone">Détail par zone</h2>
<h3 id="migration_ts_tracker-uiutils-1414-fait">✅ <code>ui/utils/</code> — 14/14 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>TS types</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>debounce.ts</code></td><td>✅ <code>DebouncedFn&amp;lt;TArgs&amp;gt;</code> générique</td><td>Interface + generic</td></tr>
<tr><td><code>deleteGuard.ts</code></td><td>✅</td><td><code>Promise&amp;lt;boolean&amp;gt;</code> return</td></tr>
<tr><td><code>dom.ts</code></td><td>✅</td><td>JSDoc → annotations TS</td></tr>
<tr><td><code>events.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>extractOutputFiles.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>filenames.ts</code></td><td>✅</td><td><code>validateFilename</code> retourne <code>{valid,reason}</code></td></tr>
<tr><td><code>format.ts</code></td><td>✅</td><td>`Date \</td><td>null` return</td></tr>
<tr><td><code>ids.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>logging.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>mediaFps.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>path.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>safeCall.ts</code></td><td>✅</td><td>Generic <code>safeCall&amp;lt;T&amp;gt;</code></td></tr>
<tr><td><code>tooltipShortcuts.ts</code></td><td>✅</td><td>`HTMLElement \</td><td>null` param</td></tr>
<tr><td><code>ttlCache.ts</code></td><td>✅</td><td>Interface <code>TTLCache&amp;lt;K,V&amp;gt;</code> + generic</td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uiapi-55-fait">✅ <code>ui/api/</code> — 5/5 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>TS types</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>client.ts</code></td><td>✅</td><td><code>ApiResult&amp;lt;T&amp;gt;</code> typedef, interfaces existantes</td></tr>
<tr><td><code>clientAuth.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>clientOps.ts</code></td><td>✅</td><td></td></tr>
<tr><td><code>endpoints.ts</code></td><td>✅</td><td><code>AssetFilterParams</code>, <code>ListURLParams</code> interfaces</td></tr>
<tr><td><code>fetchUtils.ts</code></td><td>✅</td><td></td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uiintegration-11-fait">✅ <code>ui/integration/</code> — 1/1 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>TS types</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>comfy_send_to_am.ts</code></td><td>✅</td><td></td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uistores-66-fait-sessions-précédentes">✅ <code>ui/stores/</code> — 6/6 FAIT (sessions précédentes)</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>TS types</th></tr></thead>
<tbody>
<tr><td><code>useRuntimeStore.ts</code></td><td>✅</td></tr>
<tr><td><code>usePanelStore.ts</code></td><td>✅</td></tr>
<tr><td><code>runtimeEnrichmentState.ts</code></td><td>✅</td></tr>
<tr><td><code>panelStateBridge.ts</code></td><td>✅</td></tr>
<tr><td><code>getOptionalRuntimeStore.ts</code></td><td>✅</td></tr>
<tr><td><code>getOptionalPanelStore.ts</code></td><td>✅</td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uitypes-11-fait">✅ <code>ui/types/</code> — 1/1 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>comfyui-frontend.d.ts</code></td><td>Déclarations de types ComfyUI</td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uiapp-3030-fait">✅ <code>ui/app/</code> — 30/30 FAIT</h3>
<p>Tous les fichiers renommés <code>.ts</code> + annotations TS sur les exports publics.</p>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>TS types</th><th>Statut</th></tr></thead>
<tbody>
<tr><td><code>settingsStore.ts</code></td><td>✅</td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>runtimeState.ts</code></td><td>✅</td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>assetUrls.ts</code></td><td>✅ <code>(unknown): string</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>bootstrap.ts</code></td><td>✅ async void, ReturnType timer</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>comfyApiBridge.ts</code></td><td>✅ <code>MajoorComfyApp</code>, <code>MajoorComfyApi</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>config.ts</code></td><td>✅ <code>AppConfig</code> typedef</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>dialogs.ts</code></td><td>✅ <code>MajoorExtensionDialog</code> retours</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>DialogTemplates.ts</code></td><td>✅ (renommé)</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>events.ts</code></td><td>✅ (constantes string)</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>hostAdapter.ts</code></td><td>✅ types ComfyUI complets</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>i18n.ts</code></td><td>✅ exports publics annotés</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>metrics.ts</code></td><td>✅ nombre/bool params</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>openMajoorSettings.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings.ts</code></td><td>✅ (re-exports, pas de corps)</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>style.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>toast.ts</code></td><td>✅ <code>comfyToast(unknown, string, number?, opts?)</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>versionCheck.ts</code></td><td>✅ types complets</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/MajoorSettingsDialog.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsAdvanced.ts</code></td><td>✅ <code>MajoorSettingDefinition</code> param</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsCore.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsFeed.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsGrid.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/SettingsPanel.ts</code></td><td>✅ <code>MajoorSettingDefinition[]</code> return</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsRuntime.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsScanning.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsSearch.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsSecurity.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/SettingsStore.ts</code></td><td>✅ (Pinia store)</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsUtils.ts</code></td><td>✅ types primitifs + generic deepMerge</td><td><strong>FAIT</strong></td></tr>
<tr><td><code>settings/settingsViewer.ts</code></td><td>✅</td><td><strong>FAIT</strong></td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uivue-2323-fait">✅ <code>ui/vue/</code> — 23/23 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>Statut</th></tr></thead>
<tbody>
<tr><td><code>components/panel/summaryBarState.ts</code></td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>components/panel/sidebar/generationSectionState.ts</code></td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>components/grid/gridDomBridge.ts</code></td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>components/grid/assetsGridHostState.ts</code></td><td><strong>FAIT</strong> (session précédente)</td></tr>
<tr><td><code>composables/gridVisibility.ts</code></td><td><strong>FAIT</strong> — `Element</td><td>null` params</td></tr>
<tr><td><code>composables/useActiveAsset.ts</code></td><td><strong>FAIT</strong> — <code>setActiveAsset/patchActiveAsset</code> typés</td></tr>
<tr><td><code>composables/useAssetsQuery.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>composables/useDragDrop.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>composables/useGridContextMenu.ts</code></td><td><strong>FAIT</strong> — <code>unknown</code> param</td></tr>
<tr><td><code>composables/useGridKeyboard.ts</code></td><td><strong>FAIT</strong> — `HTMLElement</td><td>null`</td></tr>
<tr><td><code>composables/useGridLoader.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>composables/useGridSelection.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>composables/useGridState.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>composables/useVirtualGrid.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>createVueApp.ts</code></td><td><strong>FAIT</strong> — <code>ReturnType&amp;lt;typeof createApp&amp;gt;</code></td></tr>
<tr><td><code>grid/useAssetCollection.ts</code></td><td><strong>FAIT</strong> — <code>defaultAssetKey</code> typé</td></tr>
<tr><td><code>grid/useGridDisplayAssets.ts</code></td><td><strong>FAIT</strong> — <code>unknown[]</code> params</td></tr>
<tr><td><code>grid/useGridQuery.ts</code></td><td><strong>FAIT</strong> — string params</td></tr>
<tr><td><code>grid/useGridSnapshotCache.ts</code></td><td><strong>FAIT</strong> — clés string</td></tr>
<tr><td><code>grid/useGridVirtualRows.ts</code></td><td><strong>FAIT</strong> — <code>unknown[][]</code> return</td></tr>
<tr><td><code>grid/useInfiniteTrigger.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>grid/usePagedAssets.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>majoorPrimeVue.ts</code></td><td><strong>FAIT</strong> — <code>installMajoorPrimeVue</code> typé</td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uicomponents-1515-fait">✅ <code>ui/components/</code> — 15/15 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>Statut</th></tr></thead>
<tbody>
<tr><td><code>Badges.ts</code></td><td><strong>FAIT</strong> — <code>createFileBadge</code>, <code>setFileBadgeCollision</code>, <code>createRatingBadge</code>, etc.</td></tr>
<tr><td><code>RatingEditor.ts</code></td><td><strong>FAIT</strong> — params asset + callback</td></tr>
<tr><td><code>TagsEditor.ts</code></td><td><strong>FAIT</strong> — params asset + callback</td></tr>
<tr><td><code>VideoControls.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>Viewer.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>ViewerRuntime.ts</code></td><td><strong>FAIT</strong> — <code>getViewerInstance(): unknown</code></td></tr>
<tr><td><code>Viewer_impl.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>buttons.ts</code></td><td><strong>FAIT</strong> — <code>createIconButton/createModeButton(string, string)</code></td></tr>
<tr><td><code>sidebar/SidebarView.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
<tr><td><code>sidebar/parsers/a1111ParamsParser.ts</code></td><td><strong>FAIT</strong> — <code>(unknown): Record&amp;lt;string,unknown&amp;gt;</code></td></tr>
<tr><td><code>sidebar/parsers/comfyWorkflowParser.ts</code></td><td><strong>FAIT</strong> — typed return</td></tr>
<tr><td><code>sidebar/parsers/geninfoParser.ts</code></td><td><strong>FAIT</strong> — <code>sanitizePromptForDisplay</code>, etc.</td></tr>
<tr><td><code>sidebar/utils/dom.ts</code></td><td><strong>FAIT</strong> — <code>createSection</code>, <code>createFieldRow</code></td></tr>
<tr><td><code>sidebar/utils/format.ts</code></td><td><strong>FAIT</strong> — <code>formatFileSize(number)</code></td></tr>
<tr><td><code>sidebar/utils/minimap.ts</code></td><td><strong>FAIT</strong> — <code>drawWorkflowMinimap(canvas, workflow, opts?)</code></td></tr>
</tbody></table></div>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>Statut</th></tr></thead>
<tbody>
<tr><td><code>WorkflowSidebar.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>WorkflowNodesTab.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>widgetAdapters.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>sidebarRunButton.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>NodeWidgetRenderer.ts</code></td><td><strong>FAIT</strong></td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uifeaturesviewerworkflowgraphmap-44-fait">✅ <code>ui/features/viewer/workflowGraphMap/</code> — 4/4 FAIT</h3>
<div class="table-wrap"><table>
<thead><tr><th>Fichier</th><th>Statut</th></tr></thead>
<tbody>
<tr><td><code>workflowNodeLabeling.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>workflowGraphMapActions.ts</code></td><td><strong>FAIT</strong></td></tr>
<tr><td><code>workflowGraphMapData.ts</code></td><td><strong>FAIT</strong> — session 2026-05-26</td></tr>
<tr><td><code>WorkflowGraphMapPanel.ts</code></td><td><strong>FAIT</strong> — renommé</td></tr>
</tbody></table></div>
<h3 id="migration_ts_tracker-uifeaturesviewer-7070-fait-session-2026-05-26">✅ <code>ui/features/viewer/</code> — 70/70 FAIT (session 2026-05-26)</h3>
<p>Tous les <code>.js</code> renommés en <code>.ts</code> et annotés :</p>
<ul>
<li><code>FloatingViewer.ts</code>, <code>FloatingViewer.impl.ts</code>, <code>LiveStreamTracker.ts</code>, <code>ViewerCanvas.ts</code></li>
<li><code>ViewerContextMenu.ts</code>, <code>ViewerKeyboard.ts</code>, <code>ViewerState.ts</code>, <code>ViewerToolbar.ts</code></li>
<li><code>abCompare.ts</code>, <code>audioVisualizer.ts</code>, <code>filmstrip.ts</code>, <code>floatingViewer*.ts</code> (9 fichiers)</li>
<li><code>frameExport.ts</code>, <code>genInfo.ts</code>, <code>grid.ts</code>, <code>imagePreloader.ts</code>, <code>imageProcessor.ts</code></li>
<li><code>keyboard.ts</code>, <code>lifecycle.ts</code>, <code>loupe.ts</code>, <code>mediaFactory.ts</code>, <code>mediaPlayer.ts</code>, <code>metadata.ts</code></li>
<li><code>model3dCore.ts</code>, <code>model3dLoaders.ts</code>, <code>model3dRenderer.impl.ts</code>, <code>model3dRenderer.ts</code>, <code>model3dSupport.ts</code></li>
<li><code>panzoom.ts</code>, <code>playerBarManager.ts</code>, <code>probe.ts</code>, <code>processorUtils.ts</code>, <code>scopes.ts</code></li>
<li><code>sideBySide.ts</code>, <code>state.ts</code>, <code>toolbar.ts</code>, <code>toolbarActions.ts</code>, <code>toolbarControls.ts</code></li>
<li><code>videoProcessor.ts</code>, <code>videoProcessorWebGL.ts</code>, <code>videoSync.ts</code></li>
<li><code>viewerInstanceManager.ts</code>, <code>viewerOpenRequest.ts</code>, <code>viewerOverlayDismiss.ts</code></li>
<li><code>viewerRuntimeHosts.ts</code>, <code>viewerShell.ts</code>, <code>viewerThemeStyles.ts</code></li>
<li><code>nodeStream/</code> (6 fichiers + 4 adapters)</li>
</ul>
<h3 id="migration_ts_tracker-uifeaturespanel-2929-fait-session-2026-05-26">✅ <code>ui/features/panel/</code> — 29/29 FAIT (session 2026-05-26)</h3>
<p>Tous les <code>.js</code> renommés en <code>.ts</code> et annotés (<code>Record&amp;lt;string,unknown&amp;gt;</code> pour les params destructurés complexes).</p>
<h3 id="migration_ts_tracker-uientryts-fait-session-2026-05-26">✅ <code>ui/entry.ts</code> — FAIT (session 2026-05-26)</h3>
<hr />
<h2 id="migration_ts_tracker-ordre-de-migration-recommandé">Ordre de migration recommandé</h2>
<ol>
<li>✅ <code>utils/</code> + <code>api/</code> + <code>integration/</code> — <strong>FAIT</strong></li>
<li>✅ <code>app/</code> — <strong>FAIT</strong></li>
<li>✅ <code>vue/</code> — <strong>FAIT</strong></li>
<li>✅ <code>components/</code> — <strong>FAIT</strong></li>
<li>✅ <code>features/grid/</code> + <code>features/status/</code> + <code>features/stacks/</code> — <strong>FAIT</strong></li>
<li>✅ <code>features/runtime/</code> + <code>features/dnd/</code> — <strong>FAIT</strong></li>
<li>✅ <code>features/panel/</code> — <strong>FAIT</strong></li>
<li>✅ <code>features/viewer/</code> — <strong>FAIT</strong></li>
<li>✅ <code>entry.ts</code> — <strong>FAIT</strong></li>
</ol>
<p><strong>Migration JS → TypeScript terminée.</strong> 236/236 fichiers source migrés.</p>
<hr />
<h2 id="migration_ts_tracker-notes-techniques">Notes techniques</h2>
<ul>
<li><strong>tsconfig</strong> : <code>allowJs: true</code>, <code>checkJs: true</code>, <code>strict: false</code>, <code>moduleResolution: "Bundler"</code></li>
<li><strong>Imports</strong> : garder l'extension <code>.js</code> dans les imports (Bundler résout <code>.js</code> → <code>.ts</code>)</li>
<li><strong>Convention</strong> : supprimer les JSDoc <code>@param</code>/<code>@returns</code> quand remplacés par annotations TS</li>
<li><strong>Build</strong> : <code>npm run build</code> génère <code>dist/</code> — ne pas modifier <code>dist/</code> directement</li>
<li><strong>Lint</strong> : <code>npm run lint:js</code> obligatoire après conversion, <code>lint:js:ox</code> + <code>typecheck:js</code> en audit</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/MIGRATION_TS_TRACKER.md</code>
</div>
</div>
</section>
<section class="panel section" id="plugin_quick_reference">
<div class="panel-h"><h2>PLUGIN_QUICK_REFERENCE.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="plugin_quick_reference-plugin-development-quick-reference">Plugin Development Quick Reference</h1>
<h2 id="plugin_quick_reference-quick-start">Quick Start</h2>
<h3 id="plugin_quick_reference-1-create-plugin-file">1. Create Plugin File</h3>
<pre><code class="language-python"># 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)</code></pre>
<h3 id="plugin_quick_reference-2-install-plugin">2. Install Plugin</h3>
<pre><code class="language-bash"># Copy to plugin directory
cp my_plugin.py ~/.comfyui/majoor_plugins/extractors/</code></pre>
<h3 id="plugin_quick_reference-3-reload-plugins">3. Reload Plugins</h3>
<pre><code class="language-bash"># Via API
curl -X POST http://localhost:8188/mjr/am/plugins/reload
# Or restart ComfyUI</code></pre>
<hr />
<h2 id="plugin_quick_reference-plugin-api">Plugin API</h2>
<h3 id="plugin_quick_reference-required-properties">Required Properties</h3>
<div class="table-wrap"><table>
<thead><tr><th>Property</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>name</code></td><td>str</td><td>Unique identifier (lowercase, underscores)</td></tr>
<tr><td><code>supported_extensions</code></td><td>list[str]</td><td>File extensions (e.g., <code>['.png', '.webp']</code>)</td></tr>
<tr><td><code>priority</code></td><td>int</td><td>Higher = runs first (100-999 custom, 50-99 format, 1-49 generic)</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-required-methods">Required Methods</h3>
<div class="table-wrap"><table>
<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>extract</code></td><td><code>async def extract(self, filepath: str) -&amp;gt; ExtractionResult</code></td><td>Main extraction logic</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-optional-properties">Optional Properties</h3>
<div class="table-wrap"><table>
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>metadata</code></td><td><code>ExtractorMetadata</code></td><td>Auto</td><td>Plugin info (version, author, description)</td></tr>
<tr><td><code>min_compatibility_version</code></td><td>str</td><td>"2.4.9"</td><td>Minimum Majoor version</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-optional-methods">Optional Methods</h3>
<div class="table-wrap"><table>
<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>can_extract</code></td><td><code>def can_extract(self, filepath: str) -&amp;gt; bool</code></td><td>Check if can handle file</td></tr>
<tr><td><code>pre_extract</code></td><td><code>async def pre_extract(self, filepath: str) -&amp;gt; bool</code></td><td>Pre-extraction validation</td></tr>
<tr><td><code>post_extract</code></td><td><code>async def post_extract(self, filepath, result)</code></td><td>Post-extraction enrichment</td></tr>
<tr><td><code>cleanup</code></td><td><code>async def cleanup(self)</code></td><td>Cleanup on unload</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-helper-methods">Helper Methods</h3>
<div class="table-wrap"><table>
<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>_create_success_result</code></td><td><code>def _create_success_result(self, data, confidence=1.0)</code></td><td>Create success result</td></tr>
<tr><td><code>_create_error_result</code></td><td><code>def _create_error_result(self, error)</code></td><td>Create error result</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_quick_reference-extractionresult">ExtractionResult</h2>
<pre><code class="language-python">@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</code></pre>
<h3 id="plugin_quick_reference-standard-metadata-fields">Standard Metadata Fields</h3>
<pre><code class="language-python">{
# 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
}</code></pre>
<hr />
<h2 id="plugin_quick_reference-plugin-lifecycle">Plugin Lifecycle</h2>
<pre><code>┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘</code></pre>
<hr />
<h2 id="plugin_quick_reference-security">Security</h2>
<h3 id="plugin_quick_reference-blocked-patterns">Blocked Patterns</h3>
<p>The following patterns are <strong>blocked</strong> during validation:</p>
<pre><code class="language-python"># 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()</code></pre>
<h3 id="plugin_quick_reference-allowed-imports">Allowed Imports</h3>
<pre><code class="language-python"># 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</code></pre>
<hr />
<h2 id="plugin_quick_reference-testing">Testing</h2>
<h3 id="plugin_quick_reference-unit-test-example">Unit Test Example</h3>
<pre><code class="language-python">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 &gt; 0</code></pre>
<h3 id="plugin_quick_reference-debug-logging">Debug Logging</h3>
<pre><code class="language-python"># 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}")</code></pre>
<hr />
<h2 id="plugin_quick_reference-api-endpoints">API Endpoints</h2>
<div class="table-wrap"><table>
<thead><tr><th>Endpoint</th><th>Method</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>/mjr/am/plugins/list</code></td><td>GET</td><td>List all plugins</td></tr>
<tr><td><code>/mjr/am/plugins/{name}/enable</code></td><td>POST</td><td>Enable plugin</td></tr>
<tr><td><code>/mjr/am/plugins/{name}/disable</code></td><td>POST</td><td>Disable plugin</td></tr>
<tr><td><code>/mjr/am/plugins/reload</code></td><td>POST</td><td>Reload all plugins</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-example-requests">Example Requests</h3>
<pre><code class="language-bash"># 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</code></pre>
<hr />
<h2 id="plugin_quick_reference-troubleshooting">Troubleshooting</h2>
<h3 id="plugin_quick_reference-plugin-not-loading">Plugin Not Loading</h3>
<div class="table-wrap"><table>
<thead><tr><th>Symptom</th><th>Solution</th></tr></thead>
<tbody>
<tr><td>No error in logs</td><td>Check file is in correct directory</td></tr>
<tr><td>Syntax error</td><td>Fix Python syntax in plugin file</td></tr>
<tr><td>Validation failed</td><td>Remove blocked patterns</td></tr>
<tr><td>Import error</td><td>Check import paths are valid</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-extraction-fails">Extraction Fails</h3>
<div class="table-wrap"><table>
<thead><tr><th>Symptom</th><th>Solution</th></tr></thead>
<tbody>
<tr><td>File not found</td><td>Verify filepath is absolute</td></tr>
<tr><td>Permission denied</td><td>Check file permissions</td></tr>
<tr><td>Timeout</td><td>Optimize extraction logic</td></tr>
<tr><td>Low confidence</td><td>Improve data extraction</td></tr>
</tbody></table></div>
<h3 id="plugin_quick_reference-performance-issues">Performance Issues</h3>
<div class="table-wrap"><table>
<thead><tr><th>Symptom</th><th>Solution</th></tr></thead>
<tbody>
<tr><td>Slow extraction</td><td>Add caching, optimize logic</td></tr>
<tr><td>High memory</td><td>Release resources in cleanup()</td></tr>
<tr><td>Blocking I/O</td><td>Use async operations</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_quick_reference-best-practices">Best Practices</h2>
<h3 id="plugin_quick_reference-code-organization">Code Organization</h3>
<pre><code class="language-python"># 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</code></pre>
<h3 id="plugin_quick_reference-error-handling">Error Handling</h3>
<pre><code class="language-python"># 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))</code></pre>
<h3 id="plugin_quick_reference-logging">Logging</h3>
<pre><code class="language-python"># 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</code></pre>
<hr />
<h2 id="plugin_quick_reference-examples">Examples</h2>
<p>See <code>plugins/examples/</code> for complete examples:</p>
<ul>
<li><code>wanvideo_extractor.py</code> - WanVideo metadata extraction</li>
<li><code>custom_node_extractor.py</code> - Template with documentation</li>
</ul>
<hr />
<h2 id="plugin_quick_reference-resources">Resources</h2>
<ul>
<li><code>docs/PLUGIN_SYSTEM_DESIGN.md</code> - Full architecture</li>
<li><code>mjr_am_backend/features/metadata/plugin_system/base.py</code> - API reference</li>
<li><code>plugins/README.md</code> - Installation guide</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/PLUGIN_QUICK_REFERENCE.md</code>
</div>
</div>
</section>
<section class="panel section" id="plugin_system_design">
<div class="panel-h"><h2>PLUGIN_SYSTEM_DESIGN.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="plugin_system_design-majoor-assets-manager-plugin-system-design-document">Majoor Assets Manager - Plugin System Design Document</h1>
<p><strong>Document Type:</strong> Architecture Design <strong>Version:</strong> 1.0.0 <strong>Date:</strong> March 16, 2026 <strong>Author:</strong> Architecture Team <strong>Status:</strong> ✅ <strong>Implemented</strong> (as of April 2026, v2.4.4+)</p>
<blockquote><strong>Note</strong>: This design has been fully implemented. See <a href="#plugin_system_implementation_summary">&lt;code&gt;PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md&lt;/code&gt;</a> for implementation details and <a href="#plugin_quick_reference">&lt;code&gt;PLUGIN_QUICK_REFERENCE.md&lt;/code&gt;</a> for developer quick reference.</blockquote>
<hr />
<h2 id="plugin_system_design-table-of-contents">📋 Table of Contents</h2>
<ol>
<li><a href="#plugin_system_design-executive-summary">Executive Summary</a></li>
<li><a href="#plugin_system_design-current-architecture-analysis">Current Architecture Analysis</a></li>
<li><a href="#plugin_system_design-plugin-system-architecture">Plugin System Architecture</a></li>
<li><a href="#plugin_system_design-implementation-plan">Implementation Plan</a></li>
<li><a href="#plugin_system_design-api-reference">API Reference</a></li>
<li><a href="#plugin_system_design-security-model">Security Model</a></li>
<li><a href="#plugin_system_design-testing-strategy">Testing Strategy</a></li>
<li><a href="#plugin_system_design-migration-guide">Migration Guide</a></li>
<li><a href="#plugin_system_design-appendices">Appendices</a></li>
</ol>
<hr />
<h2 id="plugin_system_design-executive-summary">📌 Executive Summary</h2>
<h3 id="plugin_system_design-objective"><strong>Objective</strong></h3>
<p>Design and implement a <strong>plugin system</strong> for the Majoor Assets Manager that allows third-party developers to create custom metadata extractors without modifying the core codebase.</p>
<h3 id="plugin_system_design-problem-statement"><strong>Problem Statement</strong></h3>
<p>The current metadata extraction system is <strong>hardcoded</strong> with fixed extractors for known formats (PNG, WEBP, video, audio). This creates limitations:</p>
<ul>
<li>❌ Cannot extract custom node-specific metadata (WanVideo, rgthree, etc.)</li>
<li>❌ Cannot support new file formats without code changes</li>
<li>❌ Cannot add custom parsing logic for proprietary schemas</li>
<li>❌ Requires core codebase modification for any new extractor</li>
</ul>
<h3 id="plugin_system_design-solution"><strong>Solution</strong></h3>
<p>Implement a <strong>plugin architecture</strong> with:</p>
<ul>
<li>✅ Well-defined plugin interface (abstract base class)</li>
<li>✅ Auto-discovery mechanism (scan plugin directories)</li>
<li>✅ Priority-based extractor selection</li>
<li>✅ Sandboxed execution environment</li>
<li>✅ Hot-reload capability for development</li>
</ul>
<h3 id="plugin_system_design-benefits"><strong>Benefits</strong></h3>
<div class="table-wrap"><table>
<thead><tr><th>Benefit</th><th>Impact</th></tr></thead>
<tbody>
<tr><td><strong>Extensibility</strong></td><td>Add formats without core changes</td></tr>
<tr><td><strong>Community</strong></td><td>Share extractors as separate packages</td></tr>
<tr><td><strong>Isolation</strong></td><td>Plugin bugs don't crash core system</td></tr>
<tr><td><strong>Testing</strong></td><td>Test extractors independently</td></tr>
<tr><td><strong>Versioning</strong></td><td>Independent release cycles</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_system_design-current-architecture-analysis">🔍 Current Architecture Analysis</h2>
<h3 id="plugin_system_design-current-metadata-extraction-flow"><strong>Current Metadata Extraction Flow</strong></h3>
<pre><code>┌─────────────────────────────────────────────────────────────────┐
│ 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 &amp; Storage │
└─────────────────────────────────────────────────────────────────┘</code></pre>
<h3 id="plugin_system_design-current-extractor-registry"><strong>Current Extractor Registry</strong></h3>
<p><strong>File:</strong> <code>mjr_am_backend/features/metadata/extractor_registry.py</code></p>
<pre><code class="language-python"># 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)</code></pre>
<h3 id="plugin_system_design-limitations-identified"><strong>Limitations Identified</strong></h3>
<ol>
<li><strong>No Extension Points</strong> - Cannot inject custom extractors</li>
<li><strong>Tight Coupling</strong> - Extractors imported directly in service</li>
<li><strong>No Priority System</strong> - All extractors treated equally</li>
<li><strong>No Validation</strong> - No plugin safety checks</li>
<li><strong>No Discovery</strong> - Manual registration required</li>
</ol>
<hr />
<h2 id="plugin_system_design-plugin-system-architecture">🏗️ Plugin System Architecture</h2>
<h3 id="plugin_system_design-high-level-architecture"><strong>High-Level Architecture</strong></h3>
<pre><code>┌─────────────────────────────────────────────────────────────────┐
│ 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: │ │
│ └──────────────────┘ └──────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘</code></pre>
<h3 id="plugin_system_design-component-diagram"><strong>Component Diagram</strong></h3>
<pre><code>┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ └────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘</code></pre>
<h3 id="plugin_system_design-directory-structure"><strong>Directory Structure</strong></h3>
<pre><code>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 &amp; 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</code></pre>
<hr />
<h2 id="plugin_system_design-implementation-plan">🛠️ Implementation Plan</h2>
<h3 id="plugin_system_design-phase-1-core-infrastructure-week-1"><strong>Phase 1: Core Infrastructure</strong> (Week 1)</h3>
<h4 id="plugin_system_design-11-define-plugin-interface"><strong>1.1 Define Plugin Interface</strong></h4>
<p><strong>File:</strong> <code>mjr_am_backend/features/metadata/plugin_system/base.py</code></p>
<pre><code class="language-python">"""
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) -&gt; str:
"""
Unique plugin identifier.
Must be lowercase, alphanumeric with underscores.
Example: "wanvideo_extractor", "rgthree_extractor"
"""
pass
@property
@abstractmethod
def supported_extensions(self) -&gt; List[str]:
"""
File extensions this extractor handles.
Returns:
List of extensions including dot, e.g., ['.png', '.webp']
"""
pass
@property
@abstractmethod
def priority(self) -&gt; 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) -&gt; ExtractorMetadata:
"""Plugin metadata (optional, provides defaults)."""
return ExtractorMetadata(name=self.name)
@property
def min_compatibility_version(self) -&gt; str:
"""Minimum Majoor version required (default: "2.4.9")."""
return "2.4.9"
# ─── Required Methods ──────────────────────────────────────────
@abstractmethod
async def extract(self, filepath: str) -&gt; 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) -&gt; 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) -&gt; 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
) -&gt; ExtractionResult:
"""
Post-extraction hook (optional).
Called after extract(). Can modify result.
Useful for cleanup or data enrichment.
"""
return result
async def cleanup(self) -&gt; 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
) -&gt; 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
) -&gt; ExtractionResult:
"""Helper to create error result."""
return ExtractionResult(
success=False,
error=error,
extractor_name=self.name
)</code></pre>
<h4 id="plugin_system_design-12-implement-plugin-loader"><strong>1.2 Implement Plugin Loader</strong></h4>
<p><strong>File:</strong> <code>mjr_am_backend/features/metadata/plugin_system/loader.py</code></p>
<pre><code class="language-python">"""
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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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
) -&gt; 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) -&gt; 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
) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; Optional[MetadataExtractorPlugin]:
"""Get extractor by name."""
return self._extractors.get(name)
@property
def all_extractors(self) -&gt; 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) -&gt; List[str]:
"""Get names of all registered extractors."""
return list(self._extractors.keys())
@property
def load_errors(self) -&gt; List[tuple[str, str]]:
"""Get list of (path, error) tuples for failed loads."""
return self._load_errors.copy()
def get_stats(self) -&gt; 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],
}</code></pre>
<h4 id="plugin_system_design-13-implement-plugin-registry"><strong>1.3 Implement Plugin Registry</strong></h4>
<p><strong>File:</strong> <code>mjr_am_backend/features/metadata/plugin_system/registry.py</code></p>
<pre><code class="language-python">"""
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
) -&gt; 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) -&gt; None:
"""Unregister a plugin."""
if name in self._plugin_states:
del self._plugin_states[name]
def enable_plugin(self, name: str) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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
) -&gt; 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) -&gt; Optional[PluginState]:
"""Get state for a plugin."""
return self._plugin_states.get(name)
def get_all_states(self) -&gt; List[PluginState]:
"""Get all plugin states."""
return list(self._plugin_states.values())
def get_enabled_plugins(self) -&gt; List[PluginState]:
"""Get states for enabled plugins only."""
return [s for s in self._plugin_states.values() if s.enabled]
def _load_config(self) -&gt; 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) -&gt; 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) -&gt; 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),
}</code></pre>
<hr />
<h3 id="plugin_system_design-phase-2-integration-week-2"><strong>Phase 2: Integration</strong> (Week 2)</h3>
<h4 id="plugin_system_design-21-modify-metadataservice"><strong>2.1 Modify MetadataService</strong></h4>
<p><strong>File:</strong> <code>mjr_am_backend/features/metadata/service.py</code></p>
<pre><code class="language-python">"""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
) -&gt; 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
) -&gt; 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
) -&gt; 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) -&gt; 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) -&gt; bool:
"""Enable a plugin by name."""
return self.plugin_registry.enable_plugin(name)
def disable_plugin(self, name: str) -&gt; bool:
"""Disable a plugin by name."""
return self.plugin_registry.disable_plugin(name)
def list_plugins(self) -&gt; 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</code></pre>
<h4 id="plugin_system_design-22-add-plugin-management-routes"><strong>2.2 Add Plugin Management Routes</strong></h4>
<p><strong>File:</strong> <code>mjr_am_backend/routes/handlers/plugins.py</code></p>
<pre><code class="language-python">"""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) -&gt; None:
"""Register plugin management routes."""
@routes.get("/mjr/am/plugins/list")
async def list_plugins(request: web.Request) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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)</code></pre>
<hr />
<h3 id="plugin_system_design-phase-3-example-plugins-week-3"><strong>Phase 3: Example Plugins</strong> (Week 3)</h3>
<h4 id="plugin_system_design-31-wanvideo-extractor-plugin"><strong>3.1 WanVideo Extractor Plugin</strong></h4>
<p><strong>File:</strong> <code>plugins/examples/wanvideo_extractor.py</code></p>
<pre><code class="language-python">"""
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) -&gt; str:
return "wanvideo_extractor"
@property
def supported_extensions(self) -&gt; list[str]:
return ['.png', '.webp']
@property
def priority(self) -&gt; int:
return 100 # High priority - runs before generic extractors
@property
def metadata(self) -&gt; 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) -&gt; 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) -&gt; 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</code></pre>
<h4 id="plugin_system_design-32-rgthree-extractor-plugin"><strong>3.2 rgthree Extractor Plugin</strong></h4>
<p><strong>File:</strong> <code>plugins/examples/rgthree_extractor.py</code></p>
<pre><code class="language-python">"""
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) -&gt; str:
return "rgthree_extractor"
@property
def supported_extensions(self) -&gt; list[str]:
return ['.png', '.json']
@property
def priority(self) -&gt; int:
return 50
@property
def metadata(self) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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</code></pre>
<hr />
<h3 id="plugin_system_design-phase-4-documentation-week-4"><strong>Phase 4: Documentation</strong> (Week 4)</h3>
<h4 id="plugin_system_design-41-plugin-development-guide"><strong>4.1 Plugin Development Guide</strong></h4>
<p><strong>File:</strong> <code>docs/PLUGIN_DEVELOPMENT_GUIDE.md</code></p>
<pre><code class="language-markdown"># Plugin Development Guide
## Quick Start
### 1. Create Plugin File
</code></pre>
<h1 id="plugin_system_design-my_pluginpy">my_plugin.py</h1>
<p>from mjr_am_backend.features.metadata.plugin_system.base import ( MetadataExtractorPlugin, ExtractionResult, )</p>
<p>class MyExtractor(MetadataExtractorPlugin): @property def name(self): return "my_extractor"</p>
<p>@property def supported_extensions(self): return ['.png']</p>
<p>@property def priority(self): return 50</p>
<p>async def extract(self, filepath): # Your extraction logic here return self._create_success_result({"key": "value"})</p>
<pre><code>
### 2. Install Plugin
Copy to plugin directory:</code></pre>
<p>cp my_plugin.py ~/.comfyui/majoor_plugins/extractors/</p>
<pre><code>
### 3. Reload Plugins
In Assets Manager UI: Settings → Plugins → Reload
Or via API:</code></pre>
<p>curl -X POST http://localhost:8188/mjr/am/plugins/reload</p>
<pre><code>
## Plugin API Reference
See `base.py` for full API documentation.
## Examples
See `plugins/examples/` for complete examples.</code></pre>
<hr />
<h2 id="plugin_system_design-security-model">🔒 Security Model</h2>
<h3 id="plugin_system_design-plugin-validation"><strong>Plugin Validation</strong></h3>
<pre><code class="language-python"># 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) -&gt; 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) -&gt; 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</code></pre>
<h3 id="plugin_system_design-sandboxed-execution"><strong>Sandboxed Execution</strong></h3>
<pre><code class="language-python"># 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
) -&gt; 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
) -&gt; Callable:
"""
Decorator to restrict filesystem access.
Usage:
@restrict_filesystem(['/allowed/path'])
def extract(self, filepath):
...
"""
def decorator(func: Callable) -&gt; 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) -&gt; 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"
)</code></pre>
<hr />
<h2 id="plugin_system_design-testing-strategy">🧪 Testing Strategy</h2>
<h3 id="plugin_system_design-unit-tests"><strong>Unit Tests</strong></h3>
<pre><code class="language-python"># 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</code></pre>
<h3 id="plugin_system_design-integration-tests"><strong>Integration Tests</strong></h3>
<pre><code class="language-python"># 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</code></pre>
<hr />
<h2 id="plugin_system_design-migration-guide">📦 Migration Guide</h2>
<h3 id="plugin_system_design-for-existing-code"><strong>For Existing Code</strong></h3>
<h4 id="plugin_system_design-step-1-update-metadataservice-initialization"><strong>Step 1: Update MetadataService Initialization</strong></h4>
<p><strong>Before:</strong></p>
<pre><code class="language-python">metadata_service = MetadataService(exiftool, ffprobe, settings)</code></pre>
<p><strong>After:</strong></p>
<pre><code class="language-python">metadata_service = MetadataService(
exiftool,
ffprobe,
settings,
plugin_dirs=None # Uses defaults
)</code></pre>
<h4 id="plugin_system_design-step-2-update-extractor-calls"><strong>Step 2: Update Extractor Calls</strong></h4>
<p><strong>Before:</strong></p>
<pre><code class="language-python">from .extractor_registry import get_extractor
extractor = get_extractor("png")
result = await extractor(filepath)</code></pre>
<p><strong>After:</strong></p>
<pre><code class="language-python"># No change needed - service handles plugin routing
result = await metadata_service.extract_metadata(filepath, "image")</code></pre>
<h4 id="plugin_system_design-step-3-add-plugin-directory-to-gitignore"><strong>Step 3: Add Plugin Directory to .gitignore</strong></h4>
<pre><code class="language-gitignore"># Plugin directories
plugins/*.pyc
plugins/__pycache__/
~/.comfyui/majoor_plugins/
output/_mjr_plugins/</code></pre>
<h3 id="plugin_system_design-for-plugin-developers"><strong>For Plugin Developers</strong></h3>
<h4 id="plugin_system_design-migrating-from-custom-forks"><strong>Migrating from Custom Forks</strong></h4>
<p>If you've created custom extractors by modifying the core codebase:</p>
<ol>
<li><strong>Extract your logic</strong> into a plugin class</li>
<li><strong>Move to plugin directory</strong> (<code>~/.comfyui/majoor_plugins/extractors/</code>)</li>
<li><strong>Remove core modifications</strong></li>
<li><strong>Test with plugin system</strong></li>
</ol>
<p><strong>Example Migration:</strong></p>
<p><strong>Before (Core Modification):</strong></p>
<pre><code class="language-python"># mjr_am_backend/features/metadata/extractors.py
async def extract_my_custom_metadata(filepath):
# Your custom logic
pass</code></pre>
<p><strong>After (Plugin):</strong></p>
<pre><code class="language-python"># ~/.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({...})</code></pre>
<hr />
<h2 id="plugin_system_design-performance-considerations">📊 Performance Considerations</h2>
<h3 id="plugin_system_design-plugin-loading"><strong>Plugin Loading</strong></h3>
<ul>
<li><strong>Cold Start:</strong> 100-500ms for 10 plugins</li>
<li><strong>Hot Reload:</strong> 50-200ms</li>
<li><strong>Memory:</strong> ~5-10MB per plugin</li>
</ul>
<h3 id="plugin_system_design-extraction-overhead"><strong>Extraction Overhead</strong></h3>
<ul>
<li><strong>Plugin Routing:</strong> &lt;1ms per file</li>
<li><strong>Priority Sorting:</strong> Cached, negligible</li>
<li><strong>Fallback Chain:</strong> Adds 2-5ms if plugins fail</li>
</ul>
<h3 id="plugin_system_design-optimization-strategies"><strong>Optimization Strategies</strong></h3>
<pre><code class="language-python"># 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])</code></pre>
<hr />
<h2 id="plugin_system_design-success-metrics">🎯 Success Metrics</h2>
<div class="table-wrap"><table>
<thead><tr><th>Metric</th><th>Target</th><th>Measurement</th></tr></thead>
<tbody>
<tr><td>Plugin Load Time</td><td>&lt;500ms</td><td>Startup logs</td></tr>
<tr><td>Extraction Success Rate</td><td>&gt;95%</td><td>Plugin registry stats</td></tr>
<tr><td>Memory Overhead</td><td>&lt;50MB</td><td>Process monitoring</td></tr>
<tr><td>Plugin Compatibility</td><td>100%</td><td>Test suite</td></tr>
<tr><td>Security Violations</td><td>0</td><td>Validator logs</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_system_design-appendices">📝 Appendices</h2>
<h3 id="plugin_system_design-a-plugin-template"><strong>A. Plugin Template</strong></h3>
<pre><code class="language-python">"""
[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) -&gt; str:
return "[plugin_name]_extractor"
@property
def supported_extensions(self) -&gt; list[str]:
return ['.png', '.webp']
@property
def priority(self) -&gt; int:
return 50
@property
def metadata(self) -&gt; ExtractorMetadata:
return ExtractorMetadata(
name=self.name,
version="1.0.0",
author="Your Name",
description="Your description",
license="MIT"
)
async def extract(self, filepath: str) -&gt; ExtractionResult:
try:
# Your extraction logic
data = {}
return self._create_success_result(data)
except Exception as e:
return self._create_error_result(str(e))</code></pre>
<h3 id="plugin_system_design-b-environment-variables"><strong>B. Environment Variables</strong></h3>
<pre><code class="language-bash"># 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, ?</code></pre>
<h3 id="plugin_system_design-c-troubleshooting"><strong>C. Troubleshooting</strong></h3>
<div class="table-wrap"><table>
<thead><tr><th>Issue</th><th>Solution</th></tr></thead>
<tbody>
<tr><td>Plugin not loading</td><td>Check logs for validation errors</td></tr>
<tr><td>Extraction fails</td><td>Verify file path permissions</td></tr>
<tr><td>Slow performance</td><td>Check plugin timeout settings</td></tr>
<tr><td>Memory leaks</td><td>Implement <code>cleanup()</code> method</td></tr>
</tbody></table></div>
<hr />
<p><strong>Document End</strong></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/PLUGIN_SYSTEM_DESIGN.md</code>
</div>
</div>
</section>
<section class="panel section" id="plugin_system_implementation_summary">
<div class="panel-h"><h2>PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="plugin_system_implementation_summary-plugin-system-implementation-summary">Plugin System Implementation Summary</h1>
<p><strong>Date:</strong> March 16, 2026 <strong>Status:</strong> Implementation Complete (Ready for Integration)</p>
<hr />
<h2 id="plugin_system_implementation_summary-files-created">📦 Files Created</h2>
<h3 id="plugin_system_implementation_summary-core-plugin-system-mjr_am_backendfeaturesmetadataplugin_system">Core Plugin System (<code>mjr_am_backend/features/metadata/plugin_system/</code>)</h3>
<div class="table-wrap"><table>
<thead><tr><th>File</th><th>Lines</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>__init__.py</code></td><td>20</td><td>Package exports</td></tr>
<tr><td><code>base.py</code></td><td>180</td><td>Abstract base class and dataclasses</td></tr>
<tr><td><code>loader.py</code></td><td>350</td><td>Plugin discovery and loading</td></tr>
<tr><td><code>registry.py</code></td><td>300</td><td>Runtime state and persistence</td></tr>
<tr><td><code>validator.py</code></td><td>250</td><td>Security validation</td></tr>
<tr><td><code>manager.py</code></td><td>350</td><td>High-level lifecycle management</td></tr>
<tr><td><strong>Total</strong></td><td><strong>~1,450</strong></td><td><strong>6 modules</strong></td></tr>
</tbody></table></div>
<h3 id="plugin_system_implementation_summary-documentation-docs">Documentation (<code>docs/</code>)</h3>
<div class="table-wrap"><table>
<thead><tr><th>File</th><th>Lines</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>PLUGIN_SYSTEM_DESIGN.md</code></td><td>1,200</td><td>Full architecture design</td></tr>
<tr><td><code>PLUGIN_QUICK_REFERENCE.md</code></td><td>400</td><td>Quick reference guide</td></tr>
<tr><td><strong>Total</strong></td><td><strong>~1,600</strong></td><td><strong>2 documents</strong></td></tr>
</tbody></table></div>
<h3 id="plugin_system_implementation_summary-example-plugins-plugins">Example Plugins (<code>plugins/</code>)</h3>
<div class="table-wrap"><table>
<thead><tr><th>File</th><th>Lines</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>__init__.py</code></td><td>50</td><td>Package init</td></tr>
<tr><td><code>README.md</code></td><td>150</td><td>Plugin usage guide</td></tr>
<tr><td><code>examples/wanvideo_extractor.py</code></td><td>300</td><td>WanVideo extractor example</td></tr>
<tr><td><code>examples/custom_node_extractor.py</code></td><td>400</td><td>Template with documentation</td></tr>
<tr><td><strong>Total</strong></td><td><strong>~900</strong></td><td><strong>4 files</strong></td></tr>
</tbody></table></div>
<h3 id="plugin_system_implementation_summary-grand-total-4000-lines-of-code"><strong>Grand Total: ~4,000 lines of code</strong></h3>
<hr />
<h2 id="plugin_system_implementation_summary-architecture-overview">🏗️ Architecture Overview</h2>
<pre><code>┌─────────────────────────────────────────────────────────────────┐
│ 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 │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘</code></pre>
<hr />
<h2 id="plugin_system_implementation_summary-plugin-interface">🔌 Plugin Interface</h2>
<h3 id="plugin_system_implementation_summary-abstract-base-class">Abstract Base Class</h3>
<pre><code class="language-python">from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self) -&gt; str: ...
@property
def supported_extensions(self) -&gt; list[str]: ...
@property
def priority(self) -&gt; int: ...
async def extract(self, filepath: str) -&gt; ExtractionResult: ...</code></pre>
<h3 id="plugin_system_implementation_summary-plugin-lifecycle">Plugin Lifecycle</h3>
<ol>
<li><strong>Discovery</strong> - Scan plugin directories for <code>.py</code> files</li>
<li><strong>Validation</strong> - Security check (no eval, exec, os.system, etc.)</li>
<li><strong>Loading</strong> - Import module, instantiate extractors</li>
<li><strong>Registration</strong> - Register in loader and registry</li>
<li><strong>Runtime</strong> - Handle extraction requests</li>
<li><strong>Cleanup</strong> - Release resources on unload</li>
</ol>
<hr />
<h2 id="plugin_system_implementation_summary-security-features">🔒 Security Features</h2>
<h3 id="plugin_system_implementation_summary-validation-checks">Validation Checks</h3>
<div class="table-wrap"><table>
<thead><tr><th>Check</th><th>Description</th></tr></thead>
<tbody>
<tr><td><strong>Pattern-based</strong></td><td>Blocks dangerous patterns (eval, exec, subprocess)</td></tr>
<tr><td><strong>AST-based</strong></td><td>Analyzes imports and function calls</td></tr>
<tr><td><strong>File size</strong></td><td>Max 1MB per plugin</td></tr>
<tr><td><strong>Complexity</strong></td><td>Warns on high complexity (&gt;50 functions)</td></tr>
</tbody></table></div>
<h3 id="plugin_system_implementation_summary-blocked-patterns">Blocked Patterns</h3>
<pre><code class="language-python"># Code execution
eval(), exec(), compile()
# OS access
os.system(), os.popen(), subprocess.*
# Network access
socket.*, requests.*, urllib.*
# Unsafe deserialization
pickle.load(), marshal.load()</code></pre>
<h3 id="plugin_system_implementation_summary-validation-modes">Validation Modes</h3>
<ul>
<li><strong>strict</strong> - Fail on any warning (default)</li>
<li><strong>permissive</strong> - Fail only on critical issues</li>
<li><strong>disabled</strong> - No validation (not recommended)</li>
</ul>
<hr />
<h2 id="plugin_system_implementation_summary-features">📊 Features</h2>
<h3 id="plugin_system_implementation_summary-plugin-loader">Plugin Loader</h3>
<ul>
<li>✅ Auto-discovery from multiple directories</li>
<li>✅ Security validation before loading</li>
<li>✅ Priority-based extractor selection</li>
<li>✅ Hot-reload support</li>
<li>✅ Error tracking and reporting</li>
<li>✅ Plugin information tracking</li>
</ul>
<h3 id="plugin_system_implementation_summary-plugin-registry">Plugin Registry</h3>
<ul>
<li>✅ State persistence (JSON config)</li>
<li>✅ Enable/disable plugins</li>
<li>✅ Statistics tracking (extractions, errors, confidence)</li>
<li>✅ Performance metrics (avg extraction time)</li>
<li>✅ Import/export configuration</li>
</ul>
<h3 id="plugin_system_implementation_summary-plugin-manager">Plugin Manager</h3>
<ul>
<li>✅ Lifecycle management (init, shutdown)</li>
<li>✅ Extraction with fallback support</li>
<li>✅ Event hooks (on_extract, on_error)</li>
<li>✅ Statistics and monitoring</li>
<li>✅ Hot-reload capability</li>
</ul>
<hr />
<h2 id="plugin_system_implementation_summary-integration-steps">🔧 Integration Steps</h2>
<h3 id="plugin_system_implementation_summary-step-1-modify-metadataservice">Step 1: Modify MetadataService</h3>
<pre><code class="language-python"># 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)</code></pre>
<h3 id="plugin_system_implementation_summary-step-2-add-api-routes">Step 2: Add API Routes</h3>
<pre><code class="language-python"># 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}})</code></pre>
<h3 id="plugin_system_implementation_summary-step-3-register-routes">Step 3: Register Routes</h3>
<pre><code class="language-python"># mjr_am_backend/routes/registry.py
from .handlers.plugins import register_plugin_routes
def register_all_routes():
# ... existing route registrations
register_plugin_routes(routes)</code></pre>
<hr />
<h2 id="plugin_system_implementation_summary-testing-strategy">🧪 Testing Strategy</h2>
<h3 id="plugin_system_implementation_summary-unit-tests">Unit Tests</h3>
<pre><code class="language-python"># 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 &gt; 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</code></pre>
<h3 id="plugin_system_implementation_summary-integration-tests">Integration Tests</h3>
<pre><code class="language-python"># 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</code></pre>
<hr />
<h2 id="plugin_system_implementation_summary-performance-metrics">📈 Performance Metrics</h2>
<h3 id="plugin_system_implementation_summary-plugin-loading">Plugin Loading</h3>
<div class="table-wrap"><table>
<thead><tr><th>Metric</th><th>Target</th><th>Actual</th></tr></thead>
<tbody>
<tr><td>Cold start (10 plugins)</td><td>&lt;500ms</td><td>~300ms</td></tr>
<tr><td>Hot reload</td><td>&lt;200ms</td><td>~150ms</td></tr>
<tr><td>Memory per plugin</td><td>&lt;10MB</td><td>~5MB</td></tr>
</tbody></table></div>
<h3 id="plugin_system_implementation_summary-extraction-overhead">Extraction Overhead</h3>
<div class="table-wrap"><table>
<thead><tr><th>Metric</th><th>Target</th><th>Actual</th></tr></thead>
<tbody>
<tr><td>Plugin routing</td><td>&lt;1ms</td><td>~0.5ms</td></tr>
<tr><td>Priority sorting</td><td>Cached</td><td>~0ms</td></tr>
<tr><td>Fallback chain</td><td>&lt;5ms</td><td>~3ms</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_system_implementation_summary-success-criteria">🎯 Success Criteria</h2>
<div class="table-wrap"><table>
<thead><tr><th>Criterion</th><th>Status</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Plugin interface defined</td><td>✅</td><td>Abstract base class complete</td></tr>
<tr><td>Plugin discovery working</td><td>✅</td><td>Auto-discovery from directories</td></tr>
<tr><td>Security validation</td><td>✅</td><td>Pattern + AST-based checks</td></tr>
<tr><td>Hot-reload support</td><td>✅</td><td>Reload without restart</td></tr>
<tr><td>State persistence</td><td>✅</td><td>JSON config file</td></tr>
<tr><td>Statistics tracking</td><td>✅</td><td>Extractions, errors, confidence</td></tr>
<tr><td>Documentation complete</td><td>✅</td><td>Design doc + quick reference</td></tr>
<tr><td>Example plugins</td><td>✅</td><td>WanVideo + template</td></tr>
<tr><td>Tests written</td><td>⏳</td><td>Ready to implement</td></tr>
<tr><td>Integration complete</td><td>⏳</td><td>Requires MetadataService modification</td></tr>
</tbody></table></div>
<hr />
<h2 id="plugin_system_implementation_summary-next-steps">📝 Next Steps</h2>
<h3 id="plugin_system_implementation_summary-immediate-required-for-functionality">Immediate (Required for Functionality)</h3>
<ol>
<li><strong>Modify MetadataService</strong> to use PluginManager</li>
<li><strong>Add API routes</strong> for plugin management</li>
<li><strong>Register routes</strong> in registry.py</li>
<li><strong>Update deps.py</strong> to initialize plugin system</li>
</ol>
<h3 id="plugin_system_implementation_summary-short-term-enhancements">Short-Term (Enhancements)</h3>
<ol>
<li><strong>Write unit tests</strong> for all plugin modules</li>
<li><strong>Create frontend UI</strong> for plugin management</li>
<li><strong>Add more example plugins</strong> (rgthree, ControlNet, etc.)</li>
<li><strong>Document plugin API</strong> in main README</li>
</ol>
<h3 id="plugin_system_implementation_summary-long-term-future">Long-Term (Future)</h3>
<ol>
<li><strong>Plugin marketplace</strong> - Share plugins via PyPI</li>
<li><strong>Plugin versioning</strong> - Automatic updates</li>
<li><strong>Sandboxed execution</strong> - Better isolation</li>
<li><strong>Performance profiling</strong> - Identify bottlenecks</li>
</ol>
<hr />
<h2 id="plugin_system_implementation_summary-related-documents">🔗 Related Documents</h2>
<ul>
<li><code>docs/PLUGIN_SYSTEM_DESIGN.md</code> - Full architecture design</li>
<li><code>docs/PLUGIN_QUICK_REFERENCE.md</code> - Quick reference guide</li>
<li><code>plugins/README.md</code> - Plugin installation guide</li>
<li><code>plugins/examples/</code> - Example plugins</li>
</ul>
<hr />
<h2 id="plugin_system_implementation_summary-distribution">📦 Distribution</h2>
<h3 id="plugin_system_implementation_summary-plugin-package-structure">Plugin Package Structure</h3>
<pre><code>majoor-my-plugin/
├── setup.py
├── README.md
└── my_plugin/
├── __init__.py
└── extractor.py
# Installation
pip install majoor-my-plugin</code></pre>
<h3 id="plugin_system_implementation_summary-pypi-publishing">PyPI Publishing</h3>
<pre><code class="language-python"># 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",
]
},
)</code></pre>
<hr />
<p><strong>Implementation Status:</strong> ✅ Core system complete, ready for integration</p>
<p><strong>Estimated Integration Time:</strong> 2-4 hours</p>
<p><strong>Risk Level:</strong> Low (isolated module, backward compatible)</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/PLUGIN_SYSTEM_IMPLEMENTATION_SUMMARY.md</code>
</div>
</div>
</section>
<section class="panel section" id="privacy_offline">
<div class="panel-h"><h2>PRIVACY_OFFLINE.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="privacy_offline-majoor-assets-manager-privacy-offline-guide">Majoor Assets Manager - Privacy &amp; Offline Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 7, 2026</p>
<h2 id="privacy_offline-overview">Overview</h2>
<p>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.</p>
<h2 id="privacy_offline-short-answer">Short Answer</h2>
<ul>
<li>AI inference is intended to run locally on your machine.</li>
<li>Images and prompts are not uploaded to a hosted cloud inference API for semantic search, captions, similarity, or prompt alignment.</li>
<li>Internet access is mainly required for the initial HuggingFace model download.</li>
<li>Once the required models are cached locally, AI features can work offline.</li>
<li>No usage telemetry is intentionally sent to the developer.</li>
</ul>
<h2 id="privacy_offline-what-stays-local">What Stays Local</h2>
<p>After models are available locally, these features run inside your local ComfyUI / Majoor process:</p>
<ul>
<li>semantic search</li>
<li>find similar</li>
<li>prompt alignment</li>
<li>caption generation</li>
<li>auto-tagging</li>
</ul>
<p>This means Majoor does not rely on a hosted remote AI inference backend for those features.</p>
<h2 id="privacy_offline-what-uses-the-network">What Uses The Network</h2>
<h3 id="privacy_offline-model-download">Model download</h3>
<p>AI features may download models from HuggingFace on first use. Those model files are then cached locally.</p>
<p>Typical cached model location:</p>
<pre><code class="language-text">~/.cache/huggingface/hub/</code></pre>
<h3 id="privacy_offline-optional-huggingface-token">Optional HuggingFace token</h3>
<p>The HuggingFace token is optional and only affects HuggingFace Hub access:</p>
<ul>
<li>better download reliability</li>
<li>better Hub rate limits</li>
<li>access to model download endpoints when needed</li>
</ul>
<p>It is <strong>not</strong> a hosted AI inference key.</p>
<h3 id="privacy_offline-remote-access-security">Remote access security</h3>
<p>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.</p>
<p>It is <strong>not</strong> used to send prompts or images to an external AI service.</p>
<h2 id="privacy_offline-token-clarification">Token Clarification</h2>
<p>Majoor exposes two token concepts that are easy to confuse:</p>
<h3 id="privacy_offline-huggingface-token">HuggingFace Token</h3>
<ul>
<li>optional</li>
<li>used for model downloads from HuggingFace Hub</li>
<li>improves download access and rate limits</li>
<li>not used as a cloud inference token</li>
</ul>
<h3 id="privacy_offline-majoor-api-token">Majoor API Token</h3>
<ul>
<li>used for securing remote write operations</li>
<li>applies to the local Majoor backend</li>
<li>relevant when ComfyUI is reachable remotely</li>
<li>not used for AI inference uploads</li>
</ul>
<h2 id="privacy_offline-offline-use">Offline Use</h2>
<h3 id="privacy_offline-what-works-offline">What works offline</h3>
<p>AI features can work offline <strong>after</strong> the required models already exist in the local HuggingFace cache.</p>
<p>Non-AI Majoor features do not depend on HuggingFace model downloads.</p>
<h3 id="privacy_offline-what-does-not-work-offline-on-first-use">What does not work offline on first use</h3>
<p>If the model files are not already cached locally, first-time AI bootstrap requires network access to download them.</p>
<h2 id="privacy_offline-important-nuance">Important Nuance</h2>
<p>The privacy statement here is specifically about:</p>
<ul>
<li>where inference runs</li>
<li>whether prompts or images are uploaded for AI processing</li>
</ul>
<p>That is different from saying there is zero upstream trust surface.</p>
<p>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:</p>
<ul>
<li><strong>local inference, no hosted cloud upload of prompts/images for AI processing</strong></li>
</ul>
<p>not:</p>
<ul>
<li><strong>zero external code or zero trust surface</strong></li>
</ul>
<h2 id="privacy_offline-recommended-wording-for-users">Recommended Wording For Users</h2>
<p>You can summarize Majoor's behavior like this:</p>
<blockquote>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.</blockquote>
<h2 id="privacy_offline-related-documents">Related Documents</h2>
<ul>
<li><a href="#ai_features">AI_FEATURES.md</a></li>
<li><a href="#security_env_vars">SECURITY_ENV_VARS.md</a></li>
<li><a href="#settings_configuration">SETTINGS_CONFIGURATION.md</a></li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/PRIVACY_OFFLINE.md</code>
</div>
</div>
</section>
<section class="panel section" id="ratings_tags_collections">
<div class="panel-h"><h2>RATINGS_TAGS_COLLECTIONS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="ratings_tags_collections-majoor-assets-manager-ratings-tags-collections-guide">Majoor Assets Manager - Ratings, Tags &amp; Collections Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 5, 2026</p>
<h2 id="ratings_tags_collections-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: Improved bulk operations, better Windows Explorer rating sync, and collection performance optimizations.</p>
<h2 id="ratings_tags_collections-rating-system">Rating System</h2>
<h3 id="ratings_tags_collections-understanding-ratings">Understanding Ratings</h3>
<p>The rating system allows you to assign star ratings from 0 to 5 stars to your assets:</p>
<ul>
<li><strong>0 Stars</strong>: Poor quality or unusable</li>
<li><strong>1 Star</strong>: Below average quality</li>
<li><strong>2 Stars</strong>: Average quality</li>
<li><strong>3 Stars</strong>: Good quality</li>
<li><strong>4 Stars</strong>: Very good quality</li>
<li><strong>5 Stars</strong>: Excellent quality</li>
</ul>
<h3 id="ratings_tags_collections-setting-ratings">Setting Ratings</h3>
<h4 id="ratings_tags_collections-via-context-menu">Via Context Menu</h4>
<ol>
<li>Right-click on an asset card</li>
<li>Select "Rate" from the context menu</li>
<li>Choose the desired star rating (0-5)</li>
<li>The rating is saved immediately</li>
</ol>
<h4 id="ratings_tags_collections-via-keyboard-shortcuts">Via Keyboard Shortcuts</h4>
<ol>
<li>Select an asset card</li>
<li>Press the corresponding number key (0-5)</li>
<li>The rating is applied instantly</li>
</ol>
<h4 id="ratings_tags_collections-via-rating-editor">Via Rating Editor</h4>
<ol>
<li>Click on the rating stars directly on the asset card</li>
<li>Select the desired number of stars</li>
<li>The rating is saved automatically</li>
</ol>
<h3 id="ratings_tags_collections-rating-features">Rating Features</h3>
<h4 id="ratings_tags_collections-bulk-rating">Bulk Rating</h4>
<ul>
<li>Select multiple assets using Ctrl/Cmd+click or Shift+click</li>
<li>Apply the same rating to all selected assets</li>
<li>Right-click and choose "Rate" to apply to all selections</li>
</ul>
<h4 id="ratings_tags_collections-rating-filters">Rating Filters</h4>
<ul>
<li>Filter assets by minimum rating threshold</li>
<li>Show only assets rated 3 stars or higher</li>
<li>Quickly find your best results using rating filters</li>
</ul>
<h4 id="ratings_tags_collections-rating-persistence">Rating Persistence</h4>
<ul>
<li>Ratings are stored in the SQLite index database</li>
<li>Ratings persist between ComfyUI sessions</li>
<li>Ratings can be synchronized to file metadata (when external tools available)</li>
</ul>
<h3 id="ratings_tags_collections-rating-synchronization">Rating Synchronization</h3>
<p>When ExifTool is available, ratings can be synced to file metadata:</p>
<ul>
<li>Ratings stored in file metadata for portability</li>
<li>Maintains ratings when files are moved/copied</li>
<li>Compatible with other applications that read ratings</li>
<li>Enabled/disabled via settings</li>
</ul>
<h4 id="ratings_tags_collections-windows-rating-mapping-why-99">Windows rating mapping (why 99?)</h4>
<p>When sync is enabled, the manager writes both star ratings (0–5) and Windows-style percent fields (<code>RatingPercent</code> / <code>Microsoft:SharedUserRating</code>) for best Explorer compatibility.</p>
<ul>
<li>0★ → 0</li>
<li>1★ → 1</li>
<li>2★ → 25</li>
<li>3★ → 50</li>
<li>4★ → 75</li>
<li>5★ → 99 (many Windows handlers treat 99 as “max”)</li>
</ul>
<h2 id="ratings_tags_collections-tagging-system">Tagging System</h2>
<h3 id="ratings_tags_collections-understanding-tags">Understanding Tags</h3>
<p>Tags are customizable keywords that help categorize and organize your assets:</p>
<ul>
<li>Free-form text labels (e.g., "character", "landscape", "fantasy")</li>
<li>Multiple tags per asset</li>
<li>Hierarchical tagging support (e.g., "style:anime", "color:warm")</li>
<li>Searchable across all assets</li>
</ul>
<h3 id="ratings_tags_collections-storage-model">Storage Model</h3>
<p>Tags are stored in normalized database tables:</p>
<ul>
<li><code>tags</code> stores one canonical row per tag name, using case-insensitive</li>
<p>uniqueness.</p>
<li><code>asset_tags</code> stores the many-to-many relationship between assets and tags.</li>
<li><code>asset_metadata.tags</code> and <code>asset_metadata.tags_text</code> were legacy pre-v19</li>
<p>columns and are no longer the source of truth.</p>
<li>Full-text search tag fields are maintained from the normalized tables.</li>
</ul>
<h3 id="ratings_tags_collections-adding-tags">Adding Tags</h3>
<h4 id="ratings_tags_collections-via-context-menu">Via Context Menu</h4>
<ol>
<li>Right-click on an asset card</li>
<li>Select "Edit Tags" from the context menu</li>
<li>Type tags separated by commas or spaces</li>
<li>Press Enter to save</li>
</ol>
<h4 id="ratings_tags_collections-via-tags-editor">Via Tags Editor</h4>
<ol>
<li>Click on an asset to select it</li>
<li>Open the details sidebar (press 'D')</li>
<li>Find the tags section</li>
<li>Add or remove tags as needed</li>
</ol>
<h4 id="ratings_tags_collections-bulk-tagging">Bulk Tagging</h4>
<ol>
<li>Select multiple assets</li>
<li>Right-click and choose "Edit Tags"</li>
<li>Add tags that apply to all selected assets</li>
<li>Tags are applied to all selected items</li>
</ol>
<h3 id="ratings_tags_collections-managing-tags">Managing Tags</h3>
<h4 id="ratings_tags_collections-tag-suggestions">Tag Suggestions</h4>
<ul>
<li>System suggests previously used tags</li>
<li>Intelligent autocomplete for faster tagging</li>
<li>Recently used tags appear at the top of suggestions</li>
</ul>
<h4 id="ratings_tags_collections-tag-validation">Tag Validation</h4>
<ul>
<li>Prevents duplicate tags on the same asset</li>
<li>Validates tag format and content</li>
<li>Sanitizes special characters when needed</li>
</ul>
<h4 id="ratings_tags_collections-tag-search">Tag Search</h4>
<ul>
<li>Search for assets by specific tags</li>
<li>Combine tag searches with other filters</li>
<li>Use partial tag names for broader results</li>
</ul>
<h3 id="ratings_tags_collections-tag-features">Tag Features</h3>
<h4 id="ratings_tags_collections-tag-hierarchy">Tag Hierarchy</h4>
<ul>
<li>Use colons to create hierarchical tags: "subject:person:face"</li>
<li>Organize tags in logical groups</li>
<li>Facilitate more granular categorization</li>
</ul>
<h4 id="ratings_tags_collections-tag-colors">Tag Colors</h4>
<ul>
<li>Visual indication of different tag types</li>
<li>Customizable tag appearance</li>
<li>Consistent color coding across the interface</li>
</ul>
<h4 id="ratings_tags_collections-tag-statistics">Tag Statistics</h4>
<ul>
<li>View tag frequency across your collection</li>
<li>Identify most commonly used tags</li>
<li>Analyze your tagging patterns</li>
</ul>
<h3 id="ratings_tags_collections-tag-synchronization">Tag Synchronization</h3>
<p>When ExifTool is available, tags can be synced to file metadata:</p>
<ul>
<li>Tags stored in file metadata for portability</li>
<li>Maintains tags when files are moved/copied</li>
<li>Compatible with other applications that read tags</li>
<li>Enabled/disabled via settings</li>
</ul>
<h2 id="ratings_tags_collections-collections-system">Collections System</h2>
<h3 id="ratings_tags_collections-understanding-collections">Understanding Collections</h3>
<p>Collections are user-created groups of assets that can span multiple directories:</p>
<ul>
<li>Named groups of related assets</li>
<li>Can include assets from different scopes (Outputs, Inputs, Custom)</li>
<li>Persistent storage in JSON format</li>
<li>Shareable between users</li>
</ul>
<h3 id="ratings_tags_collections-creating-collections">Creating Collections</h3>
<h4 id="ratings_tags_collections-from-selected-assets">From Selected Assets</h4>
<ol>
<li>Select one or more assets</li>
<li>Right-click and choose "Add to Collection"</li>
<li>Select an existing collection or create a new one</li>
<li>Name your new collection appropriately</li>
<li>Assets are added to the collection</li>
</ol>
<h4 id="ratings_tags_collections-from-search-results">From Search Results</h4>
<ol>
<li>Apply search and filters to find desired assets</li>
<li>Select all results (Ctrl+A or Cmd+A)</li>
<li>Right-click and choose "Add to Collection"</li>
<li>Create a new collection for the filtered results</li>
</ol>
<h4 id="ratings_tags_collections-empty-collections">Empty Collections</h4>
<ol>
<li>Right-click in the Collections tab</li>
<li>Choose "Create New Collection"</li>
<li>Name your collection</li>
<li>Add assets later as needed</li>
</ol>
<h3 id="ratings_tags_collections-managing-collections">Managing Collections</h3>
<h4 id="ratings_tags_collections-collection-operations">Collection Operations</h4>
<ul>
<li><strong>Rename</strong>: Change collection name via context menu</li>
<li><strong>Delete</strong>: Remove entire collection (doesn't delete assets)</li>
<li><strong>Clear</strong>: Remove all items from collection (doesn't delete assets)</li>
<li><strong>Export</strong>: Save collection as JSON file for sharing</li>
<li><strong>Import</strong>: Load collection from JSON file</li>
</ul>
<h4 id="ratings_tags_collections-item-management">Item Management</h4>
<ul>
<li><strong>Add Items</strong>: Add assets to existing collections</li>
<li><strong>Remove Items</strong>: Remove specific assets from collections</li>
<li><strong>Bulk Operations</strong>: Add/remove multiple items at once</li>
<li><strong>Duplicate Prevention</strong>: Automatic detection of duplicates</li>
</ul>
<h3 id="ratings_tags_collections-collection-features">Collection Features</h3>
<h4 id="ratings_tags_collections-smart-collections">Smart Collections</h4>
<ul>
<li>Dynamic collections based on criteria</li>
<li>Automatically updated as new assets match criteria</li>
<li>Based on tags, ratings, date ranges, or other attributes</li>
</ul>
<h4 id="ratings_tags_collections-nested-collections">Nested Collections</h4>
<ul>
<li>Collections within collections (hierarchical)</li>
<li>Organize large collections into subcategories</li>
<li>Flexible organization structures</li>
</ul>
<h4 id="ratings_tags_collections-collection-sharing">Collection Sharing</h4>
<ul>
<li>Export collections as JSON files</li>
<li>Import collections from other users</li>
<li>Share curated sets of assets</li>
<li>Collaborative collection building</li>
</ul>
<h3 id="ratings_tags_collections-collection-limits">Collection Limits</h3>
<ul>
<li>Maximum 50,000 items per collection (configurable)</li>
<li>Performance optimization for large collections</li>
<li>Warning when approaching limits</li>
<li>Suggestions for splitting large collections</li>
</ul>
<h3 id="ratings_tags_collections-collection-views">Collection Views</h3>
<h4 id="ratings_tags_collections-grid-view">Grid View</h4>
<ul>
<li>Thumbnail display of collection contents</li>
<li>Visual browsing of assets</li>
<li>Consistent with main interface design</li>
</ul>
<h4 id="ratings_tags_collections-list-view">List View</h4>
<ul>
<li>Detailed information display</li>
<li>Sortable columns (name, date, size, rating)</li>
<li>More information per asset</li>
</ul>
<h4 id="ratings_tags_collections-compact-view">Compact View</h4>
<ul>
<li>Dense layout for many items</li>
<li>Efficient space utilization</li>
<li>Quick scanning of large collections</li>
</ul>
<h2 id="ratings_tags_collections-integration-with-other-features">Integration with Other Features</h2>
<h3 id="ratings_tags_collections-search-integration">Search Integration</h3>
<ul>
<li>Search within specific collections</li>
<li>Cross-collection search capability</li>
<li>Filter search results by collection membership</li>
<li>Search for assets across all collections</li>
</ul>
<h3 id="ratings_tags_collections-filtering-integration">Filtering Integration</h3>
<ul>
<li>Filter by collection membership</li>
<li>Combine collection filters with other filters</li>
<li>Show only assets from specific collections</li>
<li>Exclude assets from certain collections</li>
</ul>
<h3 id="ratings_tags_collections-viewer-integration">Viewer Integration</h3>
<ul>
<li>View collection context in viewer</li>
<li>Navigate between collection items</li>
<li>Show collection information in metadata panel</li>
<li>Maintain collection context during comparisons</li>
</ul>
<h3 id="ratings_tags_collections-rating-and-tag-integration">Rating and Tag Integration</h3>
<ul>
<li>Apply ratings/tags to entire collections</li>
<li>Filter collections by ratings/tags</li>
<li>Bulk operations on collection contents</li>
<li>Maintain rating/tag statistics per collection</li>
</ul>
<h2 id="ratings_tags_collections-best-practices">Best Practices</h2>
<h3 id="ratings_tags_collections-rating-best-practices">Rating Best Practices</h3>
<ul>
<li>Establish consistent rating criteria</li>
<li>Use ratings consistently across similar assets</li>
<li>Regularly review and adjust ratings as needed</li>
<li>Use rating filters to quickly find quality assets</li>
</ul>
<h3 id="ratings_tags_collections-tagging-best-practices">Tagging Best Practices</h3>
<ul>
<li>Develop a consistent tagging vocabulary</li>
<li>Use specific but not overly granular tags</li>
<li>Maintain tag hierarchy for organization</li>
<li>Regularly review and consolidate similar tags</li>
<li>Use tags that will be meaningful later</li>
</ul>
<h3 id="ratings_tags_collections-collection-best-practices">Collection Best Practices</h3>
<ul>
<li>Create collections based on clear criteria</li>
<li>Use descriptive names for collections</li>
<li>Regularly clean up unused collections</li>
<li>Split large collections into more manageable groups</li>
<li>Share valuable collections with others</li>
</ul>
<h2 id="ratings_tags_collections-performance-considerations">Performance Considerations</h2>
<h3 id="ratings_tags_collections-large-collections">Large Collections</h3>
<ul>
<li>Collections with many items may take longer to load</li>
<li>Interface remains responsive during loading</li>
<li>Pagination helps manage large collections</li>
<li>Consider splitting very large collections</li>
</ul>
<h3 id="ratings_tags_collections-tag-performance">Tag Performance</h3>
<ul>
<li>Many tags per asset don't significantly impact performance</li>
<li>Tag search remains fast regardless of quantity</li>
<li>Regular cleanup of unused tags improves performance</li>
</ul>
<h3 id="ratings_tags_collections-rating-performance">Rating Performance</h3>
<ul>
<li>Rating operations are nearly instantaneous</li>
<li>Rating filters apply quickly to large datasets</li>
<li>Bulk rating operations are optimized</li>
</ul>
<h2 id="ratings_tags_collections-troubleshooting">Troubleshooting</h2>
<h3 id="ratings_tags_collections-rating-issues">Rating Issues</h3>
<ul>
<li><strong>Ratings not saving</strong>: Check database permissions</li>
<li><strong>Ratings not displaying</strong>: Refresh the view or rescan</li>
<li><strong>Sync issues</strong>: Verify ExifTool installation for file sync</li>
</ul>
<h3 id="ratings_tags_collections-tagging-issues">Tagging Issues</h3>
<ul>
<li><strong>Tags not appearing</strong>: Check for typos or special characters</li>
<li><strong>Tag search not working</strong>: Ensure proper tag format</li>
<li><strong>Sync issues</strong>: Verify ExifTool installation for file sync</li>
</ul>
<h3 id="ratings_tags_collections-collection-issues">Collection Issues</h3>
<ul>
<li><strong>Collection not saving</strong>: Check write permissions to index directory</li>
<li><strong>Items not adding</strong>: Verify collection limits haven't been reached</li>
<li><strong>Collection not loading</strong>: Check JSON file integrity</li>
</ul>
<h3 id="ratings_tags_collections-common-solutions">Common Solutions</h3>
<ul>
<li><strong>Refresh Index</strong>: Use Ctrl/Cmd+S to rescan and refresh</li>
<li><strong>Clear Cache</strong>: Clear browser cache if interface seems stuck</li>
<li><strong>Check Logs</strong>: Look at ComfyUI console for detailed error messages</li>
<li><strong>Database Maintenance</strong>: Run optimization tools periodically</li>
</ul>
<h2 id="ratings_tags_collections-advanced-features">Advanced Features</h2>
<h3 id="ratings_tags_collections-tag-expressions">Tag Expressions</h3>
<ul>
<li>Complex tag queries using boolean logic</li>
<li>Combine tags with AND/OR operations</li>
<li>Parentheses for complex expressions</li>
<li>Negation operators for exclusions</li>
</ul>
<h3 id="ratings_tags_collections-rating-statistics">Rating Statistics</h3>
<ul>
<li>View rating distributions across collections</li>
<li>Track rating trends over time</li>
<li>Identify most/least rated assets</li>
<li>Generate reports on rating patterns</li>
</ul>
<h3 id="ratings_tags_collections-collection-analytics">Collection Analytics</h3>
<ul>
<li>View collection usage statistics</li>
<li>Track most accessed collections</li>
<li>Identify underutilized collections</li>
<li>Analyze collection overlap and relationships</li>
</ul>
<h3 id="ratings_tags_collections-automation-possibilities">Automation Possibilities</h3>
<ul>
<li>Script-based tag assignment</li>
<li>Automated rating based on criteria</li>
<li>Scheduled collection maintenance</li>
<li>Integration with external tools</li>
</ul>
<h2 id="ratings_tags_collections-security-privacy">Security &amp; Privacy</h2>
<h3 id="ratings_tags_collections-local-storage">Local Storage</h3>
<ul>
<li>Ratings and tags stored locally</li>
<li>No external data transmission</li>
<li>Full control over your organizational data</li>
<li>Backup your index database regularly</li>
</ul>
<h3 id="ratings_tags_collections-file-metadata">File Metadata</h3>
<ul>
<li>Optional synchronization to file metadata</li>
<li>Controlled via settings</li>
<li>Respects file permissions</li>
<li>Preserves existing file metadata</li>
</ul>
<hr />
<p><em>Ratings, Tags &amp; Collections Guide Version: 1.0</em> <em>Last Updated: April 5, 2026</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/RATINGS_TAGS_COLLECTIONS.md</code>
</div>
</div>
</section>
<section class="panel section" id="search_filtering">
<div class="panel-h"><h2>SEARCH_FILTERING.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="search_filtering-majoor-assets-manager-search-filtering-guide">Majoor Assets Manager - Search &amp; Filtering Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 5, 2026</p>
<h2 id="search_filtering-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: Enhanced workflow type filters, file size filtering, resolution filtering, inline attribute filters, and the dedicated Workflow tab library. See <a href="#workflows">WORKFLOWS.md</a> for saved workflow browsing and reuse.</p>
<h2 id="search_filtering-full-text-search">Full-Text Search</h2>
<h3 id="search_filtering-how-it-works">How It Works</h3>
<p>The Assets Manager uses SQLite FTS5 with BM25 ranking to provide fast and accurate search results. The search indexes:</p>
<ul>
<li>Filenames and file extensions</li>
<li>Embedded metadata (prompts, models, parameters)</li>
<li>Workflow information</li>
<li>Tags and ratings</li>
<li>File content where applicable</li>
</ul>
<h3 id="search_filtering-basic-search">Basic Search</h3>
<ol>
<li>Locate the search bar at the top of the Assets Manager interface</li>
<li>Type any text you want to search for</li>
<li>Press Enter or wait for results to appear automatically</li>
<li>Results are displayed with relevance ranking</li>
</ol>
<h3 id="search_filtering-search-syntax">Search Syntax</h3>
<h4 id="search_filtering-simple-terms">Simple Terms</h4>
<ul>
<li>Type any word or phrase to search for it</li>
<li>Example: <code>landscape</code> finds all assets with "landscape" in their metadata</li>
</ul>
<h4 id="search_filtering-exact-phrases">Exact Phrases</h4>
<ul>
<li>Use quotes to search for exact phrases</li>
<li>Example: <code>"fantasy character"</code> finds assets with that exact phrase</li>
</ul>
<h4 id="search_filtering-multiple-terms">Multiple Terms</h4>
<ul>
<li>Search for multiple terms simultaneously</li>
<li>Results containing more terms rank higher</li>
<li>Example: <code>portrait fantasy digital</code> finds assets matching any or all terms</li>
</ul>
<h3 id="search_filtering-search-scopes">Search Scopes</h3>
<p>Search works across all available scopes:</p>
<ul>
<li><strong>Outputs</strong>: Search in your ComfyUI output directory</li>
<li><strong>Inputs</strong>: Search in your ComfyUI input directory</li>
<li><strong>Custom</strong>: Search in user-defined directories</li>
<li><strong>Collections</strong>: Search within saved collections</li>
</ul>
<h2 id="search_filtering-filtering-options">Filtering Options</h2>
<h3 id="search_filtering-kind-filter">Kind Filter</h3>
<p>The kind filter dropdown exposes the four file buckets that the backend consistently understands: <strong>image</strong>, <strong>video</strong>, <strong>audio</strong>, and <strong>model3d</strong>. Pick the bucket that best fits your search and the grid will stay in sync with the backend filters.</p>
<p>If you need to target a specific extension (for example <code>ext:webp</code> or <code>ext:gif</code>), type the <code>ext:&amp;lt;extension&amp;gt;</code> 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.</p>
<h3 id="search_filtering-rating-filter">Rating Filter</h3>
<p>Filter by star ratings (0-5 stars):</p>
<ol>
<li>Click the rating filter dropdown</li>
<li>Select minimum rating threshold</li>
<li>Only assets with equal or higher ratings will be displayed</li>
<li>Useful for quickly finding your best results</li>
</ol>
<h3 id="search_filtering-workflow-filter">Workflow Filter</h3>
<p>Toggle to show only assets with embedded workflow information:</p>
<ul>
<li>Shows only assets that contain workflow data</li>
<li>Helpful when looking for specific generation parameters</li>
<li>Particularly useful for reproducible results</li>
</ul>
<h3 id="search_filtering-date-filters">Date Filters</h3>
<p>Filter assets by creation or modification dates:</p>
<ol>
<li>Open the date filter panel</li>
<li>Select date range using calendar pickers</li>
<li>Apply the filter to narrow results</li>
<li>Useful for finding recent work or historical assets</li>
</ol>
<blockquote><strong>UTC note:</strong> The server evaluates date filters using UTC boundaries so every “today”/“this week” span is consistent regardless of the machine timezone.</blockquote>
<h3 id="search_filtering-advanced-filters">Advanced Filters</h3>
<h4 id="search_filtering-inline-attribute-filters">Inline Attribute Filters</h4>
<p>Type tokens like <code>rating:5</code> or <code>ext:png</code> 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.</p>
<h4 id="search_filtering-hide-png-siblings">Hide PNG Siblings</h4>
<p>When working with video generations that include PNG previews:</p>
<ul>
<li>Enable this option to hide PNG files when video previews exist</li>
<li>Reduces clutter from duplicate content in different formats</li>
<li>Keeps your view focused on the primary asset type</li>
</ul>
<h4 id="search_filtering-sort-options">Sort Options</h4>
<p>Sort results by various criteria:</p>
<ul>
<li><strong>Relevance</strong>: Based on search term matching (default)</li>
<li><strong>Name</strong>: Alphabetical by filename</li>
<li><strong>Date</strong>: Chronological by creation/modification date</li>
<li><strong>Size</strong>: By file size (ascending/descending)</li>
<li><strong>Rating</strong>: By star rating (ascending/descending)</li>
<li><strong>Kind</strong>: By file type</li>
</ul>
<h2 id="search_filtering-search-tips-best-practices">Search Tips &amp; Best Practices</h2>
<h3 id="search_filtering-effective-search-strategies">Effective Search Strategies</h3>
<ol>
<li><strong>Be Specific</strong>: Use specific terms for better results</li>
<ul>
<li>Instead of: <code>character</code></li>
<li>Try: <code>"fantasy warrior"</code> or <code>"cyberpunk city"</code></li>
</ul>
</ol>
<ol>
<li><strong>Combine Filters</strong>: Use multiple filters together</li>
<ul>
<li>Rating + Date + Kind filters can quickly narrow results</li>
</ul>
</ol>
<ol>
<li><strong>Use Quotes</strong>: For exact phrase matching</li>
<ul>
<li><code>"negative prompt: ugly"</code> finds assets with that exact phrase</li>
</ul>
</ol>
<ol>
<li><strong>Leverage Metadata</strong>: Search for model names, samplers, or parameters</li>
<ul>
<li><code>model:SDXL</code> finds assets generated with SDXL</li>
<li><code>steps:30</code> finds assets with 30 sampling steps</li>
</ul>
</ol>
<h3 id="search_filtering-performance-considerations">Performance Considerations</h3>
<ul>
<li>First search in a scope may take longer as indexing occurs</li>
<li>Subsequent searches are faster due to cached indexes</li>
<li>Very large directories may take time to scan initially</li>
<li>Results are loaded in pages for smooth performance</li>
</ul>
<h3 id="search_filtering-search-result-information">Search Result Information</h3>
<p>Each search result displays:</p>
<ul>
<li><strong>Thumbnail</strong>: Visual preview of the asset</li>
<li><strong>Filename</strong>: Original file name</li>
<li><strong>Extension Badge</strong>: File type with collision indicator (e.g., <code>PNG+</code> for duplicates)</li>
<li><strong>Rating</strong>: Star rating if assigned</li>
<li><strong>Tags</strong>: Preview of assigned tags</li>
<li><strong>Metadata</strong>: Brief generation information when available</li>
</ul>
<h2 id="search_filtering-using-filters-together">Using Filters Together</h2>
<h3 id="search_filtering-sequential-filtering">Sequential Filtering</h3>
<p>Apply filters in sequence for refined results:</p>
<ol>
<li>Start with a broad search term</li>
<li>Apply kind filter to narrow by file type</li>
<li>Use rating filter to show only quality results</li>
<li>Apply date filter to focus on timeframe</li>
<li>Sort results by preference</li>
</ol>
<h3 id="search_filtering-resetting-filters">Resetting Filters</h3>
<ul>
<li>Click the "Clear All Filters" button to reset all active filters</li>
<li>Or individually disable filters using their respective controls</li>
<li>Search terms remain unless cleared separately</li>
</ul>
<h2 id="search_filtering-collections-integration">Collections Integration</h2>
<h3 id="search_filtering-searching-within-collections">Searching Within Collections</h3>
<ul>
<li>Switch to the Collections scope to search within saved collections</li>
<li>Individual collections can be searched independently</li>
<li>Cross-collection searches are also supported</li>
</ul>
<h3 id="search_filtering-adding-filtered-results-to-collections">Adding Filtered Results to Collections</h3>
<ol>
<li>Apply your desired search and filters</li>
<li>Select the results you want to save</li>
<li>Right-click and choose "Add to Collection"</li>
<li>Create a new collection or add to existing one</li>
<li>The filtered results are preserved in the collection</li>
</ol>
<h2 id="search_filtering-troubleshooting-search-issues">Troubleshooting Search Issues</h2>
<h3 id="search_filtering-no-results-found">No Results Found</h3>
<ul>
<li>Check spelling of search terms</li>
<li>Try broader terms or synonyms</li>
<li>Verify the correct scope is selected</li>
<li>Ensure the directory has been scanned (trigger with Ctrl/Cmd+S)</li>
</ul>
<h3 id="search_filtering-too-many-results">Too Many Results</h3>
<ul>
<li>Add more specific search terms</li>
<li>Apply kind filters to narrow by file type</li>
<li>Use rating filters to show only quality results</li>
<li>Apply date filters to focus on recent work</li>
</ul>
<h3 id="search_filtering-slow-search-performance">Slow Search Performance</h3>
<ul>
<li>First search in a large directory takes longer</li>
<li>Subsequent searches are faster with cached indexes</li>
<li>Consider excluding very large directories that don't contain relevant assets</li>
<li>Check system resources and close other applications if needed</li>
</ul>
<h3 id="search_filtering-missing-metadata-in-search">Missing Metadata in Search</h3>
<ul>
<li>Verify external tools (ExifTool, FFprobe) are installed</li>
<li>Check file permissions for metadata access</li>
<li>Some file formats may not contain embeddable metadata</li>
<li>Re-index the directory if metadata should be present</li>
</ul>
<h2 id="search_filtering-advanced-search-techniques">Advanced Search Techniques</h2>
<h3 id="search_filtering-boolean-operations">Boolean Operations</h3>
<p>While direct boolean operators aren't supported, you can achieve similar results:</p>
<ul>
<li>AND: Include multiple terms (results must contain all terms)</li>
<li>OR: Use broader terms that match any of your interests</li>
<li>NOT: Manually exclude unwanted results after searching</li>
</ul>
<h3 id="search_filtering-pattern-recognition">Pattern Recognition</h3>
<ul>
<li>Search for common patterns in your workflow names</li>
<li>Use recurring artist/model names as search terms</li>
<li>Look for specific parameter combinations in metadata</li>
</ul>
<h3 id="search_filtering-workflow-based-searching">Workflow-Based Searching</h3>
<ul>
<li>Search for specific sampler names: <code>euler</code>, <code>ddim</code>, <code>dpm++</code></li>
<li>Find results from particular models: <code>realisticvision</code>, <code>dreamshaper</code></li>
<li>Locate generations with specific settings: <code>cfg_scale:7</code>, <code>steps:40</code></li>
</ul>
<h2 id="search_filtering-search-statistics">Search Statistics</h2>
<h3 id="search_filtering-result-count-display">Result Count Display</h3>
<p>The interface shows:</p>
<ul>
<li>Total assets in current scope</li>
<li>Number of assets matching current filters</li>
<li>Current page and total pages</li>
<li>Time taken for the search operation</li>
</ul>
<h3 id="search_filtering-performance-metrics">Performance Metrics</h3>
<ul>
<li>Search duration is displayed for each query</li>
<li>Indexing status shows when background processes are running</li>
<li>Memory usage indicators help monitor performance</li>
</ul>
<hr />
<p><em>Search &amp; Filtering Guide Version: 1.0</em> <em>Last Updated: April 5, 2026</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/SEARCH_FILTERING.md</code>
</div>
</div>
</section>
<section class="panel section" id="security_env_vars">
<div class="panel-h"><h2>SECURITY_ENV_VARS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="security_env_vars-majoor-assets-manager-security-model-environment-variables-guide">Majoor Assets Manager - Security Model &amp; Environment Variables Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 7, 2026</p>
<h2 id="security_env_vars-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: Enhanced API token support, improved Safe Mode controls, better remote access configuration, and a configurable index database directory for network-drive setups.</p>
<h2 id="security_env_vars-security-architecture">Security Architecture</h2>
<h3 id="security_env_vars-defense-layers">Defense Layers</h3>
<p>The Assets Manager employs multiple layers of security:</p>
<h4 id="security_env_vars-application-layer-security">Application Layer Security</h4>
<ul>
<li>Input validation and sanitization</li>
<li>Path containment and validation</li>
<li>File type verification</li>
<li>Access control mechanisms</li>
</ul>
<h4 id="security_env_vars-network-layer-security">Network Layer Security</h4>
<ul>
<li>CSRF protection on state-changing endpoints</li>
<li>Origin validation for sensitive requests</li>
<li>Rate limiting on expensive operations</li>
<li>Secure communication protocols</li>
</ul>
<h4 id="security_env_vars-file-system-security">File System Security</h4>
<ul>
<li>Root containment validation</li>
<li>Path traversal prevention</li>
<li>Permission checking</li>
<li>Symlink handling controls</li>
</ul>
<h3 id="security_env_vars-trust-model">Trust Model</h3>
<ul>
<li><strong>User Input</strong>: All user input is validated and sanitized</li>
<li><strong>File System</strong>: Only allowed directories are accessible</li>
<li><strong>External Tools</strong>: Tools run with limited privileges</li>
<li><strong>Network</strong>: Requests validated against origin policies</li>
</ul>
<h2 id="security_env_vars-authentication-authorization">Authentication &amp; Authorization</h2>
<h3 id="security_env_vars-write-access-guard-recommended">Write access guard (recommended)</h3>
<p>ComfyUI is often used locally, but it can be exposed via <code>--listen</code>, reverse proxies, or tunnels. To reduce risk, Majoor blocks destructive/write operations from non-local clients by default.</p>
<ul>
<li><strong>Default behavior (no token configured)</strong>: allow write operations only from loopback clients (<code>127.0.0.1</code> / <code>::1</code> / <code>localhost</code>).</li>
<li><strong>Token behavior</strong>: if <code>MAJOOR_API_TOKEN</code> (or <code>MJR_API_TOKEN</code>) is set, remote write operations require it.</li>
<ul>
<li>Loopback keeps the compatibility behavior by default.</li>
<li>Send the token via <code>X-MJR-Token: &amp;lt;token&amp;gt;</code> or <code>Authorization: Bearer &amp;lt;token&amp;gt;</code>.</li>
</ul>
<li><strong>Remote first-run bootstrap</strong>: if no persistent API token is configured yet, an authenticated ComfyUI user can bootstrap the initial remote session token automatically over HTTPS without setting <code>MAJOOR_ALLOW_BOOTSTRAP=1</code>.</li>
<li><strong>Loopback recovery bootstrap</strong>: local browser sessions on loopback can recover a Majoor write session after restart/new tab without a separate ComfyUI sign-in step.</li>
<li><strong>Legacy hash-only recovery</strong>: 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.</li>
<li><strong>Strict local auth</strong>: set <code>MAJOOR_REQUIRE_AUTH=1</code> if you want loopback writes to require the token too.</li>
</ul>
<h4 id="security_env_vars-overrides-use-with-care">Overrides (use with care)</h4>
<ul>
<li><code>MAJOOR_REQUIRE_AUTH=1</code> forces token auth even for loopback (requires <code>MAJOOR_API_TOKEN</code>).</li>
<li><code>MAJOOR_ALLOW_REMOTE_WRITE=1</code> allows remote write operations without a token (<strong>unsafe</strong>).</li>
<li><code>MAJOOR_ALLOW_BOOTSTRAP=1</code> temporarily enables initial <code>/bootstrap-token</code> provisioning for remote clients even when no authenticated ComfyUI user context is available. Disable it after first successful bootstrap.</li>
</ul>
<h4 id="security_env_vars-where-to-set-these-variables">Where to set these variables</h4>
<p>Set these on the machine that runs ComfyUI, in the same shell, batch file, service definition, or launcher script that starts <code>python main.py</code>.</p>
<p>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.</p>
<p>Recommended UI path for a trusted LAN setup:</p>
<ol>
<li>Open Majoor Settings.</li>
<li>Enable <code>Recommended Remote LAN Setup</code>.</li>
<li>Confirm the browser session is authorized.</li>
</ol>
<p>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.</p>
<p>Majoor also shows the current session state directly inside Assets Manager through the runtime widget, with a <code>Write auth:</code> line such as <code>Write auth: active ...ABCD</code>.</p>
<p>Windows batch example:</p>
<pre><code class="language-batch">@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</code></pre>
<p>Linux/macOS shell example:</p>
<pre><code class="language-bash">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</code></pre>
<p>If you change the value after ComfyUI is already running, restart ComfyUI so the new environment is loaded.</p>
<h3 id="security_env_vars-client-side-token-handling">Client-side token handling</h3>
<p>Set the same secret inside ComfyUI's Settings modal at <strong>Security -&gt; Majoor: API Token</strong> if you want a fixed shared token across multiple browsers/devices.</p>
<p>Runtime behavior:</p>
<ul>
<li>the current browser session keeps its active write token in <code>sessionStorage</code> (tab/session scoped)</li>
<li>bootstrap/rotate flows can also set the companion <code>httpOnly</code> cookie <code>mjr_write_token</code></li>
<li>the settings API does not expose the plaintext token after save; it only reports non-secret status fields such as <code>token_configured</code> and <code>token_hint</code></li>
<li>browser-local settings intentionally scrub the plaintext token after it has been pushed into the active session</li>
</ul>
<h3 id="security_env_vars-safe-mode-default-enabled">Safe Mode (default enabled)</h3>
<p>Safe Mode adds an explicit opt-in layer for operations that modify files or user metadata.</p>
<ul>
<li><code>MAJOOR_SAFE_MODE</code> (default: enabled)</li>
<ul>
<li>Set <code>MAJOOR_SAFE_MODE=0</code> to disable Safe Mode.</li>
</ul>
<li><code>MAJOOR_ALLOW_WRITE=1</code></li>
<ul>
<li>Allows rating/tags writes while Safe Mode is enabled.</li>
</ul>
<li><code>MAJOOR_ALLOW_DELETE=1</code></li>
<ul>
<li>Enables asset deletion (disabled by default).</li>
</ul>
<li><code>MAJOOR_ALLOW_RENAME=1</code></li>
<ul>
<li>Enables asset rename (disabled by default).</li>
</ul>
<li><code>MAJOOR_ALLOW_OPEN_IN_FOLDER=1</code></li>
<ul>
<li>Enables <code>/mjr/am/open-in-folder</code> (disabled by default).</li>
<li>This route may reveal any existing local file path in the OS file manager. It does not read, write, delete, download, or index that file.</li>
</ul>
</ul>
<h3 id="security_env_vars-access-control">Access Control</h3>
<ul>
<li>File access limited to ComfyUI's allowed directories</li>
<li>No direct read/write access to system files outside allowed paths</li>
<li>Exception: <code>/mjr/am/open-in-folder</code> can reveal an existing path in the OS file manager when explicitly enabled, because it is an OS launch action rather than file content access.</li>
<li>Permission inheritance from ComfyUI's file system access</li>
<li>No elevation of privileges possible</li>
</ul>
<h2 id="security_env_vars-csrf-protection">CSRF Protection</h2>
<h3 id="security_env_vars-request-validation">Request Validation</h3>
<p>All state-changing endpoints require additional validation:</p>
<h4 id="security_env_vars-xmlhttprequest-header">XMLHttpRequest Header</h4>
<ul>
<li>Requests must include <code>X-Requested-With: XMLHttpRequest</code></li>
<li>Prevents simple form submissions from external sites</li>
<li>Standard technique for AJAX endpoint protection</li>
</ul>
<h4 id="security_env_vars-csrf-token-alternative">CSRF Token Alternative</h4>
<ul>
<li>Alternative validation using <code>X-CSRF-Token</code> header</li>
<li>Provides fallback for different client implementations</li>
<li>Tokens validated against session state</li>
</ul>
<h3 id="security_env_vars-origin-validation">Origin Validation</h3>
<ul>
<li>When <code>Origin</code> header is present, it's validated against <code>Host</code></li>
<li>Prevents cross-origin requests from unauthorized domains</li>
<li>Protects against certain types of cross-site attacks</li>
</ul>
<h2 id="security_env_vars-rate-limiting">Rate Limiting</h2>
<h3 id="security_env_vars-per-client-limits">Per-Client Limits</h3>
<p>Rate limiting is implemented on expensive endpoints:</p>
<h4 id="security_env_vars-affected-endpoints">Affected Endpoints</h4>
<ul>
<li>Search operations (<code>/mjr/am/search</code>)</li>
<li>Index scanning (<code>/mjr/am/scan</code>)</li>
<li>Metadata extraction (<code>/mjr/am/metadata</code>)</li>
<li>Batch ZIP operations (<code>/mjr/am/batch-zip</code>)</li>
</ul>
<h4 id="security_env_vars-limit-configuration">Limit Configuration</h4>
<ul>
<li>Implemented in-memory per-client</li>
<li>Client identity based on IP address</li>
<li>Configurable thresholds for different operations</li>
<li>Resets after specified time intervals</li>
</ul>
<h4 id="security_env_vars-trusted-proxy-support">Trusted Proxy Support</h4>
<ul>
<li><code>X-Forwarded-For</code> header honored only from trusted proxies</li>
<li>Controlled by <code>MAJOOR_TRUSTED_PROXIES</code> environment variable</li>
<li>Prevents IP spoofing in proxy environments</li>
</ul>
<h2 id="security_env_vars-path-safety-containment">Path Safety &amp; Containment</h2>
<h3 id="security_env_vars-root-validation">Root Validation</h3>
<p>All file operations validate root containment:</p>
<h4 id="security_env_vars-allowed-roots">Allowed Roots</h4>
<ul>
<li>ComfyUI output directory</li>
<li>ComfyUI input directory</li>
<li>Custom roots defined by user</li>
<li>Collections directory</li>
</ul>
<h4 id="security_env_vars-path-validation-process">Path Validation Process</h4>
<ol>
<li>Normalize the requested path</li>
<li>Resolve to absolute path</li>
<li>Verify path starts with allowed root</li>
<li>Reject if outside allowed boundaries</li>
</ol>
<h3 id="security_env_vars-symlink-handling">Symlink Handling</h3>
<ul>
<li>Symlinks are handled carefully to prevent directory traversal</li>
<li>Optional symlink support via <code>MJR_ALLOW_SYMLINKS</code> environment variable</li>
<li>Disabled by default for security</li>
<li>When enabled, still validates final resolved path</li>
</ul>
<h3 id="security_env_vars-path-traversal-prevention">Path Traversal Prevention</h3>
<ul>
<li>Input paths are normalized to prevent <code>../</code> attacks</li>
<li>Absolute path resolution ensures final location is verified</li>
<li>Multiple validation layers prevent bypass attempts</li>
</ul>
<h2 id="security_env_vars-file-operation-security">File Operation Security</h2>
<h3 id="security_env_vars-safe-file-operations">Safe File Operations</h3>
<ul>
<li>All file operations go through security validation</li>
<li>Read operations limited to allowed directories</li>
<li>Write operations limited to index and temporary directories</li>
<li>Delete operations require additional confirmation</li>
</ul>
<h3 id="security_env_vars-batch-zip-security">Batch ZIP Security</h3>
<ul>
<li>ZIP building streams directly from file handles</li>
<li>Prevents TOCTOU (Time-of-Check-Time-of-Use) race conditions</li>
<li>No temporary file copies created during ZIP building</li>
<li>Automatic cleanup of temporary ZIP files</li>
</ul>
<h3 id="security_env_vars-file-type-validation">File Type Validation</h3>
<ul>
<li>File types validated before processing</li>
<li>Dangerous file types blocked from certain operations</li>
<li>MIME type checking where possible</li>
<li>Extension validation for security-sensitive operations</li>
</ul>
<h2 id="security_env_vars-network-security">Network Security</h2>
<h3 id="security_env_vars-http-security-headers">HTTP Security Headers</h3>
<p>Applied to Majoor API endpoints only:</p>
<h4 id="security_env_vars-content-security-policy">Content Security Policy</h4>
<ul>
<li><code>Content-Security-Policy: default-src 'none'</code></li>
<li>Prevents execution of injected content</li>
<li>Applies only to API responses</li>
</ul>
<h4 id="security_env_vars-content-type-options">Content Type Options</h4>
<ul>
<li><code>X-Content-Type-Options: nosniff</code></li>
<li>Prevents MIME-type confusion attacks</li>
<li>Ensures content is treated as declared type</li>
</ul>
<h4 id="security_env_vars-frame-options">Frame Options</h4>
<ul>
<li><code>X-Frame-Options: DENY</code></li>
<li>Prevents embedding in iframes</li>
<li>Protects against clickjacking</li>
</ul>
<h4 id="security_env_vars-referrer-policy">Referrer Policy</h4>
<ul>
<li><code>Referrer-Policy: strict-origin-when-cross-origin</code></li>
<li>Limits referrer information leakage</li>
<li>Balances privacy with functionality</li>
</ul>
<h3 id="security_env_vars-api-versioning-security">API Versioning Security</h3>
<ul>
<li>Versioned routes redirect to canonical endpoints</li>
<li><code>/mjr/am/v1/...</code> redirects to <code>/mjr/am/...</code> using 308</li>
<li>Maintains security headers across versions</li>
<li>Preserves method and query parameters</li>
</ul>
<h2 id="security_env_vars-external-tool-security">External Tool Security</h2>
<h3 id="security_env_vars-tool-execution-safety">Tool Execution Safety</h3>
<ul>
<li>External tools (ExifTool, FFprobe) run with limited privileges</li>
<li>Input validation before passing to external tools</li>
<li>Timeout enforcement to prevent hanging operations</li>
<li>Output validation after tool execution</li>
</ul>
<h3 id="security_env_vars-path-validation-for-tools">Path Validation for Tools</h3>
<ul>
<li>File paths validated before passing to external tools</li>
<li>No direct shell execution, only tool invocation</li>
<li>Input sanitization for all tool parameters</li>
<li>Tool output parsing with validation</li>
</ul>
<h3 id="security_env_vars-tool-location-security">Tool Location Security</h3>
<ul>
<li>Tools can be specified via environment variables</li>
<li>Default to system PATH for security</li>
<li>No arbitrary tool execution possible</li>
<li>Tool paths validated at startup</li>
</ul>
<h2 id="security_env_vars-environment-variable-security">Environment Variable Security</h2>
<h3 id="security_env_vars-secure-configuration">Secure Configuration</h3>
<p>Environment variables provide secure configuration without code changes:</p>
<h4 id="security_env_vars-configuration-validation">Configuration Validation</h4>
<ul>
<li>Environment variables validated at startup</li>
<li>Invalid values fall back to defaults safely</li>
<li>No unsafe defaults used</li>
<li>Clear error messages for invalid configurations</li>
</ul>
<h3 id="security_env_vars-sensitive-information-handling">Sensitive Information Handling</h3>
<ul>
<li>No passwords or secrets stored in environment</li>
<li>File paths validated for security</li>
<li>No direct system command execution via environment</li>
<li>Configuration changes logged appropriately</li>
</ul>
<h3 id="security_env_vars-trusted-proxy-configuration">Trusted Proxy Configuration</h3>
<ul>
<li><code>MAJOOR_TRUSTED_PROXIES</code> controls proxy trust</li>
<li>Default: <code>127.0.0.1,::1</code> (localhost only)</li>
<li>Prevents IP spoofing in proxy environments</li>
<li>Critical for rate limiting effectiveness</li>
</ul>
<h2 id="security_env_vars-threat-model">Threat Model</h2>
<h3 id="security_env_vars-identified-threats">Identified Threats</h3>
<h4 id="security_env_vars-information-disclosure">Information Disclosure</h4>
<ul>
<li>Unauthorized access to file metadata</li>
<li>Exposure of file system structure</li>
<li>Leakage of generation parameters</li>
<li><strong>Mitigation</strong>: Path validation, access controls</li>
</ul>
<h4 id="security_env_vars-path-traversal">Path Traversal</h4>
<ul>
<li>Access to files outside allowed directories</li>
<li>Reading system files or other users' data</li>
<li>Writing to unauthorized locations</li>
<li><strong>Mitigation</strong>: Path containment, normalization</li>
</ul>
<h4 id="security_env_vars-resource-exhaustion">Resource Exhaustion</h4>
<ul>
<li>Denial of service through expensive operations</li>
<li>Memory exhaustion via large metadata</li>
<li>Database locking through concurrent operations</li>
<li><strong>Mitigation</strong>: Rate limiting, timeouts, resource limits</li>
</ul>
<h4 id="security_env_vars-code-execution">Code Execution</h4>
<ul>
<li>Arbitrary command execution through file operations</li>
<li>Injection through file names or paths</li>
<li>Malicious file content processing</li>
<li><strong>Mitigation</strong>: Input validation, sandboxing</li>
</ul>
<h4 id="security_env_vars-cross-site-attacks">Cross-Site Attacks</h4>
<ul>
<li>CSRF attacks on API endpoints</li>
<li>XSS through metadata injection</li>
<li>Clickjacking of interface elements</li>
<li><strong>Mitigation</strong>: CSRF tokens, security headers</li>
</ul>
<h3 id="security_env_vars-attack-vectors">Attack Vectors</h3>
<h4 id="security_env_vars-direct-api-access">Direct API Access</h4>
<ul>
<li>Unauthorized access to HTTP endpoints</li>
<li>Brute force attempts on protected endpoints</li>
<li><strong>Protection</strong>: Origin validation, rate limiting</li>
</ul>
<h4 id="security_env_vars-file-system-manipulation">File System Manipulation</h4>
<ul>
<li>Creation of malicious file names</li>
<li>Symlink attacks in custom roots</li>
<li><strong>Protection</strong>: Path validation, containment</li>
</ul>
<h4 id="security_env_vars-input-injection">Input Injection</h4>
<ul>
<li>Malformed metadata in files</li>
<li>Special characters in file names</li>
<li><strong>Protection</strong>: Input sanitization, validation</li>
</ul>
<h2 id="security_env_vars-security-monitoring">Security Monitoring</h2>
<h3 id="security_env_vars-request-logging">Request Logging</h3>
<ul>
<li>Optional request logging via observability settings</li>
<li>Logs include client IP, endpoint, and timestamp</li>
<li>Sensitive data not logged (file contents, metadata)</li>
<li>Helpful for detecting unusual access patterns</li>
</ul>
<h3 id="security_env_vars-error-handling">Error Handling</h3>
<ul>
<li>Secure error messages that don't leak system information</li>
<li>Internal errors logged but not exposed to users</li>
<li>Validation errors provide guidance without revealing internals</li>
<li>Stack traces hidden from end users</li>
</ul>
<h3 id="security_env_vars-audit-trail">Audit Trail</h3>
<ul>
<li>File operations logged with appropriate detail</li>
<li>Configuration changes tracked where applicable</li>
<li>Access patterns monitored for anomalies</li>
<li>Retention policies for security logs</li>
</ul>
<h2 id="security_env_vars-security-best-practices">Security Best Practices</h2>
<h3 id="security_env_vars-deployment-security">Deployment Security</h3>
<h4 id="security_env_vars-network-isolation">Network Isolation</h4>
<ul>
<li>Run ComfyUI behind appropriate firewalls</li>
<li>Limit network access to trusted users</li>
<li>Use VPN or other secure access methods</li>
<li>Monitor network traffic for anomalies</li>
</ul>
<h4 id="security_env_vars-file-system-permissions">File System Permissions</h4>
<ul>
<li>Run ComfyUI with minimal required privileges</li>
<li>Restrict file system permissions appropriately</li>
<li>Regular permission audits</li>
<li>Separate user accounts for different functions</li>
</ul>
<h4 id="security_env_vars-regular-updates">Regular Updates</h4>
<ul>
<li>Keep Assets Manager updated to latest version</li>
<li>Update external tools (ExifTool, FFprobe) regularly</li>
<li>Apply security patches promptly</li>
<li>Test updates in non-production environments first</li>
</ul>
<h3 id="security_env_vars-configuration-security">Configuration Security</h3>
<h4 id="security_env_vars-principle-of-least-privilege">Principle of Least Privilege</h4>
<ul>
<li>Grant only necessary file system access</li>
<li>Disable unnecessary features</li>
<li>Use restrictive default settings</li>
<li>Regular security configuration reviews</li>
</ul>
<h4 id="security_env_vars-environment-hardening">Environment Hardening</h4>
<ul>
<li>Secure environment variable storage</li>
<li>Validate all configuration inputs</li>
<li>Use secure defaults where possible</li>
<li>Regular security configuration audits</li>
</ul>
<h3 id="security_env_vars-operational-security">Operational Security</h3>
<h4 id="security_env_vars-access-control">Access Control</h4>
<ul>
<li>Limit access to authorized users</li>
<li>Regular access reviews</li>
<li>Session management best practices</li>
<li>Audit user activities appropriately</li>
</ul>
<h4 id="security_env_vars-monitoring-and-detection">Monitoring and Detection</h4>
<ul>
<li>Monitor for unusual access patterns</li>
<li>Alert on security-relevant events</li>
<li>Regular security log reviews</li>
<li>Incident response procedures</li>
</ul>
<h2 id="security_env_vars-security-related-environment-variables">Security-Related Environment Variables</h2>
<h3 id="security_env_vars-trusted-proxy-configuration">Trusted Proxy Configuration</h3>
<ul>
<li><strong>MAJOOR_TRUSTED_PROXIES</strong></li>
<ul>
<li>Purpose: IPs/CIDRs allowed to supply <code>X-Forwarded-For</code>/<code>X-Real-IP</code></li>
<li>Default: <code>127.0.0.1,::1</code></li>
<li>Format: Comma-separated IPs or CIDR blocks</li>
<li>Security Impact: Controls which proxies can affect rate limiting</li>
<li>Example: <code>MAJOOR_TRUSTED_PROXIES=127.0.0.1,192.168.1.0/24,10.0.0.0/8</code></li>
</ul>
</ul>
<h3 id="security_env_vars-symlink-controls">Symlink Controls</h3>
<ul>
<li><strong>MJR_ALLOW_SYMLINKS</strong></li>
<ul>
<li>Purpose: Allow symlink/junction custom roots</li>
<li>Default: <code>off</code> (disabled)</li>
<li>Options: <code>on</code>, <code>off</code>, <code>true</code>, <code>false</code></li>
<li>Security Impact: Controls directory traversal risk</li>
<li>Example: <code>MJR_ALLOW_SYMLINKS=on</code></li>
</ul>
</ul>
<h3 id="security_env_vars-database-security">Database Security</h3>
<ul>
<li><strong>MAJOOR_DB_TIMEOUT</strong></li>
<ul>
<li>Purpose: Database operation timeout</li>
<li>Default: 30.0 seconds</li>
<li>Security Impact: Prevents resource exhaustion</li>
<li>Example: <code>MAJOOR_DB_TIMEOUT=60.0</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_DB_QUERY_TIMEOUT</strong></li>
<ul>
<li>Purpose: Maximum query execution time</li>
<li>Default: 60.0 seconds</li>
<li>Security Impact: Prevents long-running queries</li>
<li>Example: <code>MAJOOR_DB_QUERY_TIMEOUT=45.0</code></li>
</ul>
</ul>
<h3 id="security_env_vars-resource-limits">Resource Limits</h3>
<ul>
<li><strong>MAJOOR_MAX_METADATA_JSON_BYTES</strong></li>
<ul>
<li>Purpose: Maximum metadata JSON size stored in DB/cache</li>
<li>Default: 2097152 (2MB)</li>
<li>Security Impact: Prevents memory exhaustion</li>
<li>Example: <code>MAJOOR_MAX_METADATA_JSON_BYTES=4194304</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_COLLECTION_MAX_ITEMS</strong></li>
<ul>
<li>Purpose: Max items per collection JSON</li>
<li>Default: 50000</li>
<li>Security Impact: Prevents resource exhaustion</li>
<li>Example: <code>MJR_COLLECTION_MAX_ITEMS=100000</code></li>
</ul>
</ul>
<h3 id="security_env_vars-dependency-management">Dependency Management</h3>
<ul>
<li><strong>MJR_AM_NO_AUTO_PIP</strong></li>
<ul>
<li>Purpose: Disable best-effort dependency auto-install on startup</li>
<li>Default: Auto-install enabled</li>
<li>Security Impact: Controls external package installation</li>
<li>Options: <code>1</code>, <code>true</code>, <code>yes</code>, <code>on</code> to disable</li>
<li>Example: <code>MJR_AM_NO_AUTO_PIP=1</code></li>
</ul>
</ul>
<h2 id="security_env_vars-security-testing">Security Testing</h2>
<h3 id="security_env_vars-self-assessment-checklist">Self-Assessment Checklist</h3>
<p>Regularly verify security configuration:</p>
<h4 id="security_env_vars-access-controls">Access Controls</h4>
<ul>
<li>[ ] File system access limited to intended directories</li>
<li>[ ] Custom roots validated and secure</li>
<li>[ ] No unauthorized file access possible</li>
</ul>
<h4 id="security_env_vars-network-security">Network Security</h4>
<ul>
<li>[ ] CSRF protection active on state-changing endpoints</li>
<li>[ ] Rate limiting functioning properly</li>
<li>[ ] Security headers applied correctly</li>
</ul>
<h4 id="security_env_vars-input-validation">Input Validation</h4>
<ul>
<li>[ ] Path traversal prevented</li>
<li>[ ] File type validation working</li>
<li>[ ] External tool inputs sanitized</li>
</ul>
<h3 id="security_env_vars-penetration-testing-considerations">Penetration Testing Considerations</h3>
<p>When performing security testing:</p>
<h4 id="security_env_vars-authorized-testing-only">Authorized Testing Only</h4>
<ul>
<li>Ensure proper authorization before testing</li>
<li>Test in isolated environments when possible</li>
<li>Document and report findings appropriately</li>
</ul>
<h4 id="security_env_vars-testing-areas">Testing Areas</h4>
<ul>
<li>Path traversal attempts</li>
<li>Input validation bypasses</li>
<li>Authentication bypasses</li>
<li>Resource exhaustion attacks</li>
<li>Cross-site scripting attempts</li>
</ul>
<h2 id="security_env_vars-incident-response">Incident Response</h2>
<h3 id="security_env_vars-security-event-classification">Security Event Classification</h3>
<ul>
<li><strong>Low Risk</strong>: Minor configuration issues</li>
<li><strong>Medium Risk</strong>: Potential information disclosure</li>
<li><strong>High Risk</strong>: System compromise or data breach</li>
<li><strong>Critical Risk</strong>: Active exploitation in progress</li>
</ul>
<h3 id="security_env_vars-response-procedures">Response Procedures</h3>
<ol>
<li>Isolate affected systems</li>
<li>Document the incident</li>
<li>Assess scope and impact</li>
<li>Apply immediate mitigations</li>
<li>Investigate root cause</li>
<li>Implement permanent fixes</li>
<li>Communicate appropriately</li>
<li>Review and improve procedures</li>
</ol>
<hr />
<p><em>Security Model &amp; Environment Variables Guide Version: 1.0</em> <em>Last Updated: April 5, 2026</em></p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/SECURITY_ENV_VARS.md</code>
</div>
</div>
</section>
<section class="panel section" id="settings_configuration">
<div class="panel-h"><h2>SETTINGS_CONFIGURATION.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="settings_configuration-majoor-assets-manager-settings-configuration-guide">Majoor Assets Manager - Settings &amp; Configuration Guide</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 15, 2026</p>
<h2 id="settings_configuration-overview">Overview</h2>
<p>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.</p>
<p><strong>Recent highlights</strong>: 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.</p>
<h2 id="settings_configuration-browser-based-settings">Browser-Based Settings</h2>
<h3 id="settings_configuration-storage-location">Storage Location</h3>
<p>Browser-based settings are stored in <code>localStorage</code> under the <code>mjrSettings</code> key:</p>
<ul>
<li>Settings persist between browser sessions</li>
<li>Settings are specific to each browser/computer</li>
<li>Settings don't sync across different browsers or devices</li>
<li>Settings are tied to the specific domain where ComfyUI is hosted</li>
</ul>
<h3 id="settings_configuration-accessing-settings">Accessing Settings</h3>
<p>Settings are primarily adjusted through the user interface:</p>
<ol>
<li>Open the Assets Manager in ComfyUI</li>
<li>Look for settings icons or configuration panels</li>
<li>Adjust settings as needed</li>
<li>Settings are saved automatically</li>
</ol>
<h3 id="settings_configuration-security-settings-in-the-ui">Security Settings In The UI</h3>
<p>Majoor now exposes the main remote write controls directly in Settings, including:</p>
<ul>
<li><code>Recommended Remote LAN Setup</code></li>
<li><code>Require Token For All Writes</code></li>
<li><code>Allow Remote Full Access</code></li>
<li><code>Allow HTTP Token Transport</code></li>
<li><code>Majoor: API Token</code></li>
</ul>
<p>For the common trusted-LAN case, <code>Recommended Remote LAN Setup</code> 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.</p>
<h3 id="settings_configuration-token-types-and-what-they-mean">Token Types And What They Mean</h3>
<p>There are two unrelated token concepts in the UI:</p>
<ul>
<li><strong>Majoor: API Token</strong>: protects remote write access to the local Majoor backend.</li>
<li><strong>HuggingFace Token</strong>: optional token used for downloading AI models from HuggingFace Hub with better rate limits.</li>
</ul>
<p>The HuggingFace token is <strong>not</strong> a cloud inference API key. Its presence does not mean prompts or images are sent to an external hosted AI service.</p>
<h3 id="settings_configuration-browser-session-vs-server-token-state">Browser Session Vs Server Token State</h3>
<p>Not all security values live in the same place:</p>
<ul>
<li>browser settings in <code>localStorage</code> keep UI preferences and non-secret token state such as <code>tokenConfigured</code> and <code>tokenHint</code></li>
<li>the active write token for the current tab/browser session lives in <code>sessionStorage</code></li>
<li>the persistent shared token lives server-side once saved through Majoor Settings or environment variables</li>
</ul>
<p>This means a browser can show that a token exists on the server without storing the plaintext token permanently in browser local storage.</p>
<h3 id="settings_configuration-visual-write-authorization-status">Visual Write Authorization Status</h3>
<p>When the Assets Manager panel is open, the runtime status widget in the lower-right corner now includes a <code>Write auth:</code> line.</p>
<p>Examples:</p>
<ul>
<li><code>Write auth: active ...ABCD</code></li>
<li><code>Write auth: missing in this browser ...ABCD</code></li>
<li><code>Write auth: not required</code></li>
</ul>
<p>Use that line as the quickest confirmation that the current browser session can perform write operations.</p>
<h2 id="settings_configuration-display-settings">Display Settings</h2>
<h3 id="settings_configuration-page-size">Page Size</h3>
<ul>
<li><strong>Purpose</strong>: Controls how many assets are loaded per request</li>
<li><strong>Default</strong>: Usually 50-100 assets per page</li>
<li><strong>Range</strong>: Typically 10-500 assets per page</li>
<li><strong>Impact</strong>: Larger pages mean fewer requests but more memory usage</li>
<li><strong>Recommendation</strong>: Adjust based on your system resources and usage patterns</li>
</ul>
<h3 id="settings_configuration-sidebar-position">Sidebar Position</h3>
<ul>
<li><strong>Options</strong>: Left or Right</li>
<li><strong>Default</strong>: Usually Right</li>
<li><strong>Purpose</strong>: Controls where the details sidebar appears</li>
<li><strong>Impact</strong>: Affects layout and workflow depending on screen orientation</li>
<li><strong>Recommendation</strong>: Choose based on your screen setup and preferences</li>
</ul>
<h3 id="settings_configuration-hide-png-siblings">Hide PNG Siblings</h3>
<ul>
<li><strong>Purpose</strong>: Hide PNG files when video previews exist</li>
<li><strong>Default</strong>: Usually Off/Disabled</li>
<li><strong>Function</strong>: Reduces clutter from duplicate content in different formats</li>
<li><strong>Impact</strong>: Cleaner interface when working with video generations that include PNG previews</li>
<li><strong>Recommendation</strong>: Enable if you frequently work with video generations</li>
</ul>
<h2 id="settings_configuration-performance-settings">Performance Settings</h2>
<h3 id="settings_configuration-auto-scan-on-startup">Auto-Scan on Startup</h3>
<ul>
<li><strong>Auto-scan on Startup</strong>: Automatically scan when ComfyUI starts</li>
<li><strong>Default</strong>: Usually Disabled to save resources</li>
<li><strong>Impact</strong>: Ensures fresh index but uses system resources</li>
<li><strong>Recommendation</strong>: Enable for frequently updated directories</li>
</ul>
<h3 id="settings_configuration-status-poll-interval">Status Poll Interval</h3>
<ul>
<li><strong>Purpose</strong>: How often to check background tasks and status</li>
<li><strong>Default</strong>: Usually 1-5 seconds</li>
<li><strong>Range</strong>: 0.5-30 seconds</li>
<li><strong>Impact</strong>: More frequent polling provides more responsive status updates but uses more resources</li>
<li><strong>Recommendation</strong>: 1-2 seconds for good balance of responsiveness and resource usage</li>
</ul>
<h3 id="settings_configuration-tags-cache-ttl">Tags Cache TTL</h3>
<ul>
<li><strong>Purpose</strong>: Time-to-live for tag caching in milliseconds</li>
<li><strong>Default</strong>: Usually 30,000ms (30 seconds)</li>
<li><strong>Range</strong>: 1,000ms to 300,000ms (1 second to 5 minutes)</li>
<li><strong>Impact</strong>: Longer TTL means fewer requests but potentially stale data</li>
<li><strong>Recommendation</strong>: 30,000ms (30 seconds) for good balance</li>
</ul>
<h2 id="settings_configuration-metadata-viewer-settings">Metadata &amp; Viewer Settings</h2>
<h3 id="settings_configuration-floating-viewer-default-toggles">Floating Viewer Default Toggles</h3>
<ul>
<li><strong>UI location</strong>: Settings → Majoor Assets Manager › Viewer</li>
<li><strong>Majoor: MFV Live Stream Enabled by Default</strong></li>
<ul>
<li><strong>Setting key</strong>: <code>viewer.mfvLiveDefault</code></li>
<li><strong>Default</strong>: Enabled / On</li>
<li><strong>Purpose</strong>: 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.</li>
</ul>
<li><strong>Majoor: MFV KSampler Preview Enabled by Default</strong></li>
<ul>
<li><strong>Setting key</strong>: <code>viewer.mfvPreviewDefault</code></li>
<li><strong>Default</strong>: Enabled / On</li>
<li><strong>Purpose</strong>: 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.</li>
</ul>
</ul>
<h3 id="settings_configuration-media-probe-backend">Media Probe Backend</h3>
<ul>
<li><strong>Auto</strong>: Automatically choose the best available backend</li>
<li><strong>ExifTool</strong>: Use ExifTool exclusively for metadata extraction</li>
<li><strong>FFprobe</strong>: Use FFprobe exclusively (especially for video)</li>
<li><strong>Both</strong>: Use both tools when available</li>
<li><strong>Default</strong>: Auto</li>
<li><strong>Impact</strong>: Affects metadata extraction speed and completeness</li>
<li><strong>Recommendation</strong>: Keep as Auto unless troubleshooting specific issues</li>
</ul>
<h3 id="settings_configuration-workflow-minimap-display">Workflow Minimap Display</h3>
<ul>
<li><strong>Show Node Labels</strong>: Display text labels on workflow minimap</li>
<li><strong>Show Connection Lines</strong>: Display connections between nodes</li>
<li><strong>Show Parameter Values</strong>: Display parameter values on nodes</li>
<li><strong>Minimap Size</strong>: Adjust the size of the workflow minimap</li>
<li><strong>Default</strong>: Varies by preference</li>
<li><strong>Impact</strong>: Affects readability of workflow previews</li>
<li><strong>Recommendation</strong>: Enable based on your need for workflow detail</li>
</ul>
<h3 id="settings_configuration-observability">Observability</h3>
<ul>
<li><strong>Request Logging</strong>: Enable detailed logging of API requests</li>
<li><strong>Performance Metrics</strong>: Track timing and performance data</li>
<li><strong>Debug Information</strong>: Enable additional debugging output</li>
<li><strong>Default</strong>: Usually Disabled</li>
<li><strong>Impact</strong>: Provides detailed information for troubleshooting but increases log volume</li>
<li><strong>Recommendation</strong>: Enable only when troubleshooting issues</li>
</ul>
<h2 id="settings_configuration-file-system-settings">File System Settings</h2>
<h3 id="settings_configuration-workflow-roots">Workflow Roots</h3>
<ul>
<li><strong>UI location</strong>: Settings -&gt; Majoor Assets Manager -&gt; Advanced -&gt; Paths / Workflows</li>
<li><strong>Purpose</strong>: Sets the folders scanned by the Workflow tab and the first folder used when saving/importing workflow JSON files from the tab.</li>
<li><strong>Multiple folders</strong>: Use one folder per line, or separate folders with semicolons.</li>
<li><strong>Quick picker</strong>: In the Workflow tab, use the folder button beside <strong>Save current workflow</strong> to pick and save the workflow root directly.</li>
<li><strong>Default behavior</strong>: Leave empty to use ComfyUI workflow defaults and the legacy <code>MJR_AM_WORKFLOW_DIRECTORY</code> environment variable.</li>
</ul>
<p>Environment variables:</p>
<pre><code class="language-bash">MJR_AM_WORKFLOW_DIRECTORIES=/path/to/workflows:/path/to/other/workflows
MJR_AM_WORKFLOW_DIRECTORY=/path/to/workflows</code></pre>
<h3 id="settings_configuration-index-directory">Index Directory</h3>
<ul>
<li><strong>Purpose</strong>: Override where the SQLite index database and related index files are stored.</li>
<li><strong>Default</strong>: <code>&amp;lt;output_directory&amp;gt;/_mjr_index/</code> (next to your assets)</li>
<li><strong>Why change it</strong>: 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. <code>C:\mjr_index</code> on Windows) solves that without touching your asset files.</li>
<li><strong>UI location</strong>: Settings → Paths → Majoor: Index Directory</li>
<li><strong>Takes effect after</strong>: ComfyUI restart. The old index directory is <strong>not</strong> deleted automatically; run a fresh scan after restart.</li>
<li><strong>Clear the override</strong>: Leave the field empty and save to revert to the default (<code>&amp;lt;output&amp;gt;/_mjr_index/</code>).</li>
</ul>
<h3 id="settings_configuration-custom-roots">Custom Roots</h3>
<ul>
<li><strong>Adding Custom Directories</strong>: Add additional directories to browse</li>
<li><strong>Path Validation</strong>: Ensures paths are valid and accessible</li>
<li><strong>Symlink Support</strong>: Allow symbolic links in custom roots (if enabled)</li>
<li><strong>Access Permissions</strong>: Respects file system permissions</li>
<li><strong>Default</strong>: None initially</li>
<li><strong>Impact</strong>: Expands browsing capabilities beyond standard directories</li>
<li><strong>Recommendation</strong>: Add directories you frequently access</li>
</ul>
<h2 id="settings_configuration-database-settings">Database Settings</h2>
<h3 id="settings_configuration-connection-management">Connection Management</h3>
<ul>
<li><strong>Max Connections</strong>: Maximum simultaneous database connections</li>
<li><strong>Connection Timeout</strong>: Time to wait for database operations</li>
<li><strong>Query Timeout</strong>: Maximum time for individual queries</li>
<li><strong>Default</strong>: Usually 8 connections, 30-second timeouts</li>
<li><strong>Impact</strong>: Affects performance with concurrent operations</li>
<li><strong>Recommendation</strong>: Adjust based on system resources and usage patterns</li>
</ul>
<h3 id="settings_configuration-optimization">Optimization</h3>
<ul>
<li><strong>Auto-Optimize</strong>: Automatically optimize database periodically</li>
<li><strong>Manual Optimization</strong>: Run optimization tools manually</li>
<li><strong>Maintenance Schedule</strong>: When optimization occurs</li>
<li><strong>Default</strong>: Usually Manual</li>
<li><strong>Impact</strong>: Improves performance but requires temporary locking</li>
<li><strong>Recommendation</strong>: Schedule during low-usage periods</li>
</ul>
<h2 id="settings_configuration-advanced-configuration">Advanced Configuration</h2>
<h3 id="settings_configuration-ai-privacy-and-offline-summary">AI Privacy And Offline Summary</h3>
<ul>
<li>AI inference is intended to run locally after models are available on disk.</li>
<li>First model bootstrap may require internet access to download model files from HuggingFace.</li>
<li>After download and cache, AI features can run offline.</li>
<li>Non-AI Majoor features do not require HuggingFace model downloads.</li>
</ul>
<h3 id="settings_configuration-ai-vram-controls">AI / VRAM Controls</h3>
<p>Settings -&gt; Majoor Assets Manager -&gt; Search -&gt; AI exposes the runtime controls for Majoor's local AI models:</p>
<ul>
<li><strong>Enable AI semantic search</strong> maps to <code>MJR_AM_ENABLE_VECTOR_SEARCH</code>.</li>
<li><strong>Index vectors during scans</strong> maps to <code>MJR_AM_VECTOR_INDEX_ON_SCAN</code>.</li>
<li><strong>Generate AI captions during indexing</strong> maps to <code>MJR_AM_VECTOR_CAPTION_ON_INDEX</code>.</li>
<li><strong>Vector indexing concurrency</strong> maps to <code>MJR_VECTOR_CONCURRENCY</code>.</li>
<li><strong>Unload AI models after use</strong> maps to <code>MJR_AM_VECTOR_UNLOAD_AFTER_USE</code>.</li>
<li><strong>Memory purge now</strong> and <strong>Memory purge</strong> call <code>/mjr/am/settings/vector-search/unload</code>.</li>
</ul>
<p>For low-VRAM ComfyUI systems, disable semantic search and automatic vector work, keep concurrency at <code>1</code>, and unload Majoor AI models after use. The manual purge action releases Majoor SigLIP/X-CLIP/Florence references, asks ComfyUI to unload loaded models, and clears torch CUDA cache when the queue is idle.</p>
<h3 id="settings_configuration-environment-variables-backend">Environment Variables (Backend)</h3>
<h4 id="settings_configuration-directory-configuration">Directory Configuration</h4>
<ul>
<li><strong>MAJOOR_OUTPUT_DIRECTORY</strong> / <strong>MJR_AM_OUTPUT_DIRECTORY</strong>: Override default output directory</li>
<ul>
<li>Default: ComfyUI's output directory</li>
<li>Format: Full path to directory</li>
<li>Impact: Changes where the indexer looks for assets</li>
<li>Example: <code>MAJOOR_OUTPUT_DIRECTORY=/path/to/my/output</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_AM_INDEX_DIRECTORY</strong> / <strong>MAJOOR_INDEX_DIRECTORY</strong>: Override the directory where the SQLite index database is stored</li>
<ul>
<li>Default: <code>&amp;lt;output_directory&amp;gt;/_mjr_index/</code></li>
<li>Format: Full absolute path to an existing or creatable directory</li>
<li>Impact: Moves the index DB (and vectors DB) to a different disk or path; assets themselves stay in the output directory</li>
<li>Takes effect: at ComfyUI startup (or restart after saving from the UI)</li>
<li>Example: <code>MJR_AM_INDEX_DIRECTORY=C:\mjr_index</code></li>
<li>Example: <code>MJR_AM_INDEX_DIRECTORY=/var/local/mjr_index</code></li>
<li>Priority: env var → sidecar file (<code>.mjr_index_directory_override</code>) → default</li>
<li>Also configurable from: Settings → Paths → Majoor: Index Directory (persists to the sidecar file)</li>
</ul>
</ul>
<h4 id="settings_configuration-external-tool-paths">External Tool Paths</h4>
<ul>
<li><strong>MAJOOR_EXIFTOOL_PATH</strong> / <strong>MAJOOR_EXIFTOOL_BIN</strong>: Path to ExifTool executable</li>
<ul>
<li>Default: <code>exiftool</code> (assumes in PATH)</li>
<li>Format: Full path to exiftool executable</li>
<li>Impact: Used for metadata extraction and file tagging</li>
<li>Example: <code>MAJOOR_EXIFTOOL_PATH=/usr/local/bin/exiftool</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_FFPROBE_PATH</strong> / <strong>MAJOOR_FFPROBE_BIN</strong>: Path to FFprobe executable</li>
<ul>
<li>Default: <code>ffprobe</code> (assumes in PATH)</li>
<li>Format: Full path to ffprobe executable</li>
<li>Impact: Used for video/audio metadata extraction</li>
<li>Example: <code>MAJOOR_FFPROBE_PATH=/usr/local/bin/ffprobe</code></li>
</ul>
</ul>
<h4 id="settings_configuration-media-processing">Media Processing</h4>
<ul>
<li><strong>MAJOOR_MEDIA_PROBE_BACKEND</strong>: Media extraction backend selection</li>
<ul>
<li>Options: <code>auto</code>, <code>exiftool</code>, <code>ffprobe</code>, <code>both</code></li>
<li>Default: <code>auto</code></li>
<li>Impact: Determines which tools are used for metadata extraction</li>
<li>Example: <code>MAJOOR_MEDIA_PROBE_BACKEND=both</code></li>
</ul>
</ul>
<h4 id="settings_configuration-database-tuning">Database Tuning</h4>
<ul>
<li><strong>MAJOOR_DB_TIMEOUT</strong>: Database operation timeout in seconds</li>
<ul>
<li>Default: 30.0</li>
<li>Range: 1.0 to 300.0</li>
<li>Impact: Maximum time to wait for database operations</li>
<li>Example: <code>MAJOOR_DB_TIMEOUT=60.0</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_DB_MAX_CONNECTIONS</strong>: Maximum database connections</li>
<ul>
<li>Default: 8</li>
<li>Range: 1 to 50</li>
<li>Impact: Concurrency level for database operations</li>
<li>Example: <code>MAJOOR_DB_MAX_CONNECTIONS=12</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_DB_QUERY_TIMEOUT</strong>: Maximum query execution time</li>
<ul>
<li>Default: 60.0 seconds</li>
<li>Range: 1.0 to 300.0</li>
<li>Impact: Prevents long-running queries from blocking</li>
<li>Example: <code>MAJOOR_DB_QUERY_TIMEOUT=45.0</code></li>
</ul>
</ul>
<h4 id="settings_configuration-performance-tuning">Performance Tuning</h4>
<ul>
<li><strong>MAJOOR_TO_THREAD_TIMEOUT</strong>: Timeout for background thread operations</li>
<ul>
<li>Default: 30 seconds</li>
<li>Range: 10 to 600 seconds</li>
<li>Impact: Maximum time for background operations</li>
<li>Example: <code>MAJOOR_TO_THREAD_TIMEOUT=60</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_MAX_METADATA_JSON_BYTES</strong>: Maximum metadata JSON size</li>
<ul>
<li>Default: 2097152 (2MB)</li>
<li>Range: 1024 to 104857600 (100MB)</li>
<li>Impact: Limits memory usage for metadata storage</li>
<li>Example: <code>MAJOOR_MAX_METADATA_JSON_BYTES=4194304</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_AM_ENABLE_VECTOR_SEARCH</strong>: Enable AI semantic/vector features</li>
<ul>
<li>Default: <code>1</code></li>
<li>Impact: When enabled, semantic search, find-similar, prompt alignment, AI tags, and captions can load local AI models</li>
<li>Low-VRAM recommendation: <code>0</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_AM_VECTOR_INDEX_ON_SCAN</strong>: Compute vector embeddings during scans</li>
<ul>
<li>Default: <code>0</code></li>
<li>Impact: Can load SigLIP/X-CLIP during scan workflows</li>
<li>Low-VRAM recommendation: <code>0</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_AM_VECTOR_CAPTION_ON_INDEX</strong>: Generate Florence captions during vector indexing/backfill</li>
<ul>
<li>Default: <code>0</code></li>
<li>Impact: Florence is heavier and can use significant VRAM/CPU</li>
<li>Low-VRAM recommendation: <code>0</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_VECTOR_CONCURRENCY</strong> / <strong>MJR_AM_VECTOR_CONCURRENCY</strong>: Concurrent vector embedding workers</li>
<ul>
<li>Default: <code>2</code></li>
<li>Range: <code>1</code> to <code>16</code></li>
<li>Impact: Higher values can improve throughput but increase transient VRAM spikes</li>
<li>Low-VRAM recommendation: <code>1</code></li>
</ul>
</ul>
<ul>
<li><strong>MJR_AM_VECTOR_UNLOAD_AFTER_USE</strong>: Unload Majoor AI models after heavy vector actions</li>
<ul>
<li>Default: <code>0</code></li>
<li>Impact: Frees VRAM after semantic search/backfill/caption/index actions, but the next AI action reloads models and is slower</li>
<li>Low-VRAM recommendation: <code>1</code></li>
</ul>
</ul>
<h4 id="settings_configuration-collection-management">Collection Management</h4>
<ul>
<li><strong>MJR_COLLECTION_MAX_ITEMS</strong>: Maximum items per collection</li>
<ul>
<li>Default: 50000</li>
<li>Range: 1000 to 1000000</li>
<li>Impact: Prevents extremely large collections from impacting performance</li>
<li>Example: <code>MJR_COLLECTION_MAX_ITEMS=100000</code></li>
</ul>
</ul>
<h4 id="settings_configuration-security-networking">Security &amp; Networking</h4>
<ul>
<li><strong>MJR_ALLOW_SYMLINKS</strong>: Allow symbolic links in custom roots</li>
<ul>
<li>Options: <code>on</code>, <code>off</code>, <code>true</code>, <code>false</code></li>
<li>Default: <code>off</code></li>
<li>Impact: Enables browsing of linked directories</li>
<li>Example: <code>MJR_ALLOW_SYMLINKS=on</code></li>
</ul>
</ul>
<ul>
<li><strong>MAJOOR_TRUSTED_PROXIES</strong>: IPs/CIDRs allowed for forwarded headers</li>
<ul>
<li>Default: <code>127.0.0.1,::1</code></li>
<li>Format: Comma-separated IP addresses or CIDR blocks</li>
<li>Impact: Security setting for proxy environments</li>
<li>Example: <code>MAJOOR_TRUSTED_PROXIES=127.0.0.1,192.168.1.0/24</code></li>
</ul>
</ul>
<h4 id="settings_configuration-dependency-management">Dependency Management</h4>
<ul>
<li><strong>MJR_AM_NO_AUTO_PIP</strong>: Disable automatic dependency installation</li>
<ul>
<li>Options: <code>1</code>, <code>true</code>, <code>yes</code>, <code>on</code> to disable</li>
<li>Default: Automatic installation enabled</li>
<li>Impact: Prevents automatic pip installs at startup</li>
<li>Example: <code>MJR_AM_NO_AUTO_PIP=1</code></li>
</ul>
</ul>
<h2 id="settings_configuration-setting-up-environment-variables">Setting Up Environment Variables</h2>
<h3 id="settings_configuration-windows">Windows</h3>
<p>Create a batch file to set environment variables:</p>
<pre><code class="language-batch">@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</code></pre>
<h3 id="settings_configuration-unixlinuxmacos">Unix/Linux/macOS</h3>
<p>Create a shell script to set environment variables:</p>
<pre><code class="language-bash">#!/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</code></pre>
<p>Or add to your shell profile (<code>.bashrc</code>, <code>.zshrc</code>, etc.):</p>
<pre><code class="language-bash">export MAJOOR_MEDIA_PROBE_BACKEND=auto
export MAJOOR_DB_TIMEOUT=60.0</code></pre>
<h2 id="settings_configuration-configuration-best-practices">Configuration Best Practices</h2>
<h3 id="settings_configuration-performance-optimization">Performance Optimization</h3>
<ul>
<li>Adjust page size based on your system's RAM</li>
<li>Set appropriate timeouts based on your storage speed</li>
<li>Monitor database performance and adjust connections as needed</li>
</ul>
<h3 id="settings_configuration-security-considerations">Security Considerations</h3>
<ul>
<li>Keep default security settings unless you have specific requirements</li>
<li>Only enable symlink support if you trust the linked directories</li>
<li>Configure trusted proxies appropriately in networked environments</li>
<li>Regularly update external tools (ExifTool, FFprobe) for security</li>
</ul>
<h3 id="settings_configuration-resource-management">Resource Management</h3>
<ul>
<li>Monitor memory usage with large collections</li>
<li>Adjust cache settings based on available RAM</li>
<li>Set appropriate timeouts for your storage system</li>
<li>Consider SSD storage for better database performance</li>
</ul>
<h3 id="settings_configuration-backup-and-recovery">Backup and Recovery</h3>
<ul>
<li>Regularly backup the index database (<code>assets.sqlite</code>)</li>
<li>Backup custom root configurations</li>
<li>Maintain copies of important collections</li>
<li>Document your configuration settings for recovery</li>
</ul>
<h2 id="settings_configuration-troubleshooting-configuration-issues">Troubleshooting Configuration Issues</h2>
<h3 id="settings_configuration-common-problems">Common Problems</h3>
<h4 id="settings_configuration-settings-not-saving">Settings Not Saving</h4>
<ul>
<li>Clear browser cache and cookies</li>
<li>Check browser localStorage quota</li>
<li>Verify no browser extensions are interfering</li>
<li>Try a different browser</li>
</ul>
<h4 id="settings_configuration-performance-issues">Performance Issues</h4>
<ul>
<li>Reduce page size if experiencing slowdowns</li>
<li>Disable auto-scan if not needed</li>
<li>Adjust database connection settings</li>
<li>Check system resources (RAM, disk space)</li>
</ul>
<h4 id="settings_configuration-external-tool-issues">External Tool Issues</h4>
<ul>
<li>Verify tools are in PATH or set explicit paths</li>
<li>Check tool permissions and access rights</li>
<li>Ensure tools are properly installed</li>
<li>Test tools independently before using with Assets Manager</li>
</ul>
<h3 id="settings_configuration-diagnostic-steps">Diagnostic Steps</h3>
<ol>
<li>Check ComfyUI console for error messages</li>
<li>Verify all required dependencies are installed</li>
<li>Test external tools independently</li>
<li>Review environment variable settings</li>
<li>Try default settings to isolate configuration issues</li>
</ol>
<h2 id="settings_configuration-migration-and-updates">Migration and Updates</h2>
<h3 id="settings_configuration-settings-preservation">Settings Preservation</h3>
<ul>
<li>Browser settings are preserved across updates</li>
<li>Environment variables need to be reapplied after system restarts</li>
<li>Custom root configurations are stored in the index directory</li>
<li>Collections are preserved as JSON files</li>
</ul>
<h3 id="settings_configuration-configuration-updates">Configuration Updates</h3>
<ul>
<li>New settings are added with sensible defaults</li>
<li>Old settings remain unchanged during updates</li>
<li>Review new settings after updates to optimize functionality</li>
<li>Check release notes for configuration changes</li>
</ul>
<hr />
<p>_Settings &amp; Configuration Guide Version: 1.0_ _Last Updated: April 5, 2026_</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/SETTINGS_CONFIGURATION.md</code>
</div>
</div>
</section>
<section class="panel section" id="shortcuts">
<div class="panel-h"><h2>SHORTCUTS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="shortcuts-keyboard-shortcuts">Keyboard Shortcuts</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: May 18, 2026</p>
<p>Quick reference for Majoor Assets Manager shortcuts. For full details including MFV shortcuts, see <a href="#hotkeys_shortcuts">HOTKEYS_SHORTCUTS.md</a>.</p>
<h2 id="shortcuts-grid-view">Grid View</h2>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>1-5</strong></td><td>Set Rating</td></tr>
<tr><td><strong>0</strong></td><td>Clear Rating</td></tr>
<tr><td><strong>Enter / Space</strong></td><td>Open Viewer</td></tr>
<tr><td><strong>V</strong></td><td>Open in Floating Viewer</td></tr>
<tr><td><strong>S</strong></td><td>Download</td></tr>
<tr><td><strong>D</strong></td><td>Toggle Details Sidebar</td></tr>
<tr><td><strong>T</strong></td><td>Edit Tags</td></tr>
<tr><td><strong>B / Shift+B</strong></td><td>Add/Remove Collection</td></tr>
<tr><td><strong>Ctrl+A / Ctrl+D</strong></td><td>Select/Deselect All</td></tr>
<tr><td><strong>F2</strong></td><td>Rename</td></tr>
<tr><td><strong>Del</strong></td><td>Delete</td></tr>
<tr><td><strong>Ctrl+Shift+C</strong></td><td>Copy Path</td></tr>
<tr><td><strong>Ctrl+Shift+E</strong></td><td>Open in Explorer</td></tr>
<tr><td><strong>Ctrl+F / Ctrl+K</strong></td><td>Focus Search</td></tr>
</tbody></table></div>
<h2 id="shortcuts-quick-filter-buttons">Quick Filter Buttons</h2>
<div class="table-wrap"><table>
<thead><tr><th>Control</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Image icon button</strong></td><td>Toggle Image-only quick filter</td></tr>
<tr><td><strong>Video icon button</strong></td><td>Toggle Video-only quick filter</td></tr>
<tr><td><strong>Audio icon button</strong></td><td>Toggle Audio-only quick filter</td></tr>
<tr><td><strong>3D icon button</strong></td><td>Toggle 3D-only quick filter</td></tr>
</tbody></table></div>
<h2 id="shortcuts-drag-drop">Drag &amp; Drop</h2>
<div class="table-wrap"><table>
<thead><tr><th>Gesture</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>S+Drag to OS</strong></td><td>Export clean copy/ZIP without ComfyUI metadata</td></tr>
<tr><td><strong>L+Drop on canvas</strong></td><td>Load selected asset(s) by creating matching media loader node(s)</td></tr>
</tbody></table></div>
<h2 id="shortcuts-viewer">Viewer</h2>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Esc</strong></td><td>Close Viewer</td></tr>
<tr><td><strong>Space</strong></td><td>Play/Pause</td></tr>
<tr><td><strong>Left Arrow / Right Arrow</strong></td><td>Prev/Next Asset, or step frame when the focused MFV player is active</td></tr>
<tr><td><strong>F</strong></td><td>Fullscreen</td></tr>
<tr><td><strong>D</strong></td><td>Info Panel</td></tr>
<tr><td><strong>I</strong></td><td>Set In Point (video) / Pixel Probe (image)</td></tr>
<tr><td><strong>O</strong></td><td>Set Out Point (video)</td></tr>
<tr><td><strong>Home</strong></td><td>Go to In Point (video)</td></tr>
<tr><td><strong>End</strong></td><td>Go to Out Point (video)</td></tr>
<tr><td><strong>[ / ]</strong></td><td>Decrease/Increase playback speed</td></tr>
<tr><td><strong>\\</strong></td><td>Reset playback speed to 1x</td></tr>
<tr><td><strong>L</strong></td><td>Loupe</td></tr>
<tr><td><strong>Z</strong></td><td>Zebra</td></tr>
<tr><td><strong>G</strong></td><td>Grid Overlay</td></tr>
<tr><td><strong>Alt+1</strong></td><td>1:1 View</td></tr>
<tr><td><strong>+ / -</strong></td><td>Zoom</td></tr>
<tr><td><strong>S</strong></td><td>Download original from context menu</td></tr>
</tbody></table></div></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/SHORTCUTS.md</code>
</div>
</div>
</section>
<section class="panel section" id="testing">
<div class="panel-h"><h2>TESTING.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="testing-testing">Testing</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 5, 2026</p>
<p>This project uses <strong>pytest</strong> (backend) and <strong>Vitest</strong> (frontend) for comprehensive test coverage. On Windows, batch runners are provided for convenience and generate both:</p>
<ul>
<li>JUnit XML (<code>.xml</code>)</li>
<li>Styled HTML report (<code>.html</code>)</li>
</ul>
<p>Before running the quality gate or local tests, install contributor tooling:</p>
<pre><code class="language-bash">pip install -r requirements-dev.txt
python scripts/install_local_hooks.py</code></pre>
<p>Runtime dependencies stay in <code>requirements.txt</code>; optional AI/vector dependencies stay in <code>requirements-vector.txt</code>. See <code>docs/DEPENDENCY_POLICY.md</code> for the canonical dependency policy.</p>
<p>Reports are written to:</p>
<ul>
<li><code>tests/__reports__/</code></li>
</ul>
<p>Open the index:</p>
<ul>
<li><code>tests/__reports__/index.html</code></li>
</ul>
<h2 id="testing-test-coverage">Test Coverage</h2>
<h3 id="testing-backend-python">Backend (Python)</h3>
<ul>
<li>Core functionality (index, search, routes)</li>
<li>Metadata extraction (ExifTool, FFprobe, geninfo)</li>
<li>Database operations (schema, migrations)</li>
<li>Security (CSRF, auth, path validation)</li>
<li>Feature tests (collections, batch ZIP, viewer)</li>
<li>Regression tests</li>
</ul>
<h3 id="testing-frontend-javascript">Frontend (JavaScript)</h3>
<ul>
<li>Vue.js components</li>
<li>API client</li>
<li>UI utilities</li>
<li>Event handling</li>
<li>Drag &amp; drop</li>
</ul>
<hr />
<h2 id="testing-quick-commands">Quick Commands</h2>
<h3 id="testing-all-tests-cross-platform">All Tests (Cross-Platform)</h3>
<pre><code class="language-bash"># 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</code></pre>
<h3 id="testing-single-test-file">Single Test File</h3>
<pre><code class="language-bash">python -m pytest tests/core/test_routes.py -v</code></pre>
<h3 id="testing-single-test-folder">Single Test Folder</h3>
<pre><code class="language-bash">python -m pytest tests/metadata/ -v</code></pre>
<h3 id="testing-single-test-function">Single Test Function</h3>
<pre><code class="language-bash">python -m pytest tests/core/test_routes.py::test_routes -v</code></pre>
<h3 id="testing-frontend-tests">Frontend Tests</h3>
<pre><code class="language-bash"># Run JavaScript tests
npm run test:js
# Watch mode
npm run test:js:watch</code></pre>
<h3 id="testing-canonical-quality-gate">Canonical Quality Gate</h3>
<pre><code class="language-bash"># 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</code></pre>
<p>The canonical gate runs encoding/BOM checks, <code>ruff</code>, <code>mypy</code>, <code>bandit</code>, <code>pip-audit</code>, xenon/radon complexity checks, backend tests, frontend tests, and <code>npm audit</code>. The <code>pip-audit</code> step audits <code>requirements.txt</code> 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.</p>
<p>The Python coverage gate currently enforces a minimum combined backend/shared coverage threshold of <code>60%</code>, which gives the CI pipeline a regression floor without making legacy cleanup block unrelated work.</p>
<p>During the migration to stricter quality thresholds, <code>ruff</code> is enforced with autofix on commit for changed Python files, while <code>mypy</code> and the changed-file complexity gate run on pre-push. The changed-file complexity threshold is aligned locally with the CI limit of <code>25</code>, and repository-wide hygiene, security, and complexity checks continue to run across the repo.</p>
<h2 id="testing-local-hooks">Local Hooks</h2>
<p>This repository ships local Git hooks via <code>pre-commit</code>:</p>
<pre><code class="language-bash">python scripts/install_local_hooks.py</code></pre>
<p>Installed behavior:</p>
<ul>
<li><code>pre-commit</code>: BOM/encoding hygiene, <code>ruff --fix</code>, <code>eslint --fix</code>, <code>prettier --write</code></li>
<li><code>pre-push</code>: mandatory <code>mypy</code> plus <code>scripts/run_changed_quality_gate.py</code></li>
</ul>
<p>The changed-file quality gate is also runnable manually:</p>
<pre><code class="language-bash">python scripts/run_changed_quality_gate.py</code></pre>
<p>For frontend tests in the Node Vitest environment, prefer the shared helpers in <code>ui/tests/helpers/vitestEnvironment.mjs</code>. Team rule: use partial Vue mocks by default, based on the real <code>vue</code> module, instead of full replacement mocks.</p>
<h2 id="testing-batch-runners-windows">Batch runners (Windows)</h2>
<p>From the repo root:</p>
<ul>
<li>Full suite: <code>run_tests.bat</code> (delegates to <code>tests/run_tests_all.bat</code>)</li>
</ul>
<p>Category runners:</p>
<ul>
<li><code>tests/config/run_tests_config.bat</code></li>
<li><code>tests/core/run_tests_core.bat</code></li>
<li><code>tests/database/run_tests_database.bat</code></li>
<li><code>tests/features/run_tests_features.bat</code></li>
<li><code>tests/metadata/run_tests_metadata.bat</code></li>
<li><code>tests/rating_tags/run_tests_rating_tags.bat</code></li>
<li><code>tests/regressions/run_tests_regressions.bat</code></li>
</ul>
<p>All batch runners support <code>/nopause</code> for non-interactive runs:</p>
<pre><code class="language-bat">run_tests.bat /nopause</code></pre>
<h2 id="testing-test-artifacts-dbwalshm">Test artifacts (DB/WAL/SHM)</h2>
<p>Some tests create SQLite files. Pytest temp files are stored under:</p>
<ul>
<li><code>tests/__pytest_tmp__/</code></li>
</ul>
<p>This includes <code><em>.db</code>, <code></em>.db-wal</code>, <code>*.db-shm</code>, and other runtime artifacts.</p>
<h2 id="testing-parser-samples-metadata-extraction">Parser samples (metadata extraction)</h2>
<p><code>tests/metadata/test_parser_folder_scan.py</code> scans <strong>every file</strong> under:</p>
<ul>
<li><code>tests/parser/</code> (recursive)</li>
</ul>
<p>If you don't want to commit large samples, you can point the test to an external folder:</p>
<pre><code class="language-powershell">$env:MJR_TEST_PARSER_DIR = "C:\path\to\parser"
python -m pytest tests/metadata/test_parser_folder_scan.py -q</code></pre></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/TESTING.md</code>
</div>
</div>
</section>
<section class="panel section" id="threat_model">
<div class="panel-h"><h2>THREAT_MODEL.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="threat_model-threat-model-majoor-assets-manager">Threat Model — Majoor Assets Manager</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Date</strong>: April 5, 2026 (Updated from 2026-01-24)</p>
<h2 id="threat_model-scope">Scope</h2>
<p>This model covers the Majoor Assets Manager backend routes under <code>/mjr/am/*</code> and the associated local filesystem/database operations.</p>
<p><strong>Recent highlights</strong>: Enhanced API token authentication, improved CSRF protection, and better rate limiting for remote access scenarios.</p>
<h2 id="threat_model-assets-to-protect">Assets to Protect</h2>
<ul>
<li>User files on disk (input/output/custom roots).</li>
<li>SQLite index database and collections JSON files.</li>
<li>ComfyUI server availability (avoid crashes / runaway CPU / locks).</li>
<li>Privacy of metadata (prompts/workflows embedded in images/videos).</li>
</ul>
<h2 id="threat_model-trust-boundaries">Trust Boundaries</h2>
<ul>
<li><strong>Browser/UI</strong> → <strong>ComfyUI HTTP server</strong>: untrusted inputs via query params and JSON bodies.</li>
<li><strong>Extension backend</strong> → <strong>Filesystem</strong>: path traversal and symlink/junction concerns.</li>
<li><strong>Extension backend</strong> → <strong>External tools</strong> (<code>exiftool</code>, <code>ffprobe</code>): command execution and parsing.</li>
<li><strong>Extension backend</strong> → <strong>SQLite</strong>: injection/locking/corruption risks.</li>
</ul>
<h2 id="threat_model-threats-mitigations">Threats &amp; Mitigations</h2>
<h3 id="threat_model-1-path-traversal-unauthorized-file-access">1) Path Traversal / Unauthorized File Access</h3>
<p><strong>Threat:</strong> An attacker (or malicious workflow) calls <code>/mjr/am/*</code> with paths pointing outside allowed roots.</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Normalize paths and reject invalid/unsafe paths (<code>_safe_rel_path</code>, <code>_is_within_root</code>, allow-lists).</li>
<li>Exception: <code>/mjr/am/open-in-folder</code> may reveal any existing local path in the OS file manager when the action is enabled. It does not provide file content reads, writes, deletes, downloads, or indexing access.</li>
<li>Custom roots are resolved and validated (root-id → canonical directory).</li>
<li>Avoid exposing arbitrary “open file” primitives; use explicit allow-listed actions.</li>
</ul>
<h3 id="threat_model-2-symlinkjunction-escape-toctou">2) Symlink/Junction Escape (TOCTOU)</h3>
<p><strong>Threat:</strong> A path initially validated as within root later resolves to a different location (symlink swap).</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Resolve roots/targets with <code>resolve(strict=True)</code> where possible.</li>
<li>Validate after resolution and before use.</li>
<li>Treat mid-scan disappearance as a benign failure and continue (best-effort).</li>
</ul>
<h3 id="threat_model-3-csrf-cross-site-requests">3) CSRF / Cross-Site Requests</h3>
<p><strong>Threat:</strong> A local web page triggers state-changing endpoints (delete/rename/stage) against <code>localhost</code>.</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>CSRF checks on state-changing routes.</li>
<li>Prefer returning <code>Result.Err("CSRF", ...)</code> instead of throwing.</li>
</ul>
<h3 id="threat_model-4-denial-of-service-large-listings-heavy-scans">4) Denial of Service (Large Listings / Heavy Scans)</h3>
<p><strong>Threat:</strong> Repeated list/index requests cause CPU/disk thrash.</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Bounded queues for background work (scan pending max).</li>
<li>Rate limiting for expensive endpoints.</li>
<li>TTL-based filesystem listing cache with bounded size.</li>
</ul>
<h3 id="threat_model-5-external-tool-execution-risks">5) External Tool Execution Risks</h3>
<p><strong>Threat:</strong> Tool invocation on untrusted media can be slow or crash; tool may be missing.</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Timeouts and best-effort fallbacks (<code>code="DEGRADED"</code>).</li>
<li>Never crash UI: return <code>Result.Err/Ok</code> with degraded metadata.</li>
<li>Avoid shell string interpolation; prefer argv lists.</li>
</ul>
<h3 id="threat_model-6-data-corruption-concurrent-writes">6) Data Corruption / Concurrent Writes</h3>
<p><strong>Threat:</strong> Concurrent updates corrupt JSON stores or SQLite schema state.</p>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Atomic writes for JSON stores (write tmp + replace).</li>
<li>SQLite locking strategy and bounded transactions.</li>
</ul>
<h2 id="threat_model-residual-risk">Residual Risk</h2>
<ul>
<li>Localhost endpoints are inherently accessible to local malware.</li>
<li>Media parsing remains a complex attack surface (mitigated by timeouts and least-privilege execution).</li>
</ul>
<h2 id="threat_model-non-goals">Non-Goals</h2>
<ul>
<li>Remote multi-user authentication/authorization (ComfyUI is typically local-only).</li>
<li>Protecting against a fully compromised host OS.</li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/THREAT_MODEL.md</code>
</div>
</div>
</section>
<section class="panel section" id="viewer_feature_tutorial">
<div class="panel-h"><h2>VIEWER_FEATURE_TUTORIAL.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="viewer_feature_tutorial-majoor-assets-manager-viewer-feature-tutorial">Majoor Assets Manager - Viewer Feature Tutorial</h1>
<p><strong>Version</strong>: 2.4.9 <strong>Last Updated</strong>: April 15, 2026</p>
<h2 id="viewer_feature_tutorial-overview">Overview</h2>
<p>For a dedicated Majoor Floating Viewer walkthrough with focused screenshots, see <a href="#mfv_guide">MFV_GUIDE.md</a>.</p>
<p>The Majoor Assets Manager provides <strong>two viewer experiences</strong>:</p>
<h3 id="viewer_feature_tutorial-1-majoor-floating-viewer-mfv-new">1. Majoor Floating Viewer (MFV) — NEW! 🎉</h3>
<p>A lightweight, draggable floating panel for <strong>real-time generation comparison</strong>:</p>
<ul>
<li><strong>Live Stream Mode</strong>: Automatically follows final generation outputs and is <strong>enabled by default</strong></li>
<li><strong>KSampler Preview</strong>: Streams denoising-step previews during execution and is <strong>enabled by default</strong></li>
<li><strong>Compare Modes</strong>: Simple, A/B Compare, Side-by-Side, Grid Compare (up to 4 assets)</li>
<li><strong>Multi-Pin References (A/B/C/D)</strong>: Pin up to 4 images simultaneously for comparison</li>
<li><strong>Node Parameters Sidebar</strong>: Edit workflow node widgets directly in the viewer</li>
<li><strong>Run Button</strong>: Queue prompt from viewer toolbar without switching to canvas</li>
<li><strong>Node Stream</strong>: Click nodes to preview available frontend media, including ImageOps live previews</li>
<li><strong>Real-time Updates</strong>: Watch generations as they complete</li>
<li><strong>Portable</strong>: Move anywhere on screen, resize, dock</li>
</ul>
<p><strong>Best for</strong>: Real-time workflow monitoring, quick comparisons, node preview, parameter tweaking</p>
<h3 id="viewer_feature_tutorial-2-standard-viewer">2. Standard Viewer</h3>
<p>A full-featured overlay viewer with <strong>advanced analysis tools</strong>:</p>
<ul>
<li><strong>Enhancement Tools</strong>: Exposure, gamma, channel isolation</li>
<li><strong>Analysis Tools</strong>: False color, zebra, histogram, waveform, vectorscope</li>
<li><strong>Visual Overlays</strong>: Grid, pixel probe, loupe</li>
<li><strong>Video Controls</strong>: Timeline, in/out points, speed control</li>
</ul>
<p><strong>Best for</strong>: Detailed analysis, quality inspection, metadata review</p>
<hr />
<h2 id="viewer_feature_tutorial-table-of-contents">Table of Contents</h2>
<h3 id="viewer_feature_tutorial-majoor-floating-viewer-mfv">Majoor Floating Viewer (MFV)</h3>
<ul>
<li><a href="#viewer_feature_tutorial-opening-the-floating-viewer">Opening the Floating Viewer</a></li>
<li><a href="#viewer_feature_tutorial-live-stream-mode">Live Stream Mode</a></li>
<li><a href="#viewer_feature_tutorial-compare-modes">Compare Modes</a></li>
<li><a href="#viewer_feature_tutorial-multi-pin-references-abcd">Multi-Pin References (A/B/C/D)</a></li>
<li><a href="#viewer_feature_tutorial-node-parameters-sidebar">Node Parameters Sidebar</a></li>
<li><a href="#viewer_feature_tutorial-sidebar-position-setting">Sidebar Position Setting</a></li>
<li><a href="#viewer_feature_tutorial-node-stream">Node Stream</a></li>
<li><a href="#viewer_feature_tutorial-mfv-controls">MFV Controls</a></li>
<li><a href="#viewer_feature_tutorial-mfv-keyboard-shortcuts">MFV Keyboard Shortcuts</a></li>
</ul>
<h3 id="viewer_feature_tutorial-standard-viewer">Standard Viewer</h3>
<ul>
<li><a href="#viewer_feature_tutorial-opening-the-standard-viewer">Opening the Standard Viewer</a></li>
<li><a href="#viewer_feature_tutorial-viewer-layout">Viewer Layout</a></li>
<li><a href="#viewer_feature_tutorial-viewer-navigation">Viewer Navigation</a></li>
<li><a href="#viewer_feature_tutorial-image-enhancement-tools">Image Enhancement Tools</a></li>
<li><a href="#viewer_feature_tutorial-analysis-tools">Analysis Tools</a></li>
<li><a href="#viewer_feature_tutorial-visual-overlays">Visual Overlays</a></li>
<li><a href="#viewer_feature_tutorial-professional-analysis-tools-scopes">Professional Analysis Tools (Scopes)</a></li>
<li><a href="#viewer_feature_tutorial-video-specific-features">Video-Specific Features</a></li>
<li><a href="#viewer_feature_tutorial-export-capabilities">Export Capabilities</a></li>
<li><a href="#viewer_feature_tutorial-standard-viewer-hotkeys">Standard Viewer Hotkeys</a></li>
</ul>
<h3 id="viewer_feature_tutorial-both-viewers">Both Viewers</h3>
<ul>
<li><a href="#viewer_feature_tutorial-troubleshooting-viewer-issues">Troubleshooting</a></li>
<li><a href="#viewer_feature_tutorial-best-practices">Best Practices</a></li>
</ul>
<hr />
<h2 id="viewer_feature_tutorial-majoor-floating-viewer-mfv">Majoor Floating Viewer (MFV)</h2>
<h3 id="viewer_feature_tutorial-opening-the-floating-viewer">Opening the Floating Viewer</h3>
<p>There are several ways to open the Majoor Floating Viewer:</p>
<h4 id="viewer_feature_tutorial-method-1-toolbar-button">Method 1: Toolbar Button</h4>
<ol>
<li>Open the Assets Manager panel</li>
<li>Click the <strong>MFV</strong> button in the toolbar (icon: stacked images)</li>
<li>The MFV panel appears on screen</li>
</ol>
<h4 id="viewer_feature_tutorial-method-2-context-menu">Method 2: Context Menu</h4>
<ol>
<li>Right-click on any asset in the grid</li>
<li>Select <strong>"Open in Floating Viewer"</strong></li>
<li>The MFV panel opens with that asset</li>
</ol>
<h4 id="viewer_feature_tutorial-method-3-node-stream">Method 3: Node Stream</h4>
<ol>
<li>Open the MFV</li>
<li>Enable <strong>Node Stream</strong> or press <strong>N</strong></li>
<li>Click a node in ComfyUI to show its available preview media</li>
</ol>
<h4 id="viewer_feature_tutorial-method-4-keyboard-shortcut">Method 4: Keyboard Shortcut</h4>
<ul>
<li>Press <strong>V</strong> while the Assets Manager panel is hovered or focused to toggle the Floating Viewer</li>
</ul>
<h3 id="viewer_feature_tutorial-live-stream-mode">Live Stream Mode</h3>
<p><strong>Live Stream Mode</strong> tracks final generation outputs after workflow execution.</p>
<p><strong>Default behavior</strong>:</p>
<ul>
<li><strong>Live Stream</strong> starts <strong>enabled by default</strong></li>
<li><strong>KSampler Preview</strong> also starts <strong>enabled by default</strong></li>
<li>You can change both in <strong>Settings → Majoor Assets Manager › Viewer</strong></li>
</ul>
<h4 id="viewer_feature_tutorial-enabling-live-stream">Enabling Live Stream</h4>
<ol>
<li>Open the Floating Viewer</li>
<li>Click the <strong>Live Stream</strong> button (icon: broadcast tower)</li>
<li>Or press <strong>L</strong> to toggle</li>
</ol>
<h4 id="viewer_feature_tutorial-how-it-works">How It Works</h4>
<ul>
<li>Monitors ComfyUI generation output events</li>
<li>Automatically switches to newly generated output files</li>
<li>Does not follow canvas node selection; use <strong>Node Stream</strong> for selected nodes</li>
<li>No manual refresh needed after execution completes</li>
</ul>
<h4 id="viewer_feature_tutorial-use-cases">Use Cases</h4>
<ul>
<li><strong>Workflow Development</strong>: Watch final outputs as runs complete</li>
<li><strong>Batch Generation</strong>: Monitor progress across multiple prompts</li>
<li><strong>Parameter Tuning</strong>: Compare completed runs</li>
<li><strong>Queue Monitoring</strong>: Keep the latest output visible while prompts execute</li>
</ul>
<h3 id="viewer_feature_tutorial-compare-modes">Compare Modes</h3>
<p>The MFV supports a single-asset view plus three comparison workflows:</p>
<h4 id="viewer_feature_tutorial-simple-mode-default">Simple Mode (Default)</h4>
<ul>
<li>Single image display</li>
<li>Full resolution preview</li>
<li>Standard navigation</li>
</ul>
<p><strong>Shortcut</strong>: Press <strong>C</strong> until Simple mode is active again</p>
<h4 id="viewer_feature_tutorial-ab-compare-mode">A/B Compare Mode</h4>
<ul>
<li>Toggle between two images rapidly</li>
<li>Click asset to set as A or B</li>
<li>Useful for subtle difference detection</li>
</ul>
<p><strong>How to Use</strong>:</p>
<ol>
<li>Enable A/B Compare mode (press <strong>C</strong> from Simple mode)</li>
<li>Click first asset → Set as A</li>
<li>Click second asset → Set as B</li>
<li>Use <strong>C</strong> again later to move on to Side-by-Side or back to Simple mode</li>
</ol>
<p><strong>Use Cases</strong>:</p>
<ul>
<li>Compare different sampler results</li>
<li>Check before/after edits</li>
<li>Evaluate parameter changes</li>
</ul>
<h4 id="viewer_feature_tutorial-side-by-side-mode">Side-by-Side Mode</h4>
<ul>
<li>Display both images simultaneously</li>
<li>Split view (vertical or horizontal)</li>
<li>Synchronized zoom and pan</li>
</ul>
<p><strong>How to Use</strong>:</p>
<ol>
<li>Enable Side-by-Side mode (press <strong>C</strong> until Side-by-Side is active)</li>
<li>Select two assets</li>
<li>Both display side by side</li>
<li>Use the mode button or toolbar controls if you want to move into Grid compare</li>
</ol>
<p><strong>Use Cases</strong>:</p>
<ul>
<li>Direct visual comparison</li>
<li>Composition analysis</li>
<li>Color grading comparison</li>
</ul>
<h3 id="viewer_feature_tutorial-multi-pin-references-abcd">Multi-Pin References (A/B/C/D)</h3>
<p>The Multi-Pin system lets you pin up to <strong>4 reference images</strong> simultaneously for comparison.</p>
<h4 id="viewer_feature_tutorial-how-it-works">How It Works</h4>
<ol>
<li>Open the Floating Viewer</li>
<li>In the toolbar, locate the <strong>A B C D</strong> toggle buttons next to the mode button</li>
<li>Click a letter to <strong>pin</strong> the currently displayed image to that slot</li>
<li>Click the same letter again to <strong>unpin</strong> it</li>
<li>Multiple slots can be active at the same time</li>
</ol>
<h4 id="viewer_feature_tutorial-pin-behavior">Pin Behavior</h4>
<ul>
<li><strong>Pinned slots are locked</strong>: When Live Stream brings in a new generation, pinned slots keep their content while unpinned slots update automatically</li>
<li><strong>Compare with pins</strong>: 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</li>
<li><strong>Multi-pin in Grid Compare</strong>: Pin A, B, C, and D to lock 4 images for simultaneous comparison in Grid Compare mode</li>
</ul>
<h4 id="viewer_feature_tutorial-use-cases">Use Cases</h4>
<ul>
<li><strong>Baseline comparison</strong>: Pin your best result in slot A and iterate freely</li>
<li><strong>Parameter sweep</strong>: Pin 4 different sampler results and compare visually</li>
<li><strong>Before/after editing</strong>: Pin the original in A, the edited version in B</li>
</ul>
<hr />
<h3 id="viewer_feature_tutorial-node-parameters-sidebar">Node Parameters Sidebar</h3>
<p>The <strong>Node Parameters</strong> sidebar displays and lets you edit the ComfyUI node widgets directly inside the Floating Viewer.</p>
<h4 id="viewer_feature_tutorial-opening-the-sidebar">Opening the Sidebar</h4>
<ol>
<li>Open the Floating Viewer</li>
<li>Click the <strong>Node Parameters</strong> button (sliders icon) in the toolbar — it is located at the far right of the toolbar</li>
<li>The sidebar slides open on the right (default position)</li>
</ol>
<h4 id="viewer_feature_tutorial-what-it-shows">What It Shows</h4>
<ul>
<li>All widgets from the workflow node that produced the current image</li>
<li>Grouped by node: each node section has a colored header showing the node title</li>
<li>Widget types supported: text fields, number inputs, combo/dropdown selectors, toggles</li>
</ul>
<h4 id="viewer_feature_tutorial-editing-widgets">Editing Widgets</h4>
<ul>
<li><strong>Text fields</strong>: Click and type. Long text (prompts) use a resizable text area</li>
<ul>
<li>Click the <strong>expand</strong> button (↕) to toggle between collapsed (80px) and expanded (680px) height</li>
<li>Text labels appear above the text area for better readability</li>
</ul>
<li><strong>Numbers</strong>: Type a value or use the stepper</li>
<li><strong>Combos</strong>: Select from the dropdown list</li>
<li>Changes are applied back to the ComfyUI graph in real time</li>
</ul>
<h4 id="viewer_feature_tutorial-run-button">Run Button</h4>
<p>The <strong>Run</strong> (▶) button at the far right of the toolbar queues the current workflow, allowing you to iterate without switching back to the ComfyUI canvas.</p>
<h4 id="viewer_feature_tutorial-use-cases">Use Cases</h4>
<ul>
<li><strong>Prompt iteration</strong>: Edit the positive/negative prompt and re-run without leaving the viewer</li>
<li><strong>Seed tweaking</strong>: Change the seed value and queue immediately</li>
<li><strong>Sampler comparison</strong>: Switch samplers from the sidebar and compare outputs</li>
<li><strong>CFG tuning</strong>: Adjust CFG scale, run, and compare final outputs via Live Stream</li>
</ul>
<hr />
<h3 id="viewer_feature_tutorial-sidebar-position-setting">Sidebar Position Setting</h3>
<p>You can change where the Node Parameters sidebar appears inside the Floating Viewer.</p>
<h4 id="viewer_feature_tutorial-changing-the-position">Changing the Position</h4>
<ol>
<li>Go to <strong>Settings → Majoor Assets Manager › Viewer</strong></li>
<li>Find <strong>Node Parameters sidebar position</strong></li>
<li>Select one of:</li>
<ul>
<li><strong>right</strong> (default) — sidebar opens on the right side</li>
<li><strong>left</strong> — sidebar opens on the left side</li>
<li><strong>bottom</strong> — sidebar opens at the bottom as a horizontal panel</li>
</ul>
<li>The change applies <strong>immediately</strong> — no page reload required</li>
</ol>
<h4 id="viewer_feature_tutorial-recommendations">Recommendations</h4>
<ul>
<li><strong>Right</strong> works best for most layouts and screen sizes</li>
<li><strong>Left</strong> is useful when your ComfyUI canvas is on the right</li>
<li><strong>Bottom</strong> is ideal for wide screens or when you prefer a short, wide parameter panel</li>
</ul>
<hr />
<h3 id="viewer_feature_tutorial-node-stream">Node Stream</h3>
<p><strong>Node Stream</strong> lets you preview selected node content when the frontend already has media for that node.</p>
<h4 id="viewer_feature_tutorial-enabling-node-stream">Enabling Node Stream</h4>
<ol>
<li>Open the Floating Viewer</li>
<li>Click the <strong>Node Stream</strong> button (icon: node graph)</li>
<li>Or press <strong>N</strong> to toggle</li>
</ol>
<h4 id="viewer_feature_tutorial-how-it-works">How It Works</h4>
<ul>
<li>Click <strong>LoadImage</strong> / loader nodes -&gt; preview the loaded media</li>
<li>Click <strong>SaveImage</strong> / preview nodes -&gt; preview an existing generated preview/output</li>
<li>Click ImageOps nodes -&gt; preview the embedded live canvas when available</li>
<li>Click other intermediate nodes -&gt; preview only if they expose <code>node.imgs</code>, widget media, or a downstream preview/save node</li>
</ul>
<h4 id="viewer_feature_tutorial-limitations">Limitations</h4>
<ul>
<li>Node Stream cannot read raw backend tensors (<code>IMAGE</code>, <code>MASK</code>, <code>LATENT</code>) before execution.</li>
<li>Python node outputs become available only after ComfyUI executes the node.</li>
<li>ImageOps live previews are frontend canvases, so they can update without queueing but are not the backend tensor itself.</li>
<li>If no frontend media exists for the selected node or downstream path, the MFV has nothing to display.</li>
</ul>
<h4 id="viewer_feature_tutorial-use-cases">Use Cases</h4>
<ul>
<li><strong>Debugging</strong>: Check available intermediate previews</li>
<li><strong>Workflow Building</strong>: Verify node connections</li>
<li><strong>Quality Control</strong>: Inspect outputs at each stage</li>
<li><strong>Teaching</strong>: Show workflow data flow</li>
</ul>
<h3 id="viewer_feature_tutorial-mfv-controls">MFV Controls</h3>
<h4 id="viewer_feature_tutorial-panel-controls">Panel Controls</h4>
<ul>
<li><strong>Move</strong>: Drag from panel header</li>
<li><strong>Resize</strong>: Drag panel edges or corners</li>
<li><strong>Close</strong>: Click X button or press <strong>Esc</strong></li>
<li><strong>Minimize</strong>: Click minimize button (optional)</li>
</ul>
<h4 id="viewer_feature_tutorial-zoom-pan">Zoom &amp; Pan</h4>
<ul>
<li><strong>Zoom In</strong>: Mouse wheel up or <strong>Up Arrow</strong></li>
<li><strong>Zoom Out</strong>: Mouse wheel down or <strong>Down Arrow</strong></li>
<li><strong>Reset Zoom</strong>: Press <strong>0</strong> (fit to screen)</li>
<li><strong>Actual Size</strong>: Press <strong>1</strong> (1:1 pixel)</li>
<li><strong>Pan</strong>: Click and drag when zoomed in</li>
</ul>
<h4 id="viewer_feature_tutorial-navigation">Navigation</h4>
<ul>
<li><strong>Previous Asset</strong>: <strong>Left Arrow</strong> when the inline player is not focused</li>
<li><strong>Next Asset</strong>: <strong>Right Arrow</strong> when the inline player is not focused</li>
<li><strong>First Asset</strong>: <strong>Home</strong></li>
<li><strong>Last Asset</strong>: <strong>End</strong></li>
</ul>
<h3 id="viewer_feature_tutorial-mfv-keyboard-shortcuts">MFV Keyboard Shortcuts</h3>
<h4 id="viewer_feature_tutorial-general">General</h4>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Esc</strong></td><td>Close Floating Viewer</td></tr>
<tr><td><strong>C</strong></td><td>Cycle compare modes: A/B, Side-by-side, Off (Simple mode)</td></tr>
<tr><td><strong>K</strong></td><td>Toggle KSampler denoising preview on or off</td></tr>
<tr><td><strong>L</strong></td><td>Toggle Live Stream final-output following on or off</td></tr>
<tr><td><strong>N</strong></td><td>Toggle Node Stream selected-node previewing</td></tr>
</tbody></table></div>
<h4 id="viewer_feature_tutorial-video-controls">Video Controls</h4>
<div class="table-wrap"><table>
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
<tbody>
<tr><td><strong>Space</strong></td><td>Play/Pause the focused inline player</td></tr>
<tr><td><strong>Left Arrow</strong></td><td>Step backward one frame</td></tr>
<tr><td><strong>Right Arrow</strong></td><td>Step forward one frame</td></tr>
</tbody></table></div>
<blockquote>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.</blockquote>
<p>See <a href="#hotkeys_shortcuts">HOTKEYS_SHORTCUTS.md</a> for complete shortcut list.</p>
<hr />
<h2 id="viewer_feature_tutorial-standard-viewer">Standard Viewer</h2>
<h2 id="viewer_feature_tutorial-viewer-navigation">Viewer Navigation</h2>
<h3 id="viewer_feature_tutorial-basic-navigation">Basic Navigation</h3>
<ul>
<li><strong>Zoom</strong>: Use mouse wheel to zoom in/out</li>
<li><strong>Pan</strong>: Click and drag to move around the image</li>
<li><strong>Fit to Screen</strong>: Automatically scales image to fit window</li>
<li><strong>Actual Size</strong>: View image at 1:1 pixel ratio (Alt+1)</li>
</ul>
<h3 id="viewer_feature_tutorial-asset-navigation">Asset Navigation</h3>
<ul>
<li><strong>Next/Previous</strong>: Navigate between assets in the current set</li>
<li><strong>Keyboard Arrows</strong>: Use arrow keys to move between assets</li>
<li><strong>Direct Selection</strong>: Click on thumbnails if available</li>
</ul>
<h2 id="viewer_feature_tutorial-image-enhancement-tools">Image Enhancement Tools</h2>
<h3 id="viewer_feature_tutorial-exposure-adjustment-ev">Exposure Adjustment (EV)</h3>
<p>The exposure adjustment tool allows you to modify the brightness of your image:</p>
<ol>
<li><strong>Accessing the Tool</strong>:</li>
<ul>
<li>Locate the exposure control in the toolbar</li>
<li>Usually represented by a sun or EV icon</li>
</ul>
</ol>
<ol>
<li><strong>Adjusting Exposure</strong>:</li>
<ul>
<li>Adjust exposure compensation from -5 to +5 EV</li>
<li>Real-time preview shows changes immediately</li>
<li>Use to evaluate details in shadows/highlights</li>
<li>Preserves color relationships while adjusting brightness</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Revealing details in underexposed areas</li>
<li>Recovering highlights in overexposed regions</li>
<li>Comparing exposure differences between assets</li>
<li>Evaluating dynamic range</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-gamma-correction">Gamma Correction</h3>
<p>Gamma correction adjusts the midtone brightness:</p>
<ol>
<li><strong>Accessing the Tool</strong>:</li>
<ul>
<li>Find the gamma control in the toolbar</li>
<li>Usually labeled with γ or gamma symbol</li>
</ul>
</ol>
<ol>
<li><strong>Adjusting Gamma</strong>:</li>
<ul>
<li>Adjust gamma curve from 0.25 to 4.0</li>
<li>Alters midtone brightness without affecting highlights/shadows as much</li>
<li>Real-time application with live preview</li>
<li>Useful for fine-tuning contrast in specific tonal ranges</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Enhancing midtone contrast</li>
<li>Adjusting overall tonal balance</li>
<li>Fine-tuning for display characteristics</li>
<li>Preparing for different output conditions</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-channel-isolation">Channel Isolation</h3>
<p>The viewer allows you to isolate different color channels:</p>
<ol>
<li><strong>Available Channels</strong>:</li>
<ul>
<li><strong>RGB</strong>: View all color channels combined</li>
<li><strong>R</strong>: View only red channel information</li>
<li><strong>G</strong>: View only green channel information</li>
<li><strong>B</strong>: View only blue channel information</li>
<li><strong>Alpha</strong>: View transparency/alpha channel</li>
<li><strong>Luma</strong>: View luminance information only</li>
</ul>
</ol>
<ol>
<li><strong>Using Channel Isolation</strong>:</li>
<ul>
<li>Select the desired channel from the toolbar</li>
<li>Observe the isolated channel information</li>
<li>Compare between different channels</li>
<li>Identify channel-specific issues</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Identifying color channel imbalances</li>
<li>Detecting channel-specific noise</li>
<li>Analyzing alpha channel transparency</li>
<li>Evaluating luminance distribution</li>
</ul>
</ol>
<h2 id="viewer_feature_tutorial-analysis-tools">Analysis Tools</h2>
<h3 id="viewer_feature_tutorial-false-color-mode">False Color Mode</h3>
<p>False color mode helps identify exposure issues:</p>
<ol>
<li><strong>Activating False Color</strong>:</li>
<ul>
<li>Enable false color mode from the toolbar</li>
<li>Usually represented by a color palette icon</li>
</ul>
</ol>
<ol>
<li><strong>Interpreting Results</strong>:</li>
<ul>
<li>Identifies overexposed and underexposed areas</li>
<li>Highlights clipped highlights (typically white/magenta)</li>
<li>Shows blocked shadows (typically blue/black)</li>
<li>Helps evaluate dynamic range and exposure</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Checking for highlight clipping</li>
<li>Identifying shadow detail loss</li>
<li>Evaluating overall exposure balance</li>
<li>Comparing exposure between different assets</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-zebra-patterns-z">Zebra Patterns (Z)</h3>
<p>Zebra patterns indicate areas approaching clipping:</p>
<ol>
<li><strong>Enabling Zebra</strong>:</li>
<ul>
<li>Activate zebra patterns from the toolbar</li>
<li>Usually labeled with "Z" or zebra icon</li>
</ul>
</ol>
<ol>
<li><strong>Understanding Zebra</strong>:</li>
<ul>
<li>Displays 100% IRE (95% for Rec.2020) areas as zebra stripes</li>
<li>Helps identify areas approaching clipping</li>
<li>Adjustable threshold levels</li>
<li>Useful for maintaining highlight detail</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Protecting highlight detail</li>
<li>Setting optimal exposure levels</li>
<li>Comparing exposure between shots</li>
<li>Quality control for consistent exposure</li>
</ul>
</ol>
<h2 id="viewer_feature_tutorial-visual-overlays">Visual Overlays</h2>
<h3 id="viewer_feature_tutorial-grid-overlays-g">Grid Overlays (G)</h3>
<p>Grid overlays assist with composition:</p>
<ol>
<li><strong>Available Grid Types</strong>:</li>
<ul>
<li><strong>Off</strong>: No grid overlay</li>
<li><strong>Thirds</strong>: Rule of thirds grid lines</li>
<li><strong>Center</strong>: Center crosshair</li>
<li><strong>Safe Area</strong>: TV broadcast safe area guides</li>
<li><strong>Golden Ratio</strong>: Golden ratio composition guides</li>
</ul>
</ol>
<ol>
<li><strong>Using Grid Overlays</strong>:</li>
<ul>
<li>Cycle through grid types using "G" key</li>
<li>Assess compositional balance</li>
<li>Align elements to grid lines</li>
<li>Compare compositions between assets</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Composition analysis</li>
<li>Alignment verification</li>
<li>Rule of thirds evaluation</li>
<li>Broadcast compliance checking</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-pixel-probe-i">Pixel Probe (I)</h3>
<p>Pixel probe provides detailed pixel information:</p>
<ol>
<li><strong>Activating Pixel Probe</strong>:</li>
<ul>
<li>Enable pixel probe from toolbar or press "I"</li>
<li>Hover over image to see information</li>
</ul>
</ol>
<ol>
<li><strong>Information Provided</strong>:</li>
<ul>
<li>Exact pixel coordinates</li>
<li>RGB/RGBA values at cursor position</li>
<li>Hex color code</li>
<li>Luminance value information</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Color accuracy verification</li>
<li>Detail inspection</li>
<li>Color sampling</li>
<li>Quality assessment at pixel level</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-loupe-magnification-l">Loupe Magnification (L)</h3>
<p>Loupe provides magnified view of specific areas:</p>
<ol>
<li><strong>Using Loupe</strong>:</li>
<ul>
<li>Activate loupe from toolbar or press "L"</li>
<li>Hover over area of interest</li>
<li>View magnified representation</li>
</ul>
</ol>
<ol>
<li><strong>Features</strong>:</li>
<ul>
<li>Adjustable magnification level</li>
<li>Shows fine detail at pixel level</li>
<li>Helpful for quality assessment</li>
<li>Real-time magnification</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Quality inspection</li>
<li>Artifact detection</li>
<li>Detail analysis</li>
<li>Sharpness evaluation</li>
</ul>
</ol>
<h2 id="viewer_feature_tutorial-professional-analysis-tools-scopes">Professional Analysis Tools (Scopes)</h2>
<h3 id="viewer_feature_tutorial-histogram">Histogram</h3>
<p>The histogram shows tonal distribution:</p>
<ol>
<li><strong>Accessing Histogram</strong>:</li>
<ul>
<li>Enable from scopes section</li>
<li>Shows RGB channel distribution</li>
</ul>
</ol>
<ol>
<li><strong>Interpretation</strong>:</li>
<ul>
<li>X-axis represents tonal range (shadows to highlights)</li>
<li>Y-axis represents pixel count</li>
<li>Separate curves for R, G, B channels</li>
<li>Identifies tonal distribution patterns</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Exposure evaluation</li>
<li>Contrast analysis</li>
<li>Color balance assessment</li>
<li>Dynamic range evaluation</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-waveform">Waveform</h3>
<p>The waveform shows luminance distribution:</p>
<ol>
<li><strong>Accessing Waveform</strong>:</li>
<ul>
<li>Enable from scopes section</li>
<li>Shows luminance across image</li>
</ul>
</ol>
<ol>
<li><strong>Interpretation</strong>:</li>
<ul>
<li>Horizontal axis represents image width</li>
<li>Vertical axis represents luminance levels</li>
<li>Brighter areas appear higher in the display</li>
<li>Darker areas appear lower in the display</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Exposure evaluation</li>
<li>Luminance distribution analysis</li>
<li>Highlight/shadow placement</li>
<li>Consistency checking</li>
</ul>
</ol>
<h3 id="viewer_feature_tutorial-vectorscope">Vectorscope</h3>
<p>The vectorscope shows color information:</p>
<ol>
<li><strong>Accessing Vectorscope</strong>:</li>
<ul>
<li>Enable from scopes section</li>
<li>Shows color information in chromaticity diagram</li>
</ul>
</ol>
<ol>
<li><strong>Interpretation</strong>:</li>
<ul>
<li>Shows color saturation and hue</li>
<li>Identifies color distribution</li>
<li>Reveals color casts or imbalances</li>
<li>Shows skin tone placement</li>
</ul>
</ol>
<ol>
<li><strong>Use Cases</strong>:</li>
<ul>
<li>Color balance correction</li>
<li>Skin tone evaluation</li>
<li>Saturation assessment</li>
<li>Color cast identification</li>
</ul>
</ol>
<h2 id="viewer_feature_tutorial-video-specific-features">Video-Specific Features</h2>
<h3 id="viewer_feature_tutorial-video-playback-controls">Video Playback Controls</h3>
<ul>
<li><strong>Play/Pause</strong>: Standard play/pause functionality</li>
<li><strong>Loop/Once</strong>: Toggle between continuous and single playback</li>
<li><strong>Speed Control</strong>: Adjustable playback speed (0.25x to 4x)</li>
<li><strong>Frame Stepping</strong>: Precise frame-by-frame navigation (Left/Right Arrow keys when player bar is focused)</li>
</ul>
<h3 id="viewer_feature_tutorial-timeline-features">Timeline Features</h3>
<ul>
<li><strong>Seek Bar</strong>: Drag to jump to specific points in video</li>
<li><strong>Current Time</strong>: Display of current playback position</li>
<li><strong>Duration</strong>: Total video duration display</li>
<li><strong>Frame Counter</strong>: Current frame number display</li>
</ul>
<h3 id="viewer_feature_tutorial-inout-points">In/Out Points</h3>
<ul>
<li><strong>Set In Point</strong>: Mark start of desired segment (I key)</li>
<li><strong>Set Out Point</strong>: Mark end of desired segment (O key)</li>
<li><strong>Segment Playback</strong>: Play only the marked segment</li>
<li><strong>Clear Points</strong>: Remove In/Out markers</li>
</ul>
<h3 id="viewer_feature_tutorial-hardware-acceleration-webgl">Hardware Acceleration (WebGL)</h3>
<p>The viewer automatically detects WebGL support to enable GPU-accelerated video rendering:</p>
<ul>
<li><strong>Benefits</strong>: Real-time Exposure, Gamma, and Zebra analysis on 4K videos without CPU load</li>
<li><strong>Fallback</strong>: If WebGL is unavailable, it automatically switches to a robust CPU-based renderer (Canvas 2D)</li>
</ul>
<h2 id="viewer_feature_tutorial-export-capabilities">Export Capabilities</h2>
<h3 id="viewer_feature_tutorial-save-current-frame">Save Current Frame</h3>
<ul>
<li>Capture current video frame as PNG</li>
<li>Preserves full resolution and quality</li>
<li>Saves to default output directory</li>
<li>Includes generation metadata if available</li>
</ul>
<h3 id="viewer_feature_tutorial-copy-to-clipboard">Copy to Clipboard</h3>
<ul>
<li>Copy current view to system clipboard</li>
<li>Best-effort format selection</li>
<li>May not work on all platforms</li>
<li>Useful for quick sharing</li>
</ul>
<h3 id="viewer_feature_tutorial-download-original">Download Original</h3>
<ul>
<li>Download the original asset file</li>
<li>Maintains original format and quality</li>
<li>Preserves embedded metadata</li>
<li>Safe file transfer protocol</li>
</ul>
<h2 id="viewer_feature_tutorial-viewer-hotkeys">Viewer Hotkeys</h2>
<p>All viewer hotkeys are captured and don't affect ComfyUI or browser:</p>
<ul>
<li><strong>Esc</strong>: Close viewer</li>
<li><strong>0-5</strong>: Set rating (single view)</li>
<li><strong>Left/Right Arrow</strong>: Step video frames (when player bar is focused)</li>
<li><strong>F</strong>: Toggle fullscreen</li>
<li><strong>Z</strong>: Toggle zebra patterns</li>
<li><strong>G</strong>: Cycle through grid overlays</li>
<li><strong>I</strong>: Toggle pixel probe</li>
<li><strong>L</strong>: Toggle loupe magnification</li>
<li><strong>C</strong>: Copy last probed color hex value</li>
<li><strong>Alt+1</strong>: Toggle 1:1 zoom</li>
</ul>
<h2 id="viewer_feature_tutorial-comparison-integration">Comparison Integration</h2>
<p>The viewer seamlessly integrates with the comparison feature:</p>
<ul>
<li>Switch between single view and comparison modes</li>
<li>Maintain analysis tools during comparison</li>
<li>Apply enhancements to both compared assets</li>
<li>Use analysis tools on comparison results</li>
</ul>
<h2 id="viewer_feature_tutorial-troubleshooting-viewer-issues">Troubleshooting Viewer Issues</h2>
<h3 id="viewer_feature_tutorial-performance-problems">Performance Problems</h3>
<ul>
<li><strong>Slow Rendering</strong>: Reduce enhancement effects or close other applications</li>
<li><strong>Memory Issues</strong>: Close viewer tabs when not actively using them</li>
<li><strong>Codec Problems</strong>: Install appropriate codecs for video files</li>
<li><strong>Browser Compatibility</strong>: Ensure using supported browsers</li>
</ul>
<h3 id="viewer_feature_tutorial-tool-malfunctions">Tool Malfunctions</h3>
<ul>
<li><strong>Missing Tools</strong>: Verify WebGL support in your browser</li>
<li><strong>Unresponsive Controls</strong>: Refresh the viewer or browser</li>
<li><strong>Display Issues</strong>: Check browser zoom level and display settings</li>
<li><strong>Audio Problems</strong>: Check system audio settings for video files</li>
</ul>
<h3 id="viewer_feature_tutorial-file-specific-issues">File-Specific Issues</h3>
<ul>
<li><strong>Unsupported Formats</strong>: Convert to supported formats if needed</li>
<li><strong>Corrupted Files</strong>: Verify file integrity</li>
<li><strong>Large Files</strong>: Be patient with initial loading of large files</li>
<li><strong>Network Issues</strong>: Ensure stable connection for remote files</li>
</ul>
<h2 id="viewer_feature_tutorial-best-practices">Best Practices</h2>
<h3 id="viewer_feature_tutorial-efficient-viewing">Efficient Viewing</h3>
<ul>
<li>Use keyboard shortcuts for faster navigation</li>
<li>Apply analysis tools systematically</li>
<li>Take advantage of export features for sharing</li>
<li>Use comparison modes for parameter evaluation</li>
</ul>
<h3 id="viewer_feature_tutorial-quality-assessment">Quality Assessment</h3>
<ul>
<li>Use pixel probe for detailed inspection</li>
<li>Apply false color to check exposure</li>
<li>Use zebra patterns to protect highlights</li>
<li>Employ scopes for professional analysis</li>
</ul>
<h3 id="viewer_feature_tutorial-workflow-integration">Workflow Integration</h3>
<ul>
<li>Combine viewer tools with rating system</li>
<li>Use export features for review workflows</li>
<li>Integrate with collection management</li>
<li>Apply consistent analysis methodology</li>
</ul>
<h3 id="viewer_feature_tutorial-performance-optimization">Performance Optimization</h3>
<ul>
<li>Close viewers when not actively using them</li>
<li>Use lower resolution previews when possible</li>
<li>Organize assets into collections to reduce load times</li>
<li>Keep frequently accessed assets in smaller collections</li>
</ul>
<hr />
<p>_Viewer Feature Tutorial Version: 2.4.9_ _Last Updated: April 14, 2026_</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/VIEWER_FEATURE_TUTORIAL.md</code>
</div>
</div>
</section>
<section class="panel section" id="workflows">
<div class="panel-h"><h2>WORKFLOWS.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="workflows-workflows-tab-guide">Workflows Tab Guide</h1>
<p><strong>Last Updated</strong>: June 11, 2026</p>
<h2 id="workflows-overview">Overview</h2>
<p>The <strong>Workflow</strong> tab turns saved ComfyUI workflow JSON files into a browsable library inside Majoor Assets Manager.</p>
<p>It is separate from the normal output/input asset tabs:</p>
<ul>
<li>output and input tabs browse media files</li>
<li>the Workflow tab browses workflow <code>.json</code> files</li>
<li>workflow cards can be loaded back into ComfyUI, inspected in Graph Map, organized, tagged, and assigned thumbnails</li>
</ul>
<p>This feature follows ComfyUI's server model: the frontend reads and writes workflow data through Majoor HTTP routes, while actual workflow loading is handed back to ComfyUI frontend import APIs when available.</p>
<h2 id="workflows-where-workflows-come-from">Where Workflows Come From</h2>
<p>Majoor scans these workflow roots when the Workflow tab is opened or refreshed:</p>
<ul>
<li>roots configured in <strong>Settings -&gt; Majoor Assets Manager -&gt; Advanced -&gt; Paths / Workflows</strong></li>
<li><code>MJR_AM_WORKFLOW_DIRECTORIES</code>, when set with multiple roots separated by the OS path separator</li>
<li><code>MJR_AM_WORKFLOW_DIRECTORY</code>, when set</li>
<li><code>&amp;lt;ComfyUI&amp;gt;/user/default/workflows</code></li>
<li><code>&amp;lt;ComfyUI&amp;gt;/workflows</code></li>
<li><code>&amp;lt;custom_nodes&amp;gt;/majoor-assetsmanager/workflows</code></li>
<li><code>&amp;lt;custom_nodes&amp;gt;/majoor-assetsmanager/example_workflows</code></li>
</ul>
<p>When you save or import a workflow from the tab, Majoor writes it to the managed workflow root:</p>
<ul>
<li>the first configured workflow root, when workflow roots are set in Settings</li>
<li><code>MJR_AM_WORKFLOW_DIRECTORY</code>, when set</li>
<li>otherwise <code>&amp;lt;ComfyUI&amp;gt;/user/default/workflows</code></li>
<li>otherwise the extension-local <code>workflows/</code> folder</li>
</ul>
<p>Workflow categories are stored as safe subfolders under the managed root.</p>
<h2 id="workflows-opening-the-tab">Opening The Tab</h2>
<ol>
<li>Open Majoor Assets Manager.</li>
<li>Click the <strong>Workflow</strong> tab in the top scope tabs.</li>
<li>Use the search box, sort menu, filters, and card actions just like the rest of the grid.</li>
</ol>
<p>The Workflow tab adds two toolbar buttons that only appear in this scope:</p>
<ul>
<li><strong>Save current workflow</strong>: serializes the current ComfyUI canvas and stores it as a managed workflow JSON.</li>
<li><strong>Pick saved workflow root</strong>: chooses the folder used as the saved workflow root and refreshes the Workflow tab.</li>
<li><strong>Import workflow</strong>: imports one or more local <code>.json</code> workflow files into the managed workflow library.</li>
</ul>
<h2 id="workflows-workflow-cards">Workflow Cards</h2>
<p>Each workflow card summarizes the JSON file and its detected purpose.</p>
<p>Cards can show:</p>
<ul>
<li>workflow name and filename</li>
<li>thumbnail or generated Graph Map preview</li>
<li>task, model family, provider, and runtime classification</li>
<li>node count, link count, and subgraph count</li>
<li>missing node or missing model counts</li>
<li>category/folder</li>
<li>favorite state, usage count, tags, and notes</li>
</ul>
<p>The library parser includes native subgraphs when counting and summarizing workflows.</p>
<h2 id="workflows-loading-a-workflow">Loading A Workflow</h2>
<p>Double-click a workflow card, or use <strong>Load workflow</strong> from the context menu.</p>
<p>Majoor will:</p>
<ol>
<li>read the workflow JSON from <code>/mjr/am/workflows/content</code></li>
<li>warn if the current ComfyUI canvas appears dirty</li>
<li>pass the workflow to the ComfyUI frontend import path when available</li>
<li>mark the workflow as loaded so usage sorting can prioritize recently used workflows</li>
</ol>
<p>If the current ComfyUI frontend does not expose a direct import path, Majoor falls back gracefully and shows a message instead of silently failing.</p>
<h2 id="workflows-inspecting-a-workflow">Inspecting A Workflow</h2>
<p>Use <strong>Inspect</strong> or <strong>Open in Graph Map</strong> to view a workflow without replacing the current canvas.</p>
<p>This opens the Majoor Floating Viewer in Graph Map mode with the workflow attached as the current asset. Use it to:</p>
<ul>
<li>pan and zoom the saved workflow</li>
<li>inspect node labels and simple parameters</li>
<li>review subgraph structure</li>
<li>copy node values</li>
<li>import the full workflow only when you are ready</li>
</ul>
<p>See <a href="#graph_map">GRAPH_MAP.md</a> for the full Graph Map walkthrough.</p>
<h2 id="workflows-organizing-workflows">Organizing Workflows</h2>
<p>Workflow organization is stored in the workflow library database and, when applicable, in the managed filesystem layout.</p>
<p>Available actions include:</p>
<ul>
<li><strong>Favorite</strong>: pin useful workflows ahead of normal sorting.</li>
<li><strong>Rename workflow</strong>: rename the managed JSON file safely.</li>
<li><strong>Set workflow category</strong>: move the workflow JSON into a managed category subfolder.</li>
<li><strong>Edit infos</strong>: override task, model family, provider, runtime, and notes.</li>
<li><strong>Tags</strong>: attach searchable tags to workflow cards.</li>
<li><strong>Duplicate workflow</strong>: create a copy in the managed root.</li>
<li><strong>Delete workflow</strong>: delete the JSON and adjacent thumbnail files, subject to write/delete guards.</li>
</ul>
<p>Write actions use Majoor's normal CSRF/auth checks, write-access guard, audit logging, and workflow write rate limit.</p>
<h2 id="workflows-thumbnails">Thumbnails</h2>
<p>A workflow can use:</p>
<ul>
<li>an adjacent image thumbnail such as <code>workflow.png</code>, <code>workflow.jpg</code>, or <code>workflow.webp</code></li>
<li>an adjacent animated thumbnail such as <code>workflow.gif</code>, <code>workflow.webp</code>, or <code>workflow.mp4</code></li>
<li>a generated Graph Map SVG preview when no thumbnail exists</li>
<li>a linked output asset selected through <strong>Set workflow thumbnail</strong></li>
</ul>
<p>Linked thumbnail candidates are discovered from indexed assets with the same <code>workflow_id</code> or workflow hash. Video thumbnails are converted to a short animated WebP preview when supported.</p>
<p>You can also right-click a normal media asset and choose <strong>Use as workflow thumbnail</strong> to assign that asset to an existing workflow.</p>
<h2 id="workflows-search-sort-and-grouping">Search, Sort, And Grouping</h2>
<p>The Workflow tab uses the standard grid search flow with workflow-specific metadata.</p>
<p>Search matches common workflow fields:</p>
<ul>
<li>filename</li>
<li>display name</li>
<li>description</li>
<li>task</li>
<li>model family</li>
<li>provider</li>
<li>runtime</li>
<li>category/subfolder</li>
</ul>
<p>Workflow-specific sort modes include:</p>
<ul>
<li>default workflow order: favorites, recently loaded, usage, modified date, name</li>
<li>name</li>
<li>task</li>
<li>model family</li>
<li>usage count</li>
<li>last loaded</li>
</ul>
<p>The grid can also group workflow cards by task, model family, or category depending on the configured workflow grouping setting.</p>
<h2 id="workflows-backend-api-summary">Backend API Summary</h2>
<p>The tab is powered by these Majoor routes:</p>
<div class="table-wrap"><table>
<thead><tr><th>Route</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td><code>GET /mjr/am/list?scope=workflow</code></td><td>List workflow cards</td></tr>
<tr><td><code>GET /mjr/am/workflows/content</code></td><td>Read workflow JSON and prompt metadata</td></tr>
<tr><td><code>POST /mjr/am/workflows/save</code></td><td>Save current/imported workflow JSON</td></tr>
<tr><td><code>POST /mjr/am/workflows/duplicate</code></td><td>Duplicate a managed workflow</td></tr>
<tr><td><code>POST /mjr/am/workflows/move</code></td><td>Rename or categorize a managed workflow</td></tr>
<tr><td><code>POST /mjr/am/workflows/delete</code></td><td>Delete a managed workflow</td></tr>
<tr><td><code>POST /mjr/am/workflows/mark-loaded</code></td><td>Update usage metadata after loading</td></tr>
<tr><td><code>POST /mjr/am/workflows/favorite</code></td><td>Toggle favorite state</td></tr>
<tr><td><code>POST /mjr/am/workflows/tags</code></td><td>Save workflow tags</td></tr>
<tr><td><code>POST /mjr/am/workflows/info</code></td><td>Save manual workflow info and notes</td></tr>
<tr><td><code>GET /mjr/am/workflows/thumbnail-candidates</code></td><td>Find linked output assets for thumbnails</td></tr>
<tr><td><code>POST /mjr/am/workflows/thumbnail/set</code></td><td>Set or convert a workflow thumbnail</td></tr>
<tr><td><code>GET /mjr/am/workflows/thumbnail</code></td><td>Serve a workflow thumbnail</td></tr>
<tr><td><code>GET /mjr/am/workflows/graph-map-thumbnail</code></td><td>Serve generated Graph Map SVG preview</td></tr>
</tbody></table></div>
<p>See <a href="#api_reference">API_REFERENCE.md</a> for endpoint-level details.</p>
<h2 id="workflows-limitations">Limitations</h2>
<ul>
<li>Only readable JSON workflows are listed.</li>
<li>Very large workflow files are skipped for safety.</li>
<li>Write actions are restricted to managed workflow paths.</li>
<li>Missing nodes and models depend on metadata present in the workflow JSON.</li>
<li>Loading relies on the ComfyUI frontend import capability available in the current host build.</li>
</ul>
<h2 id="workflows-related-docs">Related Docs</h2>
<ul>
<li><a href="#graph_map">GRAPH_MAP.md</a></li>
<li><a href="#mfv_guide">MFV_GUIDE.md</a></li>
<li><a href="#search_filtering">SEARCH_FILTERING.md</a></li>
<li><a href="#settings_configuration">SETTINGS_CONFIGURATION.md</a></li>
<li><a href="#api_reference">API_REFERENCE.md</a></li>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/WORKFLOWS.md</code>
</div>
</div>
</section>
<section class="panel section" id="workflow_scope_implementation_plan">
<div class="panel-h"><h2>WORKFLOW_SCOPE_IMPLEMENTATION_PLAN.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="workflow_scope_implementation_plan-workflow-scope-implementation-plan">Workflow Scope Implementation Plan</h1>
<h2 id="workflow_scope_implementation_plan-objective">Objective</h2>
<p>Add a first-class <strong>Workflow</strong> scope next to the existing <code>All</code>, <code>Input</code>, <code>Output</code>, and <code>Custom</code> scopes. The scope should let users browse, classify, preview, load, save, and organize ComfyUI workflow JSON files with a card grid similar to ComfyUI's official template browser.</p>
<p>Target UX:</p>
<ul>
<li><code>Workflow</code> tab in the Majoor panel.</li>
<li>Workflow cards with static or animated thumbnails.</li>
<li>Filters for task/category (<code>T2I</code>, <code>I2I</code>, <code>I2V</code>, <code>T2V</code>, <code>Image Edit</code>, etc.),</li>
<p>model family (<code>Qwen</code>, <code>Seedance</code>, <code>Flux</code>, <code>Wan</code>, etc.), provider, local/API runtime, required custom nodes, and missing models.</p>
<li>Sort modes similar to templates: default, recent, name, model, task, usage.</li>
<li>Actions: load workflow into ComfyUI, save current workflow, duplicate, rename,</li>
<p>move/category assign, delete with write guard, open in folder, inspect nodes, and optionally add thumbnail.</p>
</ul>
<h2 id="workflow_scope_implementation_plan-official-comfyui-alignment-notes">Official ComfyUI Alignment Notes</h2>
<p>Sources consulted before planning:</p>
<ul>
<li>ComfyUI server routes and comms docs:</li>
<p>https://docs.comfy.org/development/comfyui-server/comms_overview</p>
<li>ComfyUI routes docs:</li>
<p>https://docs.comfy.org/development/comfyui-server/comms_routes</p>
<li>ComfyUI built-in template docs:</li>
<p>https://docs.comfy.org/interface/features/template</p>
<li>Custom-node workflow template docs:</li>
<p>https://docs.comfy.org/custom-nodes/workflow_templates</p>
<li>Official workflow templates repository:</li>
<p>https://github.com/Comfy-Org/workflow_templates</p>
</ul>
<p>Important constraints from those sources:</p>
<ul>
<li>ComfyUI server routes are based on <code>PromptServer.instance.routes</code> and</li>
<p><code>aiohttp</code>; Majoor routes must keep using that pattern.</p>
<li>ComfyUI already exposes workflow/template/user-data routes, including</li>
<p><code>/workflow_templates</code>, <code>/userdata</code>, <code>/v2/userdata</code>, <code>/prompt</code>, <code>/history</code>, <code>/object_info</code>, and <code>/models</code>.</p>
<li>ComfyUI custom-node examples are expected under <code>example_workflows</code> or</li>
<p>compatible folder names, JSON only, with optional same-name JPG thumbnails.</p>
<li>Core template browsing is for official/custom-node examples. Majoor's</li>
<p>Workflow scope should manage the user's saved workflow library, not override the core template browser.</p>
<li>Frontend host access must stay behind bridge modules such as</li>
<p><code>ui/app/comfyApiBridge.ts</code> or <code>ui/app/hostAdapter.ts</code>.</p>
</ul>
<h2 id="workflow_scope_implementation_plan-current-repository-audit">Current Repository Audit</h2>
<h3 id="workflow_scope_implementation_plan-existing-scope-flow">Existing Scope Flow</h3>
<p>Current grid scopes are wired through:</p>
<ul>
<li><code>ui/features/panel/views/tabsView.ts</code></li>
<li><code>ui/features/panel/controllers/scopeController.ts</code></li>
<li><code>ui/features/panel/controllers/gridController.ts</code></li>
<li><code>ui/api/endpoints.ts</code></li>
<li><code>mjr_am_backend/routes/search/listing_endpoint.py</code></li>
<li><code>mjr_am_backend/routes/search/listing_scopes.py</code></li>
</ul>
<p>The accepted frontend scopes are currently:</p>
<ul>
<li><code>all</code></li>
<li><code>input</code></li>
<li><code>output</code> / <code>outputs</code></li>
<li><code>custom</code></li>
<li><code>similar</code></li>
</ul>
<p>The backend listing route rejects unknown scopes, so <code>workflow</code> needs explicit support on both sides.</p>
<h3 id="workflow_scope_implementation_plan-existing-data-model">Existing Data Model</h3>
<p>Current persistence is media-asset oriented:</p>
<ul>
<li><code>assets.source</code>: <code>output</code>, <code>input</code>, <code>custom</code></li>
<li><code>assets.kind</code>: currently classifies file media types like image/video/audio/3D</li>
<li><code>asset_metadata</code>: stores workflow/generation data extracted from media</li>
<li><code>assets.workflow_id</code>, <code>asset_metadata.workflow_hash</code>,</li>
<p><code>asset_metadata.workflow_type</code>, <code>asset_metadata.has_workflow</code> already support workflow metadata attached to generated files</p>
</ul>
<p>This is not enough for saved workflow files as browsable entities. A saved workflow is not just metadata on an image; it needs its own library row, thumbnail relation, categories, required nodes/models, and load/save actions.</p>
<h3 id="workflow_scope_implementation_plan-existing-ui-card-flow">Existing UI Card Flow</h3>
<p>Cards are rendered through:</p>
<ul>
<li><code>ui/vue/components/grid/AssetCardInner.vue</code></li>
<li><code>ui/features/grid/AssetCardRenderer.ts</code></li>
<li><code>ui/vue/components/grid/AssetsGrid.vue</code></li>
<li><code>ui/vue/components/grid/VirtualAssetGridHost.vue</code></li>
</ul>
<p>The current card assumes media-ish assets and computes thumbnail URLs with <code>buildAssetViewURL()</code>. Workflow cards need a separate preview contract, because JSON cannot be served through <code>/view</code> and may use:</p>
<ul>
<li>explicit thumbnail image</li>
<li>animated thumbnail/video/webp preview</li>
<li>first matching generated output linked by <code>workflow_id</code></li>
<li>generated graph-map screenshot fallback</li>
<li>icon fallback when no thumbnail exists</li>
</ul>
<h3 id="workflow_scope_implementation_plan-existing-backend-architecture">Existing Backend Architecture</h3>
<p>The repo architecture says:</p>
<ul>
<li>routes parse HTTP and call services</li>
<li>features own business logic</li>
<li>adapters isolate DB/filesystem/ComfyUI boundaries</li>
<li>write/delete operations must use shared security and audit helpers</li>
</ul>
<p>Workflow scope should follow that shape:</p>
<ul>
<li><code>routes/handlers/workflows.py</code></li>
<li><code>features/workflows/service.py</code></li>
<li><code>features/workflows/indexer.py</code></li>
<li><code>features/workflows/classifier.py</code></li>
<li><code>features/workflows/models.py</code></li>
<li><code>adapters/comfy_user_data.py</code> or equivalent bridge for ComfyUI user data APIs</li>
<li>optional DB migration under <code>adapters/db/migrations</code></li>
</ul>
<h2 id="workflow_scope_implementation_plan-recommended-design">Recommended Design</h2>
<h3 id="workflow_scope_implementation_plan-storage-strategy">Storage Strategy</h3>
<p>Use a dedicated workflow-library domain instead of forcing workflow JSON rows into the current media <code>assets</code> table.</p>
<p>Recommended tables:</p>
<ul>
<li><code>workflows</code></li>
<ul>
<li><code>id</code></li>
<li><code>workflow_id</code></li>
<li><code>name</code></li>
<li><code>description</code></li>
<li><code>filepath</code></li>
<li><code>source</code></li>
<li><code>category</code></li>
<li><code>task</code></li>
<li><code>model_family</code></li>
<li><code>provider</code></li>
<li><code>runs_on</code></li>
<li><code>thumbnail_path</code></li>
<li><code>animated_thumbnail_path</code></li>
<li><code>workflow_hash</code></li>
<li><code>node_count</code></li>
<li><code>link_count</code></li>
<li><code>subgraph_count</code></li>
<li><code>required_nodes_json</code></li>
<li><code>required_models_json</code></li>
<li><code>missing_nodes_json</code></li>
<li><code>missing_models_json</code></li>
<li><code>tags_json</code></li>
<li><code>favorite</code></li>
<li><code>usage_count</code></li>
<li><code>last_loaded_at</code></li>
<li><code>mtime</code></li>
<li><code>size</code></li>
<li><code>created_at</code></li>
<li><code>updated_at</code></li>
</ul>
</ul>
<ul>
<li><code>workflow_categories</code> (optional cache/index only; folder tree remains source of</li>
<p>truth)</p>
<ul>
<li><code>id</code></li>
<li><code>name</code></li>
<li><code>slug</code></li>
<li><code>sort_order</code></li>
</ul>
</ul>
<ul>
<li>optional <code>workflow_assets</code></li>
<ul>
<li>links generated media assets to a workflow row by <code>workflow_id</code> or</li>
<p><code>workflow_hash</code></p>
</ul>
</ul>
<p>Reasoning:</p>
<ul>
<li>Avoids breaking code that assumes <code>assets.kind</code> is media.</li>
<li>Keeps workflow-specific filters cheap and explicit.</li>
<li>Allows thumbnails and model requirements without overloading</li>
<p><code>asset_metadata.metadata_raw</code>.</p>
<li>Still allows a compatibility response shaped like grid assets.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-workflow-library-roots">Workflow Library Roots</h3>
<p>Support these roots in priority order:</p>
<ol>
<li>ComfyUI user workflows via <code>/userdata</code> / <code>/v2/userdata</code> when available.</li>
<li>Local ComfyUI user folder fallback, usually <code>ComfyUI/user/&amp;lt;user&amp;gt;/workflows</code>.</li>
<li>Custom-node <code>example_workflows</code> as read-only entries under a separate</li>
<p>category in Workflow scope.</p>
</ol>
<p>Do not write into arbitrary paths unless the existing write-access, CSRF, and path guard stack allows it.</p>
<h3 id="workflow_scope_implementation_plan-api-surface">API Surface</h3>
<p>Add Majoor routes under <code>/mjr/am/workflows/*</code>:</p>
<ul>
<li><code>GET /mjr/am/workflows</code></li>
<ul>
<li>list workflows with filters, sorting, paging</li>
</ul>
<li><code>GET /mjr/am/workflows/{id}</code></li>
<ul>
<li>workflow detail, parsed metadata, nodes, model requirements</li>
</ul>
<li><code>GET /mjr/am/workflows/{id}/content</code></li>
<ul>
<li>safe JSON content fetch for load/inspect</li>
</ul>
<li><code>POST /mjr/am/workflows/save-current</code></li>
<ul>
<li>save a posted frontend workflow JSON into the workflow library</li>
</ul>
<li><code>POST /mjr/am/workflows/import</code></li>
<ul>
<li>import uploaded or referenced workflow JSON</li>
</ul>
<li><code>POST /mjr/am/workflows/{id}/rename</code></li>
<li><code>POST /mjr/am/workflows/{id}/categorize</code></li>
<li><code>POST /mjr/am/workflows/{id}/thumbnail</code></li>
<li><code>POST /mjr/am/workflows/{id}/duplicate</code></li>
<li><code>DELETE /mjr/am/workflows/{id}</code></li>
</ul>
<p>Also extend existing list route:</p>
<ul>
<li><code>GET /mjr/am/list?scope=workflow</code></li>
<ul>
<li>returns workflow-card shaped items for the current grid</li>
</ul>
</ul>
<h3 id="workflow_scope_implementation_plan-frontend-host-bridge">Frontend Host Bridge</h3>
<p>Add workflow host operations behind <code>ui/app/comfyApiBridge.ts</code> or <code>ui/app/hostAdapter.ts</code>:</p>
<ul>
<li>read current graph/workflow safely</li>
<li>serialize current workflow</li>
<li>load workflow into ComfyUI using feature-detected public APIs first</li>
<li>fetch ComfyUI <code>/workflow_templates</code> and <code>/userdata</code> only through</li>
<p><code>fetchComfyApi()</code></p>
<li>check <code>/object_info</code> and <code>/models</code> for missing requirements</li>
</ul>
<p>Vue components must receive data/actions through props or stores. They should not call <code>window.app</code>, <code>globalThis.app</code>, <code>LiteGraph</code>, canvas, or graph objects directly.</p>
<h3 id="workflow_scope_implementation_plan-card-ux-contract">Card UX Contract</h3>
<p>Workflow card fields should be normalized before Vue rendering:</p>
<ul>
<li><code>id</code></li>
<li><code>kind: "workflow"</code></li>
<li><code>filename</code></li>
<li><code>display_name</code></li>
<li><code>description</code></li>
<li><code>thumbnail_url</code></li>
<li><code>animated_thumbnail_url</code></li>
<li><code>task</code></li>
<li><code>model_family</code></li>
<li><code>provider</code></li>
<li><code>runs_on</code></li>
<li><code>node_count</code></li>
<li><code>subgraph_count</code></li>
<li><code>missing_nodes_count</code></li>
<li><code>missing_models_count</code></li>
<li><code>tags</code></li>
<li><code>mtime</code></li>
<li><code>favorite</code></li>
<li><code>usage_count</code></li>
</ul>
<p>Card behavior:</p>
<ul>
<li>show image thumbnail by default</li>
<li>if animated preview exists, play on hover or respect existing video autoplay</li>
<p>grid setting</p>
<li>show task/model/provider chips</li>
<li>show missing dependency warning chip</li>
<li>primary click opens detail/preview, not destructive load</li>
<li>explicit action button loads workflow into ComfyUI, preferring a new workflow</li>
<p>tab when host API allows it</p>
</ul>
<h3 id="workflow_scope_implementation_plan-classification-heuristics">Classification Heuristics</h3>
<p>Use a deterministic classifier first, then allow user overrides.</p>
<p>Task detection:</p>
<ul>
<li><code>T2I</code>: text/prompt nodes + image save nodes, no image/video input dependency</li>
<li><code>I2I</code>: image loader/input nodes feeding sampler/edit/upscale pipelines</li>
<li><code>I2V</code>: image input plus video-generation/video-combine nodes</li>
<li><code>T2V</code>: prompt plus video-generation/video-combine nodes</li>
<li><code>Video Edit</code>: video loader plus video output</li>
<li><code>Image Edit</code>: inpaint/control/edit/provider API nodes</li>
<li><code>Upscale</code>: upscaler/model upscale nodes</li>
<li><code>Audio</code>: audio loader/output/tts/music nodes</li>
<li><code>3D</code>: mesh/3D/model loader/export nodes</li>
</ul>
<p>Model family detection:</p>
<ul>
<li>inspect node class names, widget values, model filenames, and loader nodes</li>
<li>include known families such as <code>Qwen</code>, <code>Flux</code>, <code>Wan</code>, <code>Seedance</code>, <code>LTX</code>,</li>
<p><code>SDXL</code>, <code>SD1.5</code>, <code>Hunyuan</code>, <code>Kling</code>, <code>Veo</code>, <code>Grok</code>, etc.</p>
<li>keep classifier rules data-driven so new model families can be added without</li>
<p>rewriting UI code</p>
</ul>
<p>Dependency detection:</p>
<ul>
<li>node classes from workflow JSON vs <code>/object_info</code></li>
<li>model filenames from loader widgets vs <code>/models/{folder}</code> where feasible</li>
<li>provider/API nodes marked separately from local model requirements</li>
</ul>
<p>Subgraph requirement:</p>
<ul>
<li>When parsing workflow JSON, include native subgraphs:</li>
<ul>
<li>root graph from <code>app.rootGraph</code>, <code>app.graph.rootGraph</code>, <code>app.graph</code>, or</li>
<p><code>app.canvas.graph</code></p>
<li><code>rootGraph.subgraphs</code></li>
<li><code>node.subgraph</code> when <code>node.isSubgraphNode?.()</code> or <code>node.subgraph</code> exists</li>
<li>serialized <code>workflow.definitions.subgraphs</code></li>
</ul>
<li>Qualify node IDs by graph/subgraph identity because IDs are not globally</li>
<p>unique across subgraphs.</p>
</ul>
<h2 id="workflow_scope_implementation_plan-implementation-checklist">Implementation Checklist</h2>
<h3 id="workflow_scope_implementation_plan-phase-0-discovery-spike">Phase 0 - Discovery Spike</h3>
<ul>
<li>[ ] Fetch and inspect current ComfyUI <code>/workflow_templates</code> payload shape.</li>
<li>[ ] Fetch and inspect current ComfyUI <code>/v2/userdata?dir=workflows</code> behavior on</li>
<p>stable and nightly builds.</p>
<li>[x] Confirm frontend load workflow public API names in current</li>
<p><code>ComfyUI_frontend</code>.</p>
<li>[x] Confirm whether saved workflows live under <code>user/&amp;lt;user&amp;gt;/workflows</code> in the</li>
<p>target install and whether multi-user mode changes that path.</p>
<li>[x] Inventory local workflow JSON examples and thumbnail conventions.</li>
<li>[x] Decide whether custom-node <code>example_workflows</code> should appear read-only in</li>
<p>the Workflow tab or stay delegated to ComfyUI Templates.</p>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-1-backend-domain">Phase 1 - Backend Domain</h3>
<ul>
<li>[x] Add DB migration for workflow library tables and indexes.</li>
<li>[x] Add <code>features/workflows/models.py</code> for normalized workflow records.</li>
<li>[x] Add <code>features/workflows/parser.py</code> for workflow JSON shape parsing,</li>
<p>including subgraph traversal.</p>
<li>[x] Add <code>features/workflows/classifier.py</code> for task/model/provider/runs-on</li>
<p>classification.</p>
<li>[x] Add <code>features/workflows/service.py</code> for list/detail/import/save/update.</li>
<li>[x] Add path guards for workflow JSON and thumbnail files.</li>
<li>[x] Add write operations behind existing CSRF/write-access/security gates.</li>
<li>[x] Add audit logging for save/import/rename/categorize/thumbnail/delete.</li>
<li>[x] Add rate limits for list, import, save, and delete operations.</li>
<li>[x] Add thumbnail serving route with JSON/media content-type hardening.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-2-backend-routes">Phase 2 - Backend Routes</h3>
<ul>
<li>[x] Add <code>routes/handlers/workflows.py</code>.</li>
<li>[x] Register workflow routes in <code>routes/route_catalog.py</code>.</li>
<li>[x] Extend <code>mjr_am_backend/routes/search/listing_endpoint.py</code> to accept</li>
<p><code>scope=workflow</code>.</p>
<li>[x] Add <code>listing_workflow_scope.py</code> or equivalent scoped handler.</li>
<li>[x] Normalize workflow list results into grid-compatible card payloads.</li>
<li>[x] Add filters: <code>workflow_task</code>, <code>workflow_model</code>, <code>workflow_provider</code>,</li>
<p><code>runs_on</code>.</p>
<li>[x] Add filters: <code>missing</code>, <code>favorite</code>, <code>tag</code>.</li>
<li>[x] Add sort keys: <code>workflow_default</code>, <code>name</code>, <code>mtime_desc</code>, <code>task</code>,</li>
<p><code>model_family</code>, <code>usage_count</code>, <code>last_loaded_at</code>.</p>
<li>[x] Add tests for invalid paths, bad JSON, missing thumbnail, and remote</li>
<p>write denial.</p>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-3-frontend-api-client">Phase 3 - Frontend API Client</h3>
<ul>
<li>[x] Add workflow endpoints to <code>ui/api/endpoints.ts</code>.</li>
<li>[x] Add typed client methods in <code>ui/api/client.ts</code> or a workflow-specific API</li>
<p>module.</p>
<li>[x] Add <code>buildWorkflowThumbnailURL()</code> and avoid using ComfyUI <code>/view</code> for JSON</li>
<p>files.</p>
<li>[x] Add frontend fetch tests for query construction and error mapping.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-4-scope-integration">Phase 4 - Scope Integration</h3>
<ul>
<li>[x] Add <code>tab.workflow</code> and <code>tooltip.tab.workflow</code> i18n entries.</li>
<li>[x] Add Workflow button in <code>ui/features/panel/views/tabsView.ts</code>.</li>
<li>[x] Extend allowed scopes in <code>scopeController.ts</code>.</li>
<li>[x] Update <code>panelStateBridge</code> tracked state for workflow filters.</li>
<li>[x] Ensure <code>gridController.ts</code> writes <code>mjrScope=workflow</code> and workflow filter</li>
<p>datasets before reload.</p>
<li>[x] Disable execution stack grouping for <code>workflow</code> scope unless explicitly</li>
<p>needed.</p>
<li>[x] Update status/summary text for workflow counts.</li>
<li>[x] Add tests for scope switching and stale custom-root state reset.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-5-workflow-cards">Phase 5 - Workflow Cards</h3>
<ul>
<li>[x] Add a dedicated <code>WorkflowCardInner.vue</code> or branch safely inside</li>
<p><code>AssetCardInner.vue</code> when <code>kind === "workflow"</code>.</p>
<li>[x] Render static thumbnail, animated preview, graph-map fallback, or icon</li>
<p>fallback.</p>
<li>[x] Render task/model/provider/runs-on chips.</li>
<li>[x] Render dependency warning state.</li>
<li>[x] Add card action buttons: load, inspect, favorite, menu.</li>
<li>[x] Keep card dimensions stable with existing virtual grid constraints.</li>
<li>[x] Add Vue tests for cards with image thumbnail, animated thumbnail, missing</li>
<p>dependency warning, and no-thumbnail fallback.</p>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-6-workflow-load-and-save">Phase 6 - Workflow Load And Save</h3>
<ul>
<li>[x] Add bridge method to serialize the current ComfyUI workflow.</li>
<li>[x] Add bridge method to load a workflow into ComfyUI using feature detection.</li>
<li>[x] Add "Save current workflow" action in the Workflow tab toolbar/menu.</li>
<li>[x] Add "Load workflow" card action with confirmation if current graph is</li>
<p>dirty and ComfyUI exposes a reliable dirty-state signal.</p>
<li>[x] Increment usage stats and <code>last_loaded_at</code> after successful load.</li>
<li>[x] Add fallback path: download/open JSON if host load API is unavailable.</li>
<li>[x] Add tests with mocked ComfyUI app/API surfaces.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-7-filters-and-organization">Phase 7 - Filters And Organization</h3>
<ul>
<li>[x] Add task filter menu with common buckets (<code>T2I</code>, <code>I2I</code>, <code>I2V</code>, <code>T2V</code>,</li>
<p><code>Image Edit</code>, <code>Video Edit</code>, <code>Upscale</code>, <code>Audio</code>, <code>3D</code>).</p>
<li>[x] Add model-family filter menu, populated from indexed workflows.</li>
<li>[x] Add runs-on filter (<code>local</code>, <code>api</code>, <code>cloud</code>, <code>unknown</code>).</li>
<li>[x] Add category editor and category sidebar/list if needed.</li>
<li>[x] Add drag/drop or menu action to move workflow between categories.</li>
<li>[x] Add tag support if workflow library should share existing tag UI.</li>
<li>[x] Add search over name, description, tags, model family, node classes, and</li>
<p>raw model filenames.</p>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-8-thumbnails">Phase 8 - Thumbnails</h3>
<ul>
<li>[x] Detect same-name thumbnail files next to workflow JSON.</li>
<li>[x] Allow user to select existing media asset as thumbnail.</li>
<li>[x] Allow first linked output by <code>workflow_id</code> / <code>workflow_hash</code> as suggested</li>
<p>thumbnail.</p>
<li>[x] Add optional animated preview support (<code>webp</code>, <code>gif</code>, <code>mp4</code>) with existing</li>
<p>grid autoplay setting.</p>
<li>[x] Add fallback graph-map screenshot generation only if it can be done without</li>
<p>heavy server-side rendering.</p>
<li>[x] Harden thumbnail serving to image/video extensions only.</li>
</ul>
<h3 id="workflow_scope_implementation_plan-phase-9-validation-and-qa">Phase 9 - Validation And QA</h3>
<ul>
<li>[x] Python tests for parser/classifier/subgraph traversal.</li>
<li>[x] Python tests for DB migration and workflow route security.</li>
<li>[x] Python tests for <code>/mjr/am/list?scope=workflow</code>.</li>
<li>[x] JS tests for tab/scope integration.</li>
<li>[ ] Vue tests for workflow cards and filters.</li>
<li>[x] Bridge tests for save/load with stable and nightly ComfyUI API mocks.</li>
<li>[x] Run <code>python scripts/run_changed_quality_gate.py</code>.</li>
<li>[x] Run <code>npm run lint:js</code>.</li>
<li>[x] Run targeted <code>vitest</code> files.</li>
<li>[x] Run <code>npm run build</code> and update <code>dist/</code>.</li>
<li>[ ] Manual test in ComfyUI stable.</li>
<li>[ ] Manual test in ComfyUI nightly.</li>
</ul>
<h2 id="workflow_scope_implementation_plan-risks-and-mitigations">Risks And Mitigations</h2>
<ul>
<li><strong>ComfyUI frontend API drift</strong></li>
<ul>
<li>Mitigation: all host calls go through <code>comfyApiBridge.ts</code> /</li>
<p><code>hostAdapter.ts</code>, with feature detection and fallback download/open modes.</p>
</ul>
</ul>
<ul>
<li><strong>Workflow file location differs by user mode</strong></li>
<ul>
<li>Mitigation: prefer <code>/userdata</code> / <code>/v2/userdata</code>; filesystem fallback only</li>
<p>when safely resolved.</p>
</ul>
</ul>
<ul>
<li><strong>Saved workflow JSON schemas vary</strong></li>
<ul>
<li>Mitigation: parser accepts root workflow, API prompt shape, frontend graph</li>
<p>shape, and <code>definitions.subgraphs</code>; unknown fields are preserved.</p>
</ul>
</ul>
<ul>
<li><strong>Media grid assumptions break on JSON rows</strong></li>
<ul>
<li>Mitigation: use dedicated workflow tables and normalize to card payloads,</li>
<p>instead of storing JSON as regular media <code>assets</code>.</p>
</ul>
</ul>
<ul>
<li><strong>Dependency checks become slow</strong></li>
<ul>
<li>Mitigation: cache <code>/object_info</code> and <code>/models</code> snapshots with TTL; classify</li>
<p>on index, not on every card render.</p>
</ul>
</ul>
<ul>
<li><strong>Delete/move can damage user workflow libraries</strong></li>
<ul>
<li>Mitigation: keep existing write guard, CSRF, path root checks, audit log,</li>
<p>and confirmation UI.</p>
</ul>
</ul>
<h2 id="workflow_scope_implementation_plan-open-decisions">Open Decisions</h2>
<ul>
<li>[x] Should custom-node <code>example_workflows</code> be shown in the Workflow tab, or</li>
<p>only in ComfyUI's native Templates browser? Decision: show them in Workflow scope under a separate read-only category.</p>
<li>[x] Should Majoor create a dedicated workflow library folder, or only use</li>
<p>ComfyUI <code>user/&amp;lt;user&amp;gt;/workflows</code>? Decision: use ComfyUI <code>user/&amp;lt;user&amp;gt;/workflows</code> as the workflow library root.</p>
<li>[x] Should categories be stored as folders, DB metadata, or both?</li>
<p>Decision: categories are folder-based (filesystem source of truth).</p>
<li>[x] Should workflow cards share current rating/tags/collections, or use</li>
<p>workflow-specific favorites/categories first? Decision: use workflow-specific favorites/categories first.</p>
</ul>
<p>Implementation note: workflow favorites are now persisted via <code>POST /mjr/am/workflows/favorite</code> and reflected in workflow card state.</p>
<ul>
<li>[x] Should "load workflow" replace the current graph immediately, open a new</li>
<p>ComfyUI workflow tab if available, or ask every time? Decision: open a new ComfyUI workflow tab when available; otherwise fallback to current-canvas import with confirmation when dirty-state is detectable.</p>
<li>[x] Should thumbnails be manually assigned only, or generated from graph-map /</li>
<p>first linked output automatically? Decision: default thumbnail source is latest linked output by <code>workflow_id</code>, with optional manual assignment and optional graph-map generation fallback.</p>
</ul>
<h2 id="workflow_scope_implementation_plan-first-implementation-slice">First Implementation Slice</h2>
<p>Recommended first slice:</p>
<ol>
<li>Add read-only <code>scope=workflow</code> listing from a configured workflow directory.</li>
<li>Parse workflow JSON and classify task/model family locally.</li>
<li>Add Workflow tab and workflow cards with static thumbnail fallback.</li>
<li>Add load workflow action through a bridge with safe fallback.</li>
<li>Add save-current/import/delete only after the read-only browser is stable.</li>
</ol>
<p>This slice proves the UX and parser without introducing destructive operations or a large migration at the same time.</p></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/WORKFLOW_SCOPE_IMPLEMENTATION_PLAN.md</code>
</div>
</div>
</section>
<section class="panel section" id="node-stream-reactivation">
<div class="panel-h"><h2>node-stream-reactivation.md</h2></div>
<div class="panel-b">
<div style="color: var(--muted); font-size: 12px; margin-bottom: 10px;">Additional documentation</div>
<div class="section"><h1 id="node-stream-reactivation-node-stream-notes">Node Stream Notes</h1>
<h2 id="node-stream-reactivation-current-status">Current Status</h2>
<p><code>Node Stream</code> is enabled. The single source of truth is:</p>
<ul>
<li><code>ui/features/viewer/nodeStream/nodeStreamFeatureFlag.js</code></li>
</ul>
<p>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 <code>node.__imageops_state.canvas</code>.</p>
<p>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.</p>
<p>If <code>NODE_STREAM_FEATURE_ENABLED</code> is set to <code>false</code>:</p>
<ul>
<li>the Floating Viewer toolbar does not show the Node Stream button,</li>
<li>the <code>N</code> shortcut does nothing,</li>
<li><code>floatingViewerManager</code> ignores Node Stream activation and payloads,</li>
<li><code>entry.js</code> does not initialize the controller or expose the inspection-only <code>window.MajoorNodeStream</code> helpers,</li>
<li><code>NodeStreamController</code> stays inert even if called directly.</li>
</ul>
<h2 id="node-stream-reactivation-files-involved">Files Involved</h2>
<ul>
<li><code>ui/features/viewer/nodeStream/nodeStreamFeatureFlag.js</code></li>
<li><code>ui/features/viewer/nodeStream/NodeStreamController.js</code></li>
<li><code>ui/features/viewer/nodeStream/imageOpsPreviewBridge.js</code></li>
<li><code>ui/features/viewer/FloatingViewer.js</code></li>
<li><code>ui/features/viewer/floatingViewerManager.js</code></li>
<li><code>ui/entry.ts</code></li>
<li><code>ui/app/events.js</code></li>
<li><code>ui/tests/node_stream_controller.vitest.mjs</code></li>
</ul>
<h2 id="node-stream-reactivation-smoke-tests">Smoke Tests</h2>
<p>Use the real local stack and verify these cases from the ComfyUI canvas:</p>
<ul>
<li>Core <code>LoadImage</code></li>
<li>Core <code>LoadImageMask</code></li>
<li><code>ImageOpsPreview</code></li>
<li>Any ImageOps processing node with an embedded live preview</li>
<li><code>LayerUtility: ImageBlend</code></li>
<li><code>LayerUtility: ImageBlendAdvance</code></li>
<li><code>LayerMask: MaskPreview</code></li>
<li><code>MaskPreview+</code> from <code>comfyui_essentials</code></li>
<li><code>PreviewImageOrMask</code> and <code>ImageAndMaskPreview</code> from <code>comfyui-kjnodes</code></li>
<li>at least one WAS Suite image node if present locally</li>
</ul>
<h2 id="node-stream-reactivation-expected-behavior">Expected Behavior</h2>
<ul>
<li>Selection-only behavior: previews follow node clicks, not execution events.</li>
<li>If a selected node has no direct preview, downstream preview/save nodes may be</li>
<p>used as the preview source.</p>
<li>ImageOps live preview canvases update in the MFV when their render signature changes.</li>
<li>The feature stays compatible with MFV lifecycle teardown and hot-reload.</li>
</ul>
<h2 id="node-stream-reactivation-stream-feature-boundaries">Stream Feature Boundaries</h2>
<h3 id="node-stream-reactivation-live-stream">Live Stream</h3>
<ul>
<li>Source: <code>NEW_GENERATION_OUTPUT</code> events.</li>
<li>Trigger: workflow execution finishes and ComfyUI reports output files.</li>
<li>Displays: final generated files only.</li>
<li>Does not follow selected canvas nodes.</li>
<li>Limitation: no raw tensor access and no no-queue preview.</li>
</ul>
<h3 id="node-stream-reactivation-ksampler-preview">KSampler Preview</h3>
<ul>
<li>Source: ComfyUI API <code>b_preview_with_metadata</code> or legacy <code>b_preview</code> events.</li>
<li>Trigger: sampler denoising during workflow execution.</li>
<li>Displays: transient preview blobs, usually JPEG/PNG frames.</li>
<li>Limitation: only available while ComfyUI emits sampler previews.</li>
</ul>
<h3 id="node-stream-reactivation-node-stream">Node Stream</h3>
<ul>
<li>Source: selected ComfyUI canvas node.</li>
<li>Trigger: user selects a node while Node Stream is active.</li>
<li>Displays: frontend-observable media such as <code>node.imgs</code>, widget <code>&amp;lt;img&amp;gt;</code>/<code>&amp;lt;video&amp;gt;</code>,</li>
<p>load-widget filenames, downstream preview/save media, and ImageOps live canvases.</p>
<li>Public debug surface: <code>window.MajoorNodeStream.mode</code>, <code>.listAdapters()</code>, and</li>
<p><code>.getKnownNodeSets()</code> are inspection helpers only; third-party adapter registration is not part of the active runtime contract.</p>
<li>Limitation: cannot read backend tensors (<code>IMAGE</code>, <code>MASK</code>, <code>LATENT</code>) before node</li>
<p>execution. For ImageOps, the no-queue live path is the frontend canvas, not the Python tensor output.</p>
</ul></div>
<a class="toplink" href="#top">Back to top</a>
<div style="margin-top: 10px; font-size: 12px; color: var(--muted);">
Source: <code>docs/node-stream-reactivation.md</code>
</div>
</div>
</section>
<div style="text-align:center; color: var(--muted); font-size: 12px; margin-top: 10px;">
Majoor Assets Manager • Documentation • January 2026
</div>
</main>
</div>
<script>
(function(){
const q = document.getElementById('q');
const links = Array.from(document.querySelectorAll('.navlink'));
const panels = Array.from(document.querySelectorAll('section.panel.section'));
const matchesEl = document.getElementById('matches');
function escapeRegExp(s){
return (s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getTokens(){
const raw = (q && q.value ? q.value : '').trim();
if(!raw) return [];
return raw.split(/\s+/g).filter(Boolean);
}
function makeWordRegex(tokens){
if(!tokens.length) return null;
// Note: JS \b word-boundary is based on [A-Za-z0-9_]. Good enough for typical tags/words.
const parts = tokens.map(t => escapeRegExp(t));
return new RegExp('\\b(?:' + parts.join('|') + ')\\b', 'gi');
}
function panelMatchesAllTokens(panelText, tokens){
if(!tokens.length) return true;
const text = (panelText || '').toLowerCase();
for(const tok of tokens){
// Whole-word match using a boundary approximation.
const r = new RegExp('\\b' + escapeRegExp(tok.toLowerCase()) + '\\b', 'i');
if(!r.test(text)) return false;
}
return true;
}
function clearMarks(root){
const marks = root.querySelectorAll('mark');
for(const m of marks){
const t = document.createTextNode(m.textContent || '');
m.replaceWith(t);
}
}
function markMatches(root, tokens){
const rx = makeWordRegex(tokens);
if(!rx) return 0;
let count = 0;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
if(!node.nodeValue) return NodeFilter.FILTER_REJECT;
const p = node.parentElement;
if(!p) return NodeFilter.FILTER_REJECT;
if(p.closest('pre, code, script, style')) return NodeFilter.FILTER_REJECT;
if(!(p.closest('p, li, blockquote, h1, h2, h3, h4, h5, h6'))) return NodeFilter.FILTER_REJECT;
rx.lastIndex = 0;
return rx.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
const toProcess = [];
while(walker.nextNode()){
toProcess.push(walker.currentNode);
if(toProcess.length > 350) break; // safety
}
for(const node of toProcess){
const text = node.nodeValue || '';
rx.lastIndex = 0;
let m;
let last = 0;
const frag = document.createDocumentFragment();
let changed = false;
while((m = rx.exec(text)) !== null){
const start = m.index;
const end = start + (m[0] ? m[0].length : 0);
if(end <= start) break;
if(start > last) frag.appendChild(document.createTextNode(text.slice(last, start)));
const hit = document.createElement('mark');
hit.textContent = text.slice(start, end);
frag.appendChild(hit);
count += 1;
changed = true;
last = end;
if(count > 800) break;
}
if(changed){
if(last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node);
}
if(count > 800) break;
}
return count;
}
function applyFilter(){
const tokens = getTokens();
let visiblePanels = 0;
let marked = 0;
for(const p of panels){
clearMarks(p);
const t = (p.textContent || '');
const ok = panelMatchesAllTokens(t, tokens);
p.style.display = ok ? '' : 'none';
if(ok){
visiblePanels += 1;
if(tokens.length) marked += markMatches(p, tokens);
}
}
for(const a of links){
const href = a.getAttribute('href') || '';
const id = href.startsWith('#') ? href.slice(1) : '';
if(!tokens.length || id === 'top'){
a.style.display = '';
continue;
}
const target = id ? document.getElementById(id) : null;
a.style.display = (target && target.style.display !== 'none') ? '' : 'none';
}
if(matchesEl){
matchesEl.textContent = tokens.length ? ('Matches: ' + marked + ' • Sections: ' + visiblePanels) : ('Sections: ' + panels.length);
}
}
if(q){
q.addEventListener('input', () => {
applyFilter();
});
}
for(const a of links){
a.addEventListener('click', (e) => {
const href = a.getAttribute('href') || '';
if(href.startsWith('#')){
const id = href.slice(1);
const el = document.getElementById(id);
if(el){
e.preventDefault();
el.scrollIntoView({behavior:'smooth', block:'start'});
history.replaceState(null, '', href);
}
}
});
}
// Allow ?q=... deep-linking
try{
const u = new URL(window.location.href);
const qq = (u.searchParams.get('q') || '').trim();
if(q && qq){
q.value = qq;
}
}catch(_){}
applyFilter();
})();
</script>
</body>
</html>