davinci-resolve-mcp 2.38.0 → 2.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/docs/SKILL.md +7 -1
- package/docs/guides/control-panel.md +121 -52
- package/docs/process/release-process.md +11 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/regen_panel_screenshots.py +75 -0
- package/src/analysis_dashboard.py +197 -41
- package/src/batch_cli.py +2 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +84 -10
- package/src/utils/actor_identity.py +46 -0
- package/src/utils/brain_edits.py +4 -3
- package/src/utils/resolve_ai_ledger.py +6 -4
- package/src/utils/timeline_brain_db.py +15 -1
- package/src/utils/timeline_versioning.py +4 -3
|
@@ -3753,6 +3753,7 @@ HTML = r"""<!doctype html>
|
|
|
3753
3753
|
justify-content: flex-start;
|
|
3754
3754
|
padding-bottom: 2px;
|
|
3755
3755
|
}
|
|
3756
|
+
.control-tabs select { max-width: 150px; min-width: 110px; }
|
|
3756
3757
|
.lab-navbar {
|
|
3757
3758
|
height: 88px;
|
|
3758
3759
|
flex-wrap: wrap;
|
|
@@ -3817,10 +3818,11 @@ HTML = r"""<!doctype html>
|
|
|
3817
3818
|
<nav class="control-tabs" aria-label="Control panel sections">
|
|
3818
3819
|
<button class="control-tab active" data-panel-target="overview">Overview</button>
|
|
3819
3820
|
<div class="control-nav-item">
|
|
3820
|
-
<button class="control-tab has-menu" data-panel-target="analysis">
|
|
3821
|
+
<button class="control-tab has-menu" data-panel-target="analysis">Media <span class="tab-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span></button>
|
|
3821
3822
|
<div class="nav-dropdown" role="menu" aria-label="Analysis pages">
|
|
3822
|
-
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="media" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m7 15 3-3 2 2 4-5 1 2"></path></svg></span>
|
|
3823
|
+
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="media" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m7 15 3-3 2 2 4-5 1 2"></path></svg></span>Inventory</button>
|
|
3823
3824
|
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="review" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="14" rx="2"></rect><circle cx="9" cy="10" r="2"></circle><path d="m21 17-5-5-9 9"></path></svg></span>Review</button>
|
|
3825
|
+
<button class="nav-dropdown-item" id="navHistoryItem" data-panel-target="analysis" data-subpage-target="review" data-review-view="history" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg></span>History</button>
|
|
3824
3826
|
</div>
|
|
3825
3827
|
</div>
|
|
3826
3828
|
<button class="control-tab" data-panel-target="aiconsole">AI Console</button>
|
|
@@ -3969,7 +3971,7 @@ HTML = r"""<!doctype html>
|
|
|
3969
3971
|
<section class="span-12 subpage active" data-subpage-scope="analysis" data-subpage="media">
|
|
3970
3972
|
<div class="section-top">
|
|
3971
3973
|
<div>
|
|
3972
|
-
<h2>
|
|
3974
|
+
<h2>Inventory</h2>
|
|
3973
3975
|
<p class="section-copy">Resolve media is inventoried read-only and filtered to source clips so timelines, compounds, titles, and generated items stay out of analysis queues.</p>
|
|
3974
3976
|
<div class="section-meta" id="resolveProject">Resolve connection pending</div>
|
|
3975
3977
|
<div class="copy-status" id="copyPromptStatus"></div>
|
|
@@ -4048,7 +4050,7 @@ HTML = r"""<!doctype html>
|
|
|
4048
4050
|
</div>
|
|
4049
4051
|
<div class="controls" id="reviewControls">
|
|
4050
4052
|
<button class="secondary" id="reviewRefreshBtn">Refresh</button>
|
|
4051
|
-
<button class="secondary" id="reviewHistoryBtn" title="Timeline edit history
|
|
4053
|
+
<button class="secondary" id="reviewHistoryBtn" title="Timeline edit history">History</button>
|
|
4052
4054
|
<button class="secondary" id="reviewBackBtn" style="display:none">← Back</button>
|
|
4053
4055
|
</div>
|
|
4054
4056
|
</div>
|
|
@@ -4057,7 +4059,6 @@ HTML = r"""<!doctype html>
|
|
|
4057
4059
|
<div class="readiness-card-header">
|
|
4058
4060
|
<span class="readiness-title">Project readiness</span>
|
|
4059
4061
|
<span id="readinessEvidenceBase" class="readiness-evidence"></span>
|
|
4060
|
-
<button id="readinessRefreshBtn" class="secondary" type="button" title="Refresh readiness summary">Refresh</button>
|
|
4061
4062
|
</div>
|
|
4062
4063
|
<div id="readinessSummaryRow" class="readiness-summary-row"></div>
|
|
4063
4064
|
<div id="readinessDetails" class="readiness-details"></div>
|
|
@@ -4181,8 +4182,12 @@ HTML = r"""<!doctype html>
|
|
|
4181
4182
|
|
|
4182
4183
|
<div class="caps-section" style="margin-top:12px;">
|
|
4183
4184
|
<div class="caps-section-head"><div class="caps-section-title">Governance</div>
|
|
4184
|
-
<div class="caps-section-hint">
|
|
4185
|
+
<div class="caps-section-hint">Per-session limits for the two media-creating ops (deblur, speech). In <strong>Advisory</strong> mode you're warned in the confirm dialog but never blocked; in <strong>Enforce</strong> mode an over-tier run is refused until you raise the tier, relax the mode, or consciously override. Pick the tier that matches the job.</div></div>
|
|
4185
4186
|
<div id="aiGovTiers" class="caps-preset-cards" role="radiogroup" aria-label="AI governance tier"></div>
|
|
4187
|
+
<div class="review-view-toggle" id="aiGovMode" role="radiogroup" aria-label="Governance mode" style="margin-top:10px;">
|
|
4188
|
+
<button type="button" data-gov-mode="advisory">Advisory</button>
|
|
4189
|
+
<button type="button" data-gov-mode="enforce">Enforce</button>
|
|
4190
|
+
</div>
|
|
4186
4191
|
<div id="aiGovUsage" class="caps-usage-gauges" style="margin-top:10px;"></div>
|
|
4187
4192
|
</div>
|
|
4188
4193
|
|
|
@@ -4527,6 +4532,7 @@ HTML = r"""<!doctype html>
|
|
|
4527
4532
|
</tr></thead>
|
|
4528
4533
|
<tbody id="resolveAiOpsRows"></tbody>
|
|
4529
4534
|
</table>
|
|
4535
|
+
<div id="resolveAiOpsRecent"></div>
|
|
4530
4536
|
</div>
|
|
4531
4537
|
</div>
|
|
4532
4538
|
|
|
@@ -4853,7 +4859,7 @@ HTML = r"""<!doctype html>
|
|
|
4853
4859
|
const VIEW_ALL_PROJECTS_VALUE = '__view_all_projects__';
|
|
4854
4860
|
const PANEL_LABELS = {
|
|
4855
4861
|
overview: 'Overview',
|
|
4856
|
-
analysis: '
|
|
4862
|
+
analysis: 'Media',
|
|
4857
4863
|
aiconsole: 'AI Console',
|
|
4858
4864
|
diagnostics: 'Setup',
|
|
4859
4865
|
projects: 'Projects',
|
|
@@ -4877,7 +4883,7 @@ HTML = r"""<!doctype html>
|
|
|
4877
4883
|
};
|
|
4878
4884
|
const SUBPAGE_LABELS = {
|
|
4879
4885
|
analysis: {
|
|
4880
|
-
media: '
|
|
4886
|
+
media: 'Inventory',
|
|
4881
4887
|
review: 'Review',
|
|
4882
4888
|
},
|
|
4883
4889
|
diagnostics: {
|
|
@@ -5153,6 +5159,10 @@ HTML = r"""<!doctype html>
|
|
|
5153
5159
|
openClipDetail(clipId, { writePanelState: false, pushHash: false }).catch(alertError);
|
|
5154
5160
|
}
|
|
5155
5161
|
}
|
|
5162
|
+
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'history') {
|
|
5163
|
+
reviewSetView('history');
|
|
5164
|
+
refreshHistoryTimelines().catch(alertError);
|
|
5165
|
+
}
|
|
5156
5166
|
// Deep links: #analysis/review/combined/<id1,id2,...>
|
|
5157
5167
|
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'combined' && route[3]) {
|
|
5158
5168
|
const ids = decodeURIComponent(route[3]).split(',').filter(Boolean);
|
|
@@ -5268,12 +5278,19 @@ HTML = r"""<!doctype html>
|
|
|
5268
5278
|
renderInfoRows('overviewStatusList', [
|
|
5269
5279
|
{ label: 'Resolve project', value: resolveProject, icon: ICONS.project, pill: { tone: 'pill-ok', label: 'Read-only' } },
|
|
5270
5280
|
{ label: 'Source clips', value: `${clipLabel(clips.length)} · ${readyClips} ready`, icon: ICONS.clips, pill: { tone: clipsTone, label: `${readyClips} ready` } },
|
|
5271
|
-
{ label: 'Sequences', value: `${sequences}
|
|
5281
|
+
{ label: 'Sequences', value: sequences ? `${sequences} timeline${sequences === 1 ? '' : 's'} detected (read-only)` : 'No timelines detected', icon: ICONS.sequences },
|
|
5272
5282
|
{ label: 'Clip media status', value: `${onlineClips} online · ${missingClips} missing/offline`, icon: ICONS.status, pill: { tone: mediaTone, label: missingClips ? `${missingClips} missing` : 'All online' } },
|
|
5273
5283
|
{ label: 'Analysis progress', value: `${analyzedClips} of ${clips.length} analyzed (${analyzedPct}%)`, icon: ICONS.analysis, pill: { tone: analysisTone, label: `${analyzedPct}%` }, meter: { percent: analyzedPct } },
|
|
5274
5284
|
{ label: 'Search index', value: index.detail, icon: ICONS.search, pill: { tone: index.tone, label: index.label } },
|
|
5275
5285
|
{ label: 'Safety', value: 'Source media is read-only; outputs stay in the active project analysis root.', icon: ICONS.safety, pill: { tone: 'pill-ok', label: 'Read-only' } },
|
|
5276
5286
|
]);
|
|
5287
|
+
if (!clips.length) {
|
|
5288
|
+
const list = $('overviewStatusList');
|
|
5289
|
+
if (list) list.insertAdjacentHTML('afterbegin', chatPromptCard(
|
|
5290
|
+
'This project has no source clips yet. Import media in Resolve, then ask your assistant to take it from there:',
|
|
5291
|
+
'Look at my current Resolve project, inventory the media, and analyze the source clips.'
|
|
5292
|
+
));
|
|
5293
|
+
}
|
|
5277
5294
|
}
|
|
5278
5295
|
}
|
|
5279
5296
|
|
|
@@ -5700,6 +5717,16 @@ HTML = r"""<!doctype html>
|
|
|
5700
5717
|
analyzeBtn.disabled = !hasAnalyzableClips;
|
|
5701
5718
|
copyBtn.disabled = !hasAnalyzableClips;
|
|
5702
5719
|
setAnalyzeClientButtonsEnabled(hasAnalyzableClips);
|
|
5720
|
+
if (!allClips.length) {
|
|
5721
|
+
body.className = 'empty';
|
|
5722
|
+
body.innerHTML = chatPromptCard(
|
|
5723
|
+
'The Media Pool has no source clips yet. Import media in Resolve, then ask your assistant to inventory and analyze it:',
|
|
5724
|
+
'Look at my current Resolve project, inventory the media, and analyze the source clips.'
|
|
5725
|
+
);
|
|
5726
|
+
updateMediaPollStatus();
|
|
5727
|
+
renderControlPanels();
|
|
5728
|
+
return;
|
|
5729
|
+
}
|
|
5703
5730
|
body.className = '';
|
|
5704
5731
|
const rows = clips.map((clip, index) => {
|
|
5705
5732
|
// The server normalizes 'succeeded'/'skipped' → 'analyzed' whenever the
|
|
@@ -6325,6 +6352,7 @@ HTML = r"""<!doctype html>
|
|
|
6325
6352
|
state.resolveMediaStale = false;
|
|
6326
6353
|
if (res.status === 304) return;
|
|
6327
6354
|
const payload = await res.json();
|
|
6355
|
+
if (payload && payload.unchanged) return;
|
|
6328
6356
|
if (!res.ok || payload.success === false) {
|
|
6329
6357
|
throw new Error(payload.error || res.statusText);
|
|
6330
6358
|
}
|
|
@@ -6467,6 +6495,21 @@ HTML = r"""<!doctype html>
|
|
|
6467
6495
|
}
|
|
6468
6496
|
}
|
|
6469
6497
|
|
|
6498
|
+
function chatPromptCard(message, prompt) {
|
|
6499
|
+
return `<div class="empty" style="padding:var(--space-3);border:1px dashed var(--border-default);border-radius:var(--radius-md);display:grid;gap:8px;justify-items:start;text-align:left;">
|
|
6500
|
+
<span>${escapeHtml(message)}</span>
|
|
6501
|
+
<code style="font-size:12px;color:var(--text-secondary);white-space:normal;">\u201c${escapeHtml(prompt)}\u201d</code>
|
|
6502
|
+
<button class="secondary" type="button" data-copy-chat-prompt="${escapeHtml(prompt)}">Copy prompt</button>
|
|
6503
|
+
</div>`;
|
|
6504
|
+
}
|
|
6505
|
+
document.addEventListener('click', async (e) => {
|
|
6506
|
+
const btn = e.target.closest('[data-copy-chat-prompt]');
|
|
6507
|
+
if (!btn) return;
|
|
6508
|
+
const ok = await writePromptToClipboard(btn.dataset.copyChatPrompt);
|
|
6509
|
+
btn.textContent = ok ? 'Copied \u2014 paste in your chat session' : 'Copy prompt';
|
|
6510
|
+
setTimeout(() => { btn.textContent = 'Copy prompt'; }, 2600);
|
|
6511
|
+
});
|
|
6512
|
+
|
|
6470
6513
|
async function copyMcpPrompt() {
|
|
6471
6514
|
const prompt = buildMcpPrompt();
|
|
6472
6515
|
await writePromptToClipboard(prompt);
|
|
@@ -6829,6 +6872,7 @@ HTML = r"""<!doctype html>
|
|
|
6829
6872
|
const interval = Number($('mediaPollInterval').value || 0);
|
|
6830
6873
|
if (enabled && interval > 0) {
|
|
6831
6874
|
state.mediaPollTimer = setInterval(() => {
|
|
6875
|
+
if (document.hidden) return;
|
|
6832
6876
|
refreshResolveMedia({ silent: true }).catch(error => {
|
|
6833
6877
|
console.warn('Resolve media poll failed', error);
|
|
6834
6878
|
updateMediaPollStatus(error.message || String(error));
|
|
@@ -7306,6 +7350,17 @@ HTML = r"""<!doctype html>
|
|
|
7306
7350
|
<td style="padding:4px 6px;">${b.bytes_created ? fmtBytes(b.bytes_created) : '—'}</td>
|
|
7307
7351
|
</tr>`;
|
|
7308
7352
|
}).join('');
|
|
7353
|
+
const recent = Array.isArray(data.recent) ? data.recent.slice(0, 8) : [];
|
|
7354
|
+
const recentEl = $('resolveAiOpsRecent');
|
|
7355
|
+
if (recentEl) {
|
|
7356
|
+
recentEl.innerHTML = recent.length ? '<div class="caps-section-hint" style="margin-top:8px;">Recent runs</div>'
|
|
7357
|
+
+ recent.map(r => {
|
|
7358
|
+
const when = (r.occurred_at || '').replace('T', ' ');
|
|
7359
|
+
const who = r.actor ? ` · <span title="instance:pid that performed the op">${escapeHtml(r.actor)}</span>` : '';
|
|
7360
|
+
const ok = r.success ? 'ok' : 'failed';
|
|
7361
|
+
return `<div style="font-size:12px; color:var(--text-secondary); padding:2px 0;">${escapeHtml(when)} · ${escapeHtml(r.op)} · ${ok}${who}</div>`;
|
|
7362
|
+
}).join('') : '';
|
|
7363
|
+
}
|
|
7309
7364
|
tableEl.style.display = '';
|
|
7310
7365
|
}
|
|
7311
7366
|
|
|
@@ -7471,7 +7526,7 @@ HTML = r"""<!doctype html>
|
|
|
7471
7526
|
standard: 'Sensible default',
|
|
7472
7527
|
strict: 'Tight leash',
|
|
7473
7528
|
};
|
|
7474
|
-
state.aiGov = state.aiGov || { tier: 'standard', thresholds: {}, usage: {} };
|
|
7529
|
+
state.aiGov = state.aiGov || { tier: 'standard', mode: 'advisory', thresholds: {}, usage: {} };
|
|
7475
7530
|
|
|
7476
7531
|
function aiGovFmt(dim, v) {
|
|
7477
7532
|
if (v == null) return '∞';
|
|
@@ -7515,14 +7570,26 @@ HTML = r"""<!doctype html>
|
|
|
7515
7570
|
</div>`;
|
|
7516
7571
|
}).join('');
|
|
7517
7572
|
}
|
|
7573
|
+
function renderGovMode() {
|
|
7574
|
+
const el = $('aiGovMode');
|
|
7575
|
+
if (!el) return;
|
|
7576
|
+
const mode = state.aiGov.mode || 'advisory';
|
|
7577
|
+
el.querySelectorAll('[data-gov-mode]').forEach(btn => {
|
|
7578
|
+
const active = btn.dataset.govMode === mode;
|
|
7579
|
+
btn.classList.toggle('active', active);
|
|
7580
|
+
btn.setAttribute('aria-checked', active ? 'true' : 'false');
|
|
7581
|
+
});
|
|
7582
|
+
}
|
|
7518
7583
|
async function refreshGovernance() {
|
|
7519
7584
|
const data = await api('/api/resolve_ai/governance').catch(() => ({ success: false }));
|
|
7520
7585
|
if (!data || !data.success) return;
|
|
7521
7586
|
state.aiGov.tier = data.tier;
|
|
7587
|
+
state.aiGov.mode = data.mode || 'advisory';
|
|
7522
7588
|
state.aiGov.thresholds = data.thresholds || {};
|
|
7523
7589
|
state.aiGov.usage = data.usage || {};
|
|
7524
7590
|
state.aiGov.tiersAvailable = data.tiers_available || {};
|
|
7525
7591
|
renderGovTiers();
|
|
7592
|
+
renderGovMode();
|
|
7526
7593
|
renderGovUsage();
|
|
7527
7594
|
}
|
|
7528
7595
|
async function setGovernanceTier(tier) {
|
|
@@ -7534,6 +7601,15 @@ HTML = r"""<!doctype html>
|
|
|
7534
7601
|
if (!res || !res.success) console.warn('governance save failed:', res && res.error);
|
|
7535
7602
|
await refreshGovernance();
|
|
7536
7603
|
}
|
|
7604
|
+
async function setGovernanceMode(mode) {
|
|
7605
|
+
state.aiGov.mode = mode;
|
|
7606
|
+
renderGovMode();
|
|
7607
|
+
const res = await api('/api/resolve_ai/governance', {
|
|
7608
|
+
method: 'POST', body: JSON.stringify({ mode }),
|
|
7609
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
7610
|
+
if (!res || !res.success) console.warn('governance mode save failed:', res && res.error);
|
|
7611
|
+
await refreshGovernance();
|
|
7612
|
+
}
|
|
7537
7613
|
|
|
7538
7614
|
function initAiConsole() {
|
|
7539
7615
|
if (_aiConsoleInit) { renderAiConsole(); refreshGovernance().catch(() => {}); return; }
|
|
@@ -7549,6 +7625,13 @@ HTML = r"""<!doctype html>
|
|
|
7549
7625
|
if (card && card.dataset.govTier) setGovernanceTier(card.dataset.govTier);
|
|
7550
7626
|
});
|
|
7551
7627
|
}
|
|
7628
|
+
const govModeEl = $('aiGovMode');
|
|
7629
|
+
if (govModeEl) {
|
|
7630
|
+
govModeEl.addEventListener('click', (ev) => {
|
|
7631
|
+
const btn = ev.target.closest('[data-gov-mode]');
|
|
7632
|
+
if (btn && btn.dataset.govMode) setGovernanceMode(btn.dataset.govMode);
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7552
7635
|
refreshGovernance().catch(() => {});
|
|
7553
7636
|
}
|
|
7554
7637
|
|
|
@@ -7979,7 +8062,7 @@ HTML = r"""<!doctype html>
|
|
|
7979
8062
|
else if (view === 'shot') meta.textContent = `Shot ${state.review.currentShotIndex} of ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
7980
8063
|
else if (view === 'transcript') meta.textContent = `Transcript · ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
7981
8064
|
else if (view === 'combined') meta.textContent = `Combined review · ${state.review.combinedData?.clip_count || '?'} clips`;
|
|
7982
|
-
else if (view === 'history') meta.textContent = 'Timeline history · versions and brain edits per timeline
|
|
8065
|
+
else if (view === 'history') meta.textContent = 'Timeline history · archived versions and brain edits per timeline';
|
|
7983
8066
|
}
|
|
7984
8067
|
if (opts.writePanelState !== false) {
|
|
7985
8068
|
writePanelStateAsync({
|
|
@@ -8037,24 +8120,23 @@ HTML = r"""<!doctype html>
|
|
|
8037
8120
|
].map(stat => `<div class="readiness-stat ${stat.kind}"><span class="stat-value">${stat.value}</span><span class="stat-label">${escapeHtml(stat.label)}</span></div>`).join('');
|
|
8038
8121
|
}
|
|
8039
8122
|
if (details) {
|
|
8123
|
+
const TRUST_LABELS = { trusted: 'Trusted source', unknown: 'Unverified source', untrusted: 'Untrusted source' };
|
|
8124
|
+
const LAYER_LABELS = {
|
|
8125
|
+
technical: 'Technical', motion: 'Motion', transcription: 'Transcript',
|
|
8126
|
+
vision: 'Vision', cut_analysis: 'Cut analysis', readthrough: 'Readthrough',
|
|
8127
|
+
};
|
|
8040
8128
|
const trustChips = Object.entries(trustDist)
|
|
8041
8129
|
.sort((a, b) => b[1] - a[1])
|
|
8042
|
-
.map(([k, v]) => `<span class="chip" title="
|
|
8130
|
+
.map(([k, v]) => `<span class="chip" title="clips by source trust">${escapeHtml(TRUST_LABELS[k] || k)} · ${v}</span>`)
|
|
8043
8131
|
.join('');
|
|
8044
|
-
const layerChips =
|
|
8045
|
-
.
|
|
8132
|
+
const layerChips = Object.entries(LAYER_LABELS)
|
|
8133
|
+
.filter(([layer]) => layers[layer])
|
|
8134
|
+
.map(([layer, label]) => `<span class="chip" title="clips with this analysis layer">${escapeHtml(label)} · ${layers[layer]}</span>`)
|
|
8046
8135
|
.join('');
|
|
8047
|
-
details.innerHTML =
|
|
8136
|
+
details.innerHTML = trustChips + layerChips;
|
|
8048
8137
|
}
|
|
8049
8138
|
}
|
|
8050
8139
|
|
|
8051
|
-
document.addEventListener('click', (e) => {
|
|
8052
|
-
const target = e.target;
|
|
8053
|
-
if (target && target.id === 'readinessRefreshBtn') {
|
|
8054
|
-
refreshReadinessCard().catch(() => {});
|
|
8055
|
-
}
|
|
8056
|
-
});
|
|
8057
|
-
|
|
8058
8140
|
function populateBinFilter() {
|
|
8059
8141
|
const data = state.review.clipList;
|
|
8060
8142
|
const select = $('reviewBinFilter');
|
|
@@ -8097,10 +8179,16 @@ HTML = r"""<!doctype html>
|
|
|
8097
8179
|
}
|
|
8098
8180
|
const clips = filteredClips();
|
|
8099
8181
|
if (!clips.length) {
|
|
8100
|
-
if (
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8182
|
+
if (state.review.binFilter) {
|
|
8183
|
+
if (summary) summary.textContent = `No analyzed clips in bin "${state.review.binFilter}".`;
|
|
8184
|
+
if (grid) grid.innerHTML = '';
|
|
8185
|
+
} else {
|
|
8186
|
+
if (summary) summary.textContent = 'Nothing analyzed yet.';
|
|
8187
|
+
if (grid) grid.innerHTML = chatPromptCard(
|
|
8188
|
+
'Review fills in as clips are analyzed. Ask your assistant to analyze this project\u2019s media:',
|
|
8189
|
+
'Analyze the source clips in my current Resolve project and build the search index.'
|
|
8190
|
+
);
|
|
8191
|
+
}
|
|
8104
8192
|
return;
|
|
8105
8193
|
}
|
|
8106
8194
|
const selected = state.review.selectedBinClipIds;
|
|
@@ -8111,7 +8199,7 @@ HTML = r"""<!doctype html>
|
|
|
8111
8199
|
const total = data.clips.length;
|
|
8112
8200
|
const base = state.review.binFilter
|
|
8113
8201
|
? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'} · bin: ${state.review.binFilter}`
|
|
8114
|
-
: `${total} analyzed clip${total === 1 ? '' : 's'} in
|
|
8202
|
+
: `${total} analyzed clip${total === 1 ? '' : 's'} in this project\u2019s analysis root.`;
|
|
8115
8203
|
if (selected.size > 0) {
|
|
8116
8204
|
summary.innerHTML = `<span>${escapeHtml(base)}</span>
|
|
8117
8205
|
<span class="bin-selection-toolbar">
|
|
@@ -9215,7 +9303,7 @@ HTML = r"""<!doctype html>
|
|
|
9215
9303
|
const safe = value == null ? '' : String(value);
|
|
9216
9304
|
return `<div class="review-notes-row" data-notes-scope="${scope}">
|
|
9217
9305
|
<div class="label">Notes</div>
|
|
9218
|
-
<textarea data-notes-input="${scope}" placeholder="Editorial notes — saved
|
|
9306
|
+
<textarea data-notes-input="${scope}" placeholder="Editorial notes — saved with this clip\u2019s analysis and preserved on re-analysis\u2026">${escapeHtml(safe)}</textarea>
|
|
9219
9307
|
<div class="controls">
|
|
9220
9308
|
<button class="secondary" data-notes-save="${scope}" type="button">Save notes</button>
|
|
9221
9309
|
<span class="saved" data-notes-saved="${scope}" style="opacity:0">Saved</span>
|
|
@@ -9402,13 +9490,21 @@ HTML = r"""<!doctype html>
|
|
|
9402
9490
|
extrasHtml += `<div class="group"><div class="group-title">QC flags</div><div class="chip-row" style="display:flex;flex-wrap:wrap;gap:4px">${qcFlags.map(f => `<span class="review-chip">${escapeHtml(String(f))}</span>`).join('')}</div></div>`;
|
|
9403
9491
|
}
|
|
9404
9492
|
if (Array.isArray(shot.frame_indices_used) && shot.frame_indices_used.length) {
|
|
9405
|
-
|
|
9493
|
+
const n = shot.frame_indices_used.length;
|
|
9494
|
+
extrasHtml += `<div class="group"><div class="group-title">Sampled frames</div><div class="value" style="font-size:12px;color:var(--text-secondary)" title="frame indices ${escapeHtml(shot.frame_indices_used.join(', '))}">${n} frame${n === 1 ? '' : 's'} sampled from this shot (shown below)</div></div>`;
|
|
9406
9495
|
}
|
|
9407
9496
|
const emptyHint = !hasAnyGroup && !editing
|
|
9408
9497
|
? `<div class="empty" style="padding:var(--space-3);border:1px dashed var(--border-default);border-radius:var(--radius-md);color:var(--text-tertiary);font-size:12px">Shot-level analysis fields aren't populated in the report. The clip-level analysis blocks (above on the clip detail page) cover this clip. Re-run analysis with a fuller vision pass to fill in per-shot Visual / Content / Editorial fields.</div>`
|
|
9409
9498
|
: '';
|
|
9410
9499
|
$('reviewShotFields').innerHTML = groupsHtml + extrasHtml + emptyHint;
|
|
9411
9500
|
const frames = data.frames || [];
|
|
9501
|
+
const FRAME_REASON_LABELS = {
|
|
9502
|
+
shot_start: 'Shot start', shot_end: 'Shot end', shot_progress: 'Mid-shot',
|
|
9503
|
+
shot_representative: 'Key frame', cut_after: 'After cut', cut_before: 'Before cut',
|
|
9504
|
+
flash_candidate: 'Flash frame', motion_peak: 'Motion peak', interval: 'Interval sample',
|
|
9505
|
+
scene_change: 'Scene change', first_usable: 'First usable', last_usable: 'Last usable',
|
|
9506
|
+
midpoint: 'Midpoint',
|
|
9507
|
+
};
|
|
9412
9508
|
$('reviewShotFrames').innerHTML = frames.map(f => {
|
|
9413
9509
|
const peak = f.motion_peak ? 'peak' : '';
|
|
9414
9510
|
const src = clipId ? `/api/clips/${encodeURIComponent(clipId)}/frames/${f.frame_index}` : '';
|
|
@@ -9416,7 +9512,7 @@ HTML = r"""<!doctype html>
|
|
|
9416
9512
|
${src ? `<img class="review-thumb" loading="lazy" src="${src}" alt="frame ${f.frame_index}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'review-thumb placeholder',textContent:'frame missing'}))">` : ''}
|
|
9417
9513
|
<div class="label">
|
|
9418
9514
|
<span>${f.time_seconds != null ? `${Number(f.time_seconds).toFixed(2)}s` : `#${f.frame_index}`}</span>
|
|
9419
|
-
<span>${escapeHtml(f.selection_reason || '')}</span>
|
|
9515
|
+
<span title="${escapeHtml(f.selection_reason || '')}">${escapeHtml(FRAME_REASON_LABELS[f.selection_reason] || f.selection_reason || '')}</span>
|
|
9420
9516
|
</div>
|
|
9421
9517
|
</div>`;
|
|
9422
9518
|
}).join('');
|
|
@@ -9562,7 +9658,11 @@ HTML = r"""<!doctype html>
|
|
|
9562
9658
|
|
|
9563
9659
|
function ensureReviewPanelStateTimer() {
|
|
9564
9660
|
if (state.review.panelStateTimer) return;
|
|
9661
|
+
state.review.panelStateTick = 0;
|
|
9565
9662
|
state.review.panelStateTimer = window.setInterval(() => {
|
|
9663
|
+
if (document.hidden) return;
|
|
9664
|
+
state.review.panelStateTick = (state.review.panelStateTick + 1) % 5;
|
|
9665
|
+
if (!document.hasFocus() && state.review.panelStateTick !== 0) return;
|
|
9566
9666
|
if (state.activePanel === 'analysis' && state.activeSubpages.analysis === 'review') {
|
|
9567
9667
|
pollPanelStateOnce().catch(() => {});
|
|
9568
9668
|
}
|
|
@@ -9658,9 +9758,11 @@ HTML = r"""<!doctype html>
|
|
|
9658
9758
|
const active = new Set((activeValues || []).map(value => String(value || '').trim()).filter(Boolean));
|
|
9659
9759
|
const values = uniqueValues(candidates || [], Array.from(active));
|
|
9660
9760
|
hidden.value = Array.from(active).join(', ');
|
|
9661
|
-
container.innerHTML = values.map(value =>
|
|
9662
|
-
|
|
9663
|
-
|
|
9761
|
+
container.innerHTML = values.map(value => {
|
|
9762
|
+
const label = String(value).replace(/_/g, ' ');
|
|
9763
|
+
return `
|
|
9764
|
+
<button type="button" class="token-pill${active.has(value) ? ' active' : ''}" data-token-value="${escapeHtml(value)}" aria-pressed="${active.has(value) ? 'true' : 'false'}" title="${escapeHtml(value)}">${escapeHtml(label)}</button>
|
|
9765
|
+
`; }).join('');
|
|
9664
9766
|
container.querySelectorAll('[data-token-value]').forEach(button => {
|
|
9665
9767
|
button.addEventListener('click', () => {
|
|
9666
9768
|
button.classList.toggle('active');
|
|
@@ -9964,14 +10066,14 @@ HTML = r"""<!doctype html>
|
|
|
9964
10066
|
if (options.updateHash !== false) {
|
|
9965
10067
|
updateRouteHash('docs', state.activeDoc);
|
|
9966
10068
|
}
|
|
9967
|
-
const rendered = renderMarkdown(payload.content || '');
|
|
10069
|
+
const rendered = renderMarkdown(payload.content || '', payload.path || '');
|
|
9968
10070
|
setHtml('docReader', rendered.html);
|
|
9969
10071
|
renderDocSectionNav(rendered.sections);
|
|
9970
10072
|
setText('docSource', payload.path || '');
|
|
9971
10073
|
applyDocFilters();
|
|
9972
10074
|
}
|
|
9973
10075
|
|
|
9974
|
-
function renderMarkdown(markdown) {
|
|
10076
|
+
function renderMarkdown(markdown, docPath) {
|
|
9975
10077
|
const lines = String(markdown || '').split(/\r?\n/);
|
|
9976
10078
|
const parts = [];
|
|
9977
10079
|
const sections = [];
|
|
@@ -10000,7 +10102,9 @@ HTML = r"""<!doctype html>
|
|
|
10000
10102
|
const line = rawLine.trimEnd();
|
|
10001
10103
|
if (!line.trim()) continue;
|
|
10002
10104
|
const badgeMatches = parseBadgeTokens(line.trim());
|
|
10003
|
-
|
|
10105
|
+
// Badge rows are external status shields; a linked LOCAL image (e.g.
|
|
10106
|
+
// the README's control-panel screenshot) must reach the image handlers.
|
|
10107
|
+
if (badgeMatches.length && badgeMatches.every(token => /^https?:/i.test(token.src))) {
|
|
10004
10108
|
while (index + 1 < lines.length) {
|
|
10005
10109
|
const nextMatches = parseBadgeTokens(lines[index + 1].trim());
|
|
10006
10110
|
if (!nextMatches.length) break;
|
|
@@ -10029,9 +10133,14 @@ HTML = r"""<!doctype html>
|
|
|
10029
10133
|
parts.push(`<h${level} id="${id}" class="doc-block" data-md-type="heading">${inlineMarkdown(heading[2])}</h${level}>`);
|
|
10030
10134
|
continue;
|
|
10031
10135
|
}
|
|
10136
|
+
const linkedImage = line.match(/^\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)$/);
|
|
10137
|
+
if (linkedImage) {
|
|
10138
|
+
parts.push(renderImageBlock(linkedImage[1], linkedImage[2], docPath));
|
|
10139
|
+
continue;
|
|
10140
|
+
}
|
|
10032
10141
|
const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
10033
10142
|
if (image) {
|
|
10034
|
-
parts.push(renderImageBlock(image[1], image[2]));
|
|
10143
|
+
parts.push(renderImageBlock(image[1], image[2], docPath));
|
|
10035
10144
|
continue;
|
|
10036
10145
|
}
|
|
10037
10146
|
const bullet = line.match(/^[-*]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/);
|
|
@@ -10099,8 +10208,27 @@ HTML = r"""<!doctype html>
|
|
|
10099
10208
|
return tokens;
|
|
10100
10209
|
}
|
|
10101
10210
|
|
|
10102
|
-
function
|
|
10103
|
-
|
|
10211
|
+
function resolveDocAsset(src, docPath) {
|
|
10212
|
+
const clean = String(src || '').trim();
|
|
10213
|
+
if (/^(https?:|data:|\/)/i.test(clean)) return clean;
|
|
10214
|
+
const stack = [];
|
|
10215
|
+
const parts = String(docPath || '').split('/').slice(0, -1).concat(clean.split('/'));
|
|
10216
|
+
for (const part of parts) {
|
|
10217
|
+
if (!part || part === '.') continue;
|
|
10218
|
+
if (part === '..') { stack.pop(); continue; }
|
|
10219
|
+
stack.push(part);
|
|
10220
|
+
}
|
|
10221
|
+
const rel = stack.join('/');
|
|
10222
|
+
const marker = 'docs/images/';
|
|
10223
|
+
if (rel.startsWith(marker)) {
|
|
10224
|
+
return '/api/doc_asset/' + rel.slice(marker.length).split('/').map(encodeURIComponent).join('/');
|
|
10225
|
+
}
|
|
10226
|
+
return clean;
|
|
10227
|
+
}
|
|
10228
|
+
|
|
10229
|
+
function renderImageBlock(alt, src, docPath) {
|
|
10230
|
+
const resolved = resolveDocAsset(src, docPath);
|
|
10231
|
+
return `<figure class="doc-image doc-block" data-md-type="image"><img src="${escapeAttribute(resolved)}" alt="${escapeAttribute(alt || src)}" loading="lazy"></figure>`;
|
|
10104
10232
|
}
|
|
10105
10233
|
|
|
10106
10234
|
function renderMarkdownTable(lines) {
|
|
@@ -10280,6 +10408,13 @@ HTML = r"""<!doctype html>
|
|
|
10280
10408
|
closeNavDropdowns();
|
|
10281
10409
|
closeActionMenus();
|
|
10282
10410
|
setPanel(control.dataset.panelTarget, { subpage: control.dataset.subpageTarget });
|
|
10411
|
+
if (control.dataset.reviewView === 'history') {
|
|
10412
|
+
reviewSetView('history');
|
|
10413
|
+
refreshHistoryTimelines().catch(alertError);
|
|
10414
|
+
if (window.location.hash !== '#analysis/review/history') {
|
|
10415
|
+
window.history.replaceState(null, '', '#analysis/review/history');
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10283
10418
|
});
|
|
10284
10419
|
});
|
|
10285
10420
|
document.querySelectorAll('[data-doc]').forEach(control => {
|
|
@@ -10456,7 +10591,7 @@ HTML = r"""<!doctype html>
|
|
|
10456
10591
|
refreshRestartBanner().catch(() => {});
|
|
10457
10592
|
// Poll for restart marker every 30s
|
|
10458
10593
|
if (state.updates.restartTimer) clearInterval(state.updates.restartTimer);
|
|
10459
|
-
state.updates.restartTimer = setInterval(() => refreshRestartBanner().catch(() => {}), 30000);
|
|
10594
|
+
state.updates.restartTimer = setInterval(() => { if (!document.hidden) refreshRestartBanner().catch(() => {}); }, 30000);
|
|
10460
10595
|
|
|
10461
10596
|
// Auto-save preference (server-side preference; reads via /api/setup/defaults)
|
|
10462
10597
|
$('prefAutoSaveAfterArchive')?.addEventListener('change', async e => {
|
|
@@ -13972,9 +14107,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
13972
14107
|
raw = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
|
13973
14108
|
etag = '"' + hashlib.md5(raw).hexdigest() + '"'
|
|
13974
14109
|
if self.headers.get("If-None-Match") == etag:
|
|
13975
|
-
|
|
14110
|
+
tiny = json.dumps({"success": True, "unchanged": True, "etag": etag}).encode("utf-8")
|
|
14111
|
+
self.send_response(200)
|
|
14112
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
14113
|
+
self.send_header("Content-Length", str(len(tiny)))
|
|
13976
14114
|
self.send_header("ETag", etag)
|
|
13977
14115
|
self.end_headers()
|
|
14116
|
+
self.wfile.write(tiny)
|
|
13978
14117
|
return
|
|
13979
14118
|
self.send_response(200)
|
|
13980
14119
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
@@ -14105,6 +14244,21 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
14105
14244
|
payload = _dashboard_doc(doc)
|
|
14106
14245
|
self._json(payload, 200 if payload.get("success") else 404)
|
|
14107
14246
|
return
|
|
14247
|
+
if path.startswith("/api/doc_asset/"):
|
|
14248
|
+
rel = unquote(path[len("/api/doc_asset/"):])
|
|
14249
|
+
base = os.path.realpath(os.path.join(_repo_root(), "docs", "images"))
|
|
14250
|
+
full = os.path.realpath(os.path.join(base, rel))
|
|
14251
|
+
if not (full.startswith(base + os.sep) or full == base):
|
|
14252
|
+
self._json({"success": False, "error": "path escape"}, HTTPStatus.FORBIDDEN)
|
|
14253
|
+
return
|
|
14254
|
+
content_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
14255
|
+
".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp"}
|
|
14256
|
+
ext = os.path.splitext(full)[1].lower()
|
|
14257
|
+
if ext not in content_types or not os.path.isfile(full):
|
|
14258
|
+
self._json({"success": False, "error": "not found"}, HTTPStatus.NOT_FOUND)
|
|
14259
|
+
return
|
|
14260
|
+
self._serve_file(full, content_types[ext])
|
|
14261
|
+
return
|
|
14108
14262
|
if path == "/api/setup/schema":
|
|
14109
14263
|
self._json(_setup_defaults("schema"))
|
|
14110
14264
|
return
|
|
@@ -14678,6 +14832,8 @@ def _warm_inventory_cache(project_root: str) -> None:
|
|
|
14678
14832
|
|
|
14679
14833
|
|
|
14680
14834
|
def main() -> None:
|
|
14835
|
+
from src.utils import actor_identity
|
|
14836
|
+
actor_identity.set_instance("control-panel")
|
|
14681
14837
|
args = parse_args()
|
|
14682
14838
|
state = DashboardState(args.project_name, args.project_id, args.analysis_root)
|
|
14683
14839
|
Handler.state = state
|
package/src/batch_cli.py
CHANGED
|
@@ -526,6 +526,8 @@ _HANDLERS = {
|
|
|
526
526
|
|
|
527
527
|
|
|
528
528
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
529
|
+
from src.utils import actor_identity
|
|
530
|
+
actor_identity.set_instance("batch-cli")
|
|
529
531
|
parser = _build_parser()
|
|
530
532
|
args = parser.parse_args(argv)
|
|
531
533
|
if not hasattr(args, "json"):
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.40.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|