davinci-resolve-mcp 2.30.0 → 2.31.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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.31.0
6
+
7
+ Adds the **AI Console** to the control panel — an interactive surface for the
8
+ Resolve 21 local AI operations (Phase 2 of the staged AI-ops build: ledger →
9
+ console → governance).
10
+
11
+ A new **AI Console** tab runs the 21.0 ops against the current Media Pool folder
12
+ or a specific clip:
13
+
14
+ - **Capability matrix** — shows which AI methods this Resolve build exposes (green
15
+ = available, grey = absent on older builds) and which Extra each gated method
16
+ needs to actually run.
17
+ - **Analysis** — Classify audio / Clear classification, IntelliSearch (with
18
+ identify-faces and Better-mode toggles), Analyze for slate (16-color marker
19
+ picker), Transcribe (with speaker-detection toggle), Clear transcription.
20
+ - **Motion deblur** and **Speech generator** — full options forms; because both
21
+ create new media files they route through a confirmation modal (the same
22
+ confirm-token gate the MCP tools use) before running.
23
+ - **Session** — Disable background tasks for the current Resolve session.
24
+ - A live result readout, and the *Resolve 21 AI ops* ledger refreshes after each
25
+ run so file/byte totals stay current.
26
+
27
+ Backend: a loopback-only `POST /api/resolve_ai/run` endpoint dispatches each op
28
+ to the consolidated `folder` / `media_pool_item` / `project_settings` /
29
+ `resolve_control` tools, relaying the confirm-token two-step. No new MCP tools or
30
+ Resolve API surface — the console reuses the existing v2.29.0 actions. Validated
31
+ live end-to-end against Resolve Studio 21.0.0.47.
32
+
5
33
  ## What's New in v2.30.0
6
34
 
7
35
  Adds the **Resolve 21 AI-ops ledger** — usage/time/file accounting for the
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.30.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.31.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-32%20(341%20full)-blue.svg)](#server-modes)
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.30.0"
38
+ VERSION = "2.31.0"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.30.0",
3
+ "version": "2.31.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -2378,6 +2378,51 @@ HTML = r"""<!doctype html>
2378
2378
  font-size: var(--text-xs);
2379
2379
  color: var(--text-muted);
2380
2380
  }
2381
+ .ai-op-row {
2382
+ display: flex;
2383
+ flex-wrap: wrap;
2384
+ align-items: center;
2385
+ gap: var(--space-3);
2386
+ grid-column: 1 / -1;
2387
+ margin: var(--space-1) 0;
2388
+ }
2389
+ .ai-op-btn {
2390
+ padding: 6px 14px;
2391
+ border-radius: var(--radius-sm, 6px);
2392
+ border: 1px solid var(--border, rgba(255,255,255,0.18));
2393
+ background: var(--accent, #3b82f6);
2394
+ color: #fff;
2395
+ font-size: var(--text-sm, 13px);
2396
+ cursor: pointer;
2397
+ }
2398
+ .ai-op-btn:hover { filter: brightness(1.08); }
2399
+ .ai-op-btn:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
2400
+ .ai-op-btn.ghost { background: transparent; color: var(--text, inherit); }
2401
+ .ai-op-btn.danger { background: #b4452f; }
2402
+ .ai-caps-grid {
2403
+ display: grid;
2404
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
2405
+ gap: var(--space-2);
2406
+ margin-top: var(--space-2);
2407
+ }
2408
+ .ai-caps-item {
2409
+ display: flex;
2410
+ align-items: center;
2411
+ gap: 8px;
2412
+ font-size: var(--text-xs);
2413
+ }
2414
+ .ai-caps-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
2415
+ .ai-caps-dot.on { background: #34a853; }
2416
+ .ai-caps-dot.off { background: rgba(255,255,255,0.25); }
2417
+ .ai-caps-extra { opacity: 0.6; }
2418
+ .ai-console-result {
2419
+ white-space: pre-wrap;
2420
+ font-family: var(--mono, ui-monospace, monospace);
2421
+ max-height: 240px;
2422
+ overflow: auto;
2423
+ }
2424
+ .ai-console-result.ok { color: var(--text, inherit); }
2425
+ .ai-console-result.err { color: #e06c5a; }
2381
2426
  .caps-section-subtitle {
2382
2427
  font-size: var(--text-xs);
2383
2428
  color: var(--text-muted);
@@ -3777,6 +3822,7 @@ HTML = r"""<!doctype html>
3777
3822
  <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>
3778
3823
  </div>
3779
3824
  </div>
3825
+ <button class="control-tab" data-panel-target="aiconsole">AI Console</button>
3780
3826
  <div class="control-nav-item">
3781
3827
  <button class="control-tab has-menu" data-panel-target="diagnostics">Setup <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>
3782
3828
  <div class="nav-dropdown" role="menu" aria-label="Diagnostic pages">
@@ -4119,6 +4165,94 @@ HTML = r"""<!doctype html>
4119
4165
  </section>
4120
4166
  </main>
4121
4167
 
4168
+ <main id="panel-aiconsole" class="panel control-grid">
4169
+ <section class="span-12">
4170
+ <div class="section-top">
4171
+ <div>
4172
+ <h2>Resolve 21 AI Console</h2>
4173
+ <p class="section-sub">Run Resolve's local AI operations on the current Media Pool folder or a specific clip. These run on Resolve's GPU/AI engine — the analysis and slate ops are safe and reversible; <strong>motion-deblur</strong> and <strong>speech generation</strong> create new media files and ask for confirmation first. Source media is never modified. Every run is recorded in the <em>Resolve 21 AI ops</em> ledger (Preferences → Caps + Safety).</p>
4174
+ </div>
4175
+ </div>
4176
+
4177
+ <div id="aiConsoleCaps" class="caps-section" style="margin-top:12px;">
4178
+ <div class="caps-section-hint">Checking which AI methods this Resolve build exposes…</div>
4179
+ </div>
4180
+
4181
+ <div class="caps-section" style="margin-top:12px;">
4182
+ <div class="caps-section-head"><div class="caps-section-title">Target</div>
4183
+ <div class="caps-section-hint">Folder ops run on the current Media Pool folder in Resolve. Choose <em>Specific clip</em> and paste a clip id (from <code>media_pool.get_clips</code>) to target one clip.</div></div>
4184
+ <div class="settings-grid">
4185
+ <label class="checkbox-row"><input type="radio" name="aiTarget" value="folder" checked><span>Current folder</span></label>
4186
+ <label class="checkbox-row"><input type="radio" name="aiTarget" value="clip"><span>Specific clip</span></label>
4187
+ <label>Clip id <input id="aiClipId" type="text" placeholder="(clip UniqueId)"></label>
4188
+ </div>
4189
+ </div>
4190
+
4191
+ <div class="caps-section" style="margin-top:12px;">
4192
+ <div class="caps-section-head"><div class="caps-section-title">Analysis</div>
4193
+ <div class="caps-section-hint">Safe, reversible. IntelliSearch and Slate require their AI Extras (Extras Download Manager).</div></div>
4194
+ <div class="settings-grid">
4195
+ <div class="ai-op-row"><button class="ai-op-btn" data-ai-op="perform_audio_classification">Classify audio</button>
4196
+ <button class="ai-op-btn ghost" data-ai-op="clear_audio_classification">Clear classification</button></div>
4197
+ <div class="ai-op-row">
4198
+ <button class="ai-op-btn" data-ai-op="analyze_for_intellisearch">IntelliSearch</button>
4199
+ <label class="checkbox-row"><input id="aiIdentifyFaces" type="checkbox"><span>Identify faces</span></label>
4200
+ <label class="checkbox-row"><input id="aiBetterMode" type="checkbox"><span>Better mode</span></label>
4201
+ </div>
4202
+ <div class="ai-op-row">
4203
+ <button class="ai-op-btn" data-ai-op="analyze_for_slate">Analyze for slate</button>
4204
+ <label>Marker <select id="aiSlateColor"></select></label>
4205
+ </div>
4206
+ <div class="ai-op-row">
4207
+ <button class="ai-op-btn" data-ai-op="transcribe_audio">Transcribe</button>
4208
+ <label class="checkbox-row"><input id="aiSpeakerDetection" type="checkbox"><span>Speaker detection</span></label>
4209
+ <button class="ai-op-btn ghost" data-ai-op="clear_transcription">Clear transcription</button>
4210
+ </div>
4211
+ </div>
4212
+ </div>
4213
+
4214
+ <div class="caps-section" style="margin-top:12px;">
4215
+ <div class="caps-section-head"><div class="caps-section-title">Motion deblur</div>
4216
+ <div class="caps-section-hint">Renders new deblurred media. Creates files; asks for confirmation. Leave fields blank for Resolve defaults.</div></div>
4217
+ <div class="settings-grid">
4218
+ <label>Format <input id="aiDeblurFormat" type="text" placeholder="mov"></label>
4219
+ <label>Codec <input id="aiDeblurCodec" type="text" placeholder="ProRes422"></label>
4220
+ <label class="checkbox-row"><input id="aiDeblurExtreme" type="checkbox"><span>Extreme mode</span></label>
4221
+ <label class="checkbox-row"><input id="aiDeblurMarkInOut" type="checkbox"><span>Use mark in/out</span></label>
4222
+ <label class="checkbox-row"><input id="aiDeblurSourceRes" type="checkbox"><span>Render at source res</span></label>
4223
+ <div class="ai-op-row"><button class="ai-op-btn danger" data-ai-op="remove_motion_blur">Remove motion blur</button></div>
4224
+ </div>
4225
+ </div>
4226
+
4227
+ <div class="caps-section" style="margin-top:12px;">
4228
+ <div class="caps-section-head"><div class="caps-section-title">Speech generator</div>
4229
+ <div class="caps-section-hint">AI text-to-speech. Requires the AI Speech Generator Extra. Creates a new audio item; asks for confirmation.</div></div>
4230
+ <div class="settings-grid">
4231
+ <label style="grid-column:1/-1;">Text <textarea id="aiSpeechText" rows="2" placeholder="Text to synthesize"></textarea></label>
4232
+ <label>Voice model <input id="aiSpeechVoice" type="text" placeholder="Female 1"></label>
4233
+ <label>Speed <input id="aiSpeechSpeed" type="number" placeholder="(default)"></label>
4234
+ <label>Pitch <input id="aiSpeechPitch" type="number" placeholder="(default)"></label>
4235
+ <label>Variation <input id="aiSpeechVariation" type="number" placeholder="(default)"></label>
4236
+ <label class="checkbox-row"><input id="aiSpeechAddTimeline" type="checkbox"><span>Add to timeline</span></label>
4237
+ <label>Timecode <input id="aiSpeechTimecode" type="text" placeholder="01:00:00:00"></label>
4238
+ <label>Audio track <input id="aiSpeechTrack" type="number" placeholder="(default)"></label>
4239
+ <div class="ai-op-row"><button class="ai-op-btn danger" data-ai-op="generate_speech">Generate speech</button></div>
4240
+ </div>
4241
+ </div>
4242
+
4243
+ <div class="caps-section" style="margin-top:12px;">
4244
+ <div class="caps-section-head"><div class="caps-section-title">Session</div>
4245
+ <div class="caps-section-hint">Quiet Resolve's background tasks for this session before heavy work. Resets on restart.</div></div>
4246
+ <div class="ai-op-row"><button class="ai-op-btn ghost" data-ai-op="disable_background_tasks">Disable background tasks</button></div>
4247
+ </div>
4248
+
4249
+ <div class="caps-section" style="margin-top:12px;">
4250
+ <div class="caps-section-head"><div class="caps-section-title">Result</div></div>
4251
+ <div id="aiConsoleResult" class="ai-console-result caps-section-hint">No op run yet.</div>
4252
+ </div>
4253
+ </section>
4254
+ </main>
4255
+
4122
4256
  <main id="panel-diagnostics" class="panel control-grid">
4123
4257
  <section class="span-12 subpage active" data-subpage-scope="diagnostics" data-subpage="resolve">
4124
4258
  <h2>Resolve</h2>
@@ -4712,6 +4846,7 @@ HTML = r"""<!doctype html>
4712
4846
  const PANEL_LABELS = {
4713
4847
  overview: 'Overview',
4714
4848
  analysis: 'Analysis',
4849
+ aiconsole: 'AI Console',
4715
4850
  diagnostics: 'Setup',
4716
4851
  projects: 'Projects',
4717
4852
  docs: 'Docs',
@@ -4720,6 +4855,7 @@ HTML = r"""<!doctype html>
4720
4855
  const PANEL_ICONS = {
4721
4856
  overview: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>',
4722
4857
  analysis: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>',
4858
+ aiconsole: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"></path><rect x="4" y="12" width="16" height="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>',
4723
4859
  diagnostics: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l2.1-2.1a6 6 0 0 1-7.6 7.6l-4 4a2.1 2.1 0 0 1-3-3l4-4a6 6 0 0 1 7.6-7.6z"></path></svg>',
4724
4860
  projects: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h5l2 3h11v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><path d="M7 7V5a2 2 0 0 1 2-2h3l2 2h3a2 2 0 0 1 2 2v3"></path></svg>',
4725
4861
  docs: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5z"></path></svg>',
@@ -4882,6 +5018,9 @@ HTML = r"""<!doctype html>
4882
5018
  syncPreferencesPanel();
4883
5019
  refreshSetupDefaults().catch(alertError);
4884
5020
  }
5021
+ if (next === 'aiconsole') {
5022
+ initAiConsole();
5023
+ }
4885
5024
  if (next === 'docs' && subpage) {
4886
5025
  loadDoc(subpage, { updateHash: false }).catch(alertError);
4887
5026
  }
@@ -7111,6 +7250,164 @@ HTML = r"""<!doctype html>
7111
7250
  tableEl.style.display = '';
7112
7251
  }
7113
7252
 
7253
+ // ─── Resolve 21 AI Console ──────────────────────────────────────
7254
+ const AI_MARKER_COLORS = ['Blue','Cyan','Green','Yellow','Red','Pink','Purple',
7255
+ 'Fuchsia','Rose','Lavender','Sky','Mint','Lemon','Sand','Cocoa','Cream'];
7256
+ const AI_OP_LABELS = {
7257
+ perform_audio_classification: 'Classify audio',
7258
+ clear_audio_classification: 'Clear classification',
7259
+ analyze_for_intellisearch: 'IntelliSearch',
7260
+ analyze_for_slate: 'Analyze for slate',
7261
+ transcribe_audio: 'Transcribe',
7262
+ clear_transcription: 'Clear transcription',
7263
+ remove_motion_blur: 'Remove motion blur',
7264
+ generate_speech: 'Generate speech',
7265
+ disable_background_tasks: 'Disable background tasks',
7266
+ };
7267
+ // Which features gate which buttons (key in resolve.ai_features.features).
7268
+ const AI_OP_FEATURE = {
7269
+ perform_audio_classification: 'perform_audio_classification',
7270
+ clear_audio_classification: 'clear_audio_classification',
7271
+ analyze_for_intellisearch: 'analyze_for_intellisearch',
7272
+ analyze_for_slate: 'analyze_for_slate',
7273
+ remove_motion_blur: 'remove_motion_blur',
7274
+ generate_speech: 'generate_speech',
7275
+ disable_background_tasks: 'disable_background_tasks',
7276
+ };
7277
+ let _aiConsoleInit = false;
7278
+
7279
+ function renderAiConsole() {
7280
+ const feats = (state.boot?.resolve?.ai_features) || {};
7281
+ const features = feats.features || {};
7282
+ const requiresExtra = feats.requires_extra || {};
7283
+ const capsEl = $('aiConsoleCaps');
7284
+ if (capsEl) {
7285
+ if (state.boot?.resolve?.available !== true) {
7286
+ capsEl.innerHTML = '<div class="caps-section-hint">Resolve is not connected. Open a project in DaVinci Resolve, then reload.</div>';
7287
+ } else {
7288
+ const items = Object.keys(AI_OP_LABELS)
7289
+ .filter(op => op in AI_OP_FEATURE)
7290
+ .map(op => {
7291
+ const key = AI_OP_FEATURE[op];
7292
+ const on = !!features[key];
7293
+ const extra = requiresExtra[key];
7294
+ return `<div class="ai-caps-item"><span class="ai-caps-dot ${on ? 'on' : 'off'}"></span>`
7295
+ + `<span>${escapeHtml(AI_OP_LABELS[op])}</span>`
7296
+ + (extra ? `<span class="ai-caps-extra">· needs ${escapeHtml(extra)}</span>` : '')
7297
+ + `</div>`;
7298
+ }).join('');
7299
+ capsEl.innerHTML = `<div class="caps-section-head"><div class="caps-section-title">Available on this Resolve build</div>`
7300
+ + `<div class="caps-section-hint">A grey dot means the method is absent (older Resolve). "needs …" means the method is present but requires that Extra to actually run — install via Extras Download Manager.</div></div>`
7301
+ + `<div class="ai-caps-grid">${items}</div>`;
7302
+ }
7303
+ }
7304
+ // Slate color dropdown (once).
7305
+ const sel = $('aiSlateColor');
7306
+ if (sel && !sel.options.length) {
7307
+ sel.innerHTML = AI_MARKER_COLORS.map(c => `<option value="${c}">${c}</option>`).join('');
7308
+ }
7309
+ }
7310
+
7311
+ function aiTarget() {
7312
+ const checked = document.querySelector('input[name="aiTarget"]:checked');
7313
+ return checked ? checked.value : 'folder';
7314
+ }
7315
+
7316
+ function aiBuildParams(op) {
7317
+ const params = {};
7318
+ if (op === 'analyze_for_intellisearch') {
7319
+ params.identify_faces = !!$('aiIdentifyFaces')?.checked;
7320
+ params.is_better_mode = !!$('aiBetterMode')?.checked;
7321
+ } else if (op === 'analyze_for_slate') {
7322
+ params.marker_color = $('aiSlateColor')?.value || 'Blue';
7323
+ } else if (op === 'transcribe_audio') {
7324
+ if ($('aiSpeakerDetection')?.checked) params.use_speaker_detection = true;
7325
+ } else if (op === 'remove_motion_blur') {
7326
+ const d = {};
7327
+ const fmt = ($('aiDeblurFormat')?.value || '').trim(); if (fmt) d.Format = fmt;
7328
+ const codec = ($('aiDeblurCodec')?.value || '').trim(); if (codec) d.Codec = codec;
7329
+ if ($('aiDeblurExtreme')?.checked) d.UseExtremeMode = true;
7330
+ if ($('aiDeblurMarkInOut')?.checked) d.UseMarkInMarkOut = true;
7331
+ if ($('aiDeblurSourceRes')?.checked) d.RenderAtSourceRes = true;
7332
+ params.deblur_option = d;
7333
+ } else if (op === 'generate_speech') {
7334
+ const text = ($('aiSpeechText')?.value || '').trim();
7335
+ const settings = { TextInput: text };
7336
+ const voice = ($('aiSpeechVoice')?.value || '').trim(); if (voice) settings.VoiceModel = voice;
7337
+ const num = (id) => { const v = ($(id)?.value || '').trim(); return v === '' ? null : Number(v); };
7338
+ const speed = num('aiSpeechSpeed'); if (speed != null) settings.Speed = speed;
7339
+ const pitch = num('aiSpeechPitch'); if (pitch != null) settings.Pitch = pitch;
7340
+ const variation = num('aiSpeechVariation'); if (variation != null) settings.Variation = variation;
7341
+ if ($('aiSpeechAddTimeline')?.checked) {
7342
+ settings.AddToTimeline = true;
7343
+ const track = num('aiSpeechTrack'); if (track != null) settings.AudioTrack = track;
7344
+ }
7345
+ params.speech_generation_settings = settings;
7346
+ const tc = ($('aiSpeechTimecode')?.value || '').trim(); if (tc) params.timecode = tc;
7347
+ }
7348
+ return params;
7349
+ }
7350
+
7351
+ function aiShowResult(op, payload, isErr) {
7352
+ const el = $('aiConsoleResult');
7353
+ if (!el) return;
7354
+ el.classList.toggle('err', !!isErr);
7355
+ el.classList.toggle('ok', !isErr);
7356
+ const head = `${AI_OP_LABELS[op] || op} — ${new Date().toLocaleTimeString()}\n`;
7357
+ el.textContent = head + JSON.stringify(payload, null, 2);
7358
+ }
7359
+
7360
+ async function runAiOp(op) {
7361
+ const target = (op === 'generate_speech' || op === 'disable_background_tasks') ? 'folder' : aiTarget();
7362
+ const params = aiBuildParams(op);
7363
+ if (target === 'clip') {
7364
+ const clipId = ($('aiClipId')?.value || '').trim();
7365
+ if (!clipId) { aiShowResult(op, { error: 'Enter a clip id, or switch target to Current folder.' }, true); return; }
7366
+ params.clip_id = clipId;
7367
+ }
7368
+ if (op === 'generate_speech' && !params.speech_generation_settings?.TextInput) {
7369
+ aiShowResult(op, { error: 'Enter text to synthesize.' }, true); return;
7370
+ }
7371
+ const buttons = document.querySelectorAll('.ai-op-btn');
7372
+ buttons.forEach(b => { b.disabled = true; });
7373
+ try {
7374
+ let res = await api('/api/resolve_ai/run', {
7375
+ method: 'POST', body: JSON.stringify({ op, target, params }),
7376
+ }).catch(err => ({ success: false, error: String(err && err.message || err) }));
7377
+ // Confirm-token two-step for the media-creating ops.
7378
+ if (res && res.status === 'confirmation_required') {
7379
+ const preview = res.preview || {};
7380
+ const proceed = await brandedConfirm({
7381
+ kicker: 'Creates new media',
7382
+ title: AI_OP_LABELS[op] || op,
7383
+ body: preview.warning || 'This operation creates new media. Proceed?',
7384
+ detail: JSON.stringify(preview, null, 2),
7385
+ confirmLabel: 'Run it',
7386
+ tone: 'danger',
7387
+ });
7388
+ if (!proceed) { aiShowResult(op, { cancelled: true }, false); return; }
7389
+ const params2 = { ...params, confirm_token: res.confirm_token };
7390
+ res = await api('/api/resolve_ai/run', {
7391
+ method: 'POST', body: JSON.stringify({ op, target, params: params2 }),
7392
+ }).catch(err => ({ success: false, error: String(err && err.message || err) }));
7393
+ }
7394
+ aiShowResult(op, res, !(res && res.success));
7395
+ // Refresh the ledger widget so creators' file/byte totals update.
7396
+ refreshResolveAiOps().catch(() => {});
7397
+ } finally {
7398
+ buttons.forEach(b => { b.disabled = false; });
7399
+ }
7400
+ }
7401
+
7402
+ function initAiConsole() {
7403
+ if (_aiConsoleInit) { renderAiConsole(); return; }
7404
+ _aiConsoleInit = true;
7405
+ renderAiConsole();
7406
+ document.querySelectorAll('#panel-aiconsole .ai-op-btn').forEach(btn => {
7407
+ btn.addEventListener('click', () => runAiOp(btn.dataset.aiOp));
7408
+ });
7409
+ }
7410
+
7114
7411
  // ─── Caps inspector + refusals + reset ──────────────────────────
7115
7412
  async function inspectCapsFromUI() {
7116
7413
  const clipId = ($('capsInspectClipId')?.value || '').trim();
@@ -10783,6 +11080,58 @@ def _resolve_identity() -> Dict[str, Any]:
10783
11080
  }
10784
11081
 
10785
11082
 
11083
+ # ── Resolve 21 AI Console: op dispatch ──────────────────────────────────────
11084
+ # Folder/clip-level ops are routed to the consolidated `folder` /
11085
+ # `media_pool_item` tools; project/resolve-level ops to their tools. The
11086
+ # consolidated tools own the confirm-token gate for the two media-creators, so
11087
+ # this dispatcher just relays params (incl. confirm_token) and the result.
11088
+
11089
+ _AI_CONSOLE_FOLDER_OPS = frozenset({
11090
+ "perform_audio_classification", "clear_audio_classification",
11091
+ "analyze_for_intellisearch", "analyze_for_slate", "remove_motion_blur",
11092
+ "transcribe_audio", "clear_transcription",
11093
+ })
11094
+
11095
+
11096
+ def _run_resolve_ai_op(body: Dict[str, Any]) -> Dict[str, Any]:
11097
+ """Dispatch one AI Console op to the right consolidated server tool.
11098
+
11099
+ body = {op, target?, params?}. target is 'folder' (current Media Pool
11100
+ folder, default) or 'clip' (params.clip_id required). Returns the tool's
11101
+ response verbatim — including a {status:'confirmation_required', confirm_token,
11102
+ preview} shape for the gated media-creating ops.
11103
+ """
11104
+ op = (body.get("op") or "").strip()
11105
+ target = (body.get("target") or "folder").strip()
11106
+ params = dict(body.get("params") or {})
11107
+ if not op:
11108
+ return {"success": False, "error": "op is required"}
11109
+ try:
11110
+ from src.server import (
11111
+ folder as _folder_tool,
11112
+ media_pool_item as _mpi_tool,
11113
+ project_settings as _ps_tool,
11114
+ resolve_control as _rc_tool,
11115
+ )
11116
+ except Exception as exc: # pragma: no cover - import guard
11117
+ return {"success": False, "error": f"server tools unavailable: {exc}"}
11118
+
11119
+ if op == "disable_background_tasks":
11120
+ return _rc_tool("disable_background_tasks_for_current_session", {})
11121
+ if op == "generate_speech":
11122
+ return _ps_tool("generate_speech", params)
11123
+ if op not in _AI_CONSOLE_FOLDER_OPS:
11124
+ return {"success": False, "error": f"unknown op {op!r}"}
11125
+ if target == "clip":
11126
+ clip_id = params.get("clip_id") or body.get("clip_id")
11127
+ if not clip_id:
11128
+ return {"success": False, "error": "clip target requires a clip_id"}
11129
+ params["clip_id"] = clip_id
11130
+ return _mpi_tool(op, params)
11131
+ # default: operate on the current Media Pool folder
11132
+ return _folder_tool(op, params)
11133
+
11134
+
10786
11135
  def _clip_props(clip: Any) -> Dict[str, Any]:
10787
11136
  props, _ = _safe_call(clip, "GetClipProperty", "")
10788
11137
  return props if isinstance(props, dict) else {}
@@ -13953,6 +14302,21 @@ class Handler(BaseHTTPRequestHandler):
13953
14302
  except Exception as exc:
13954
14303
  self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
13955
14304
  return
14305
+ if path == "/api/resolve_ai/run":
14306
+ # Run a Resolve 21 AI op from the panel. Loopback-only because it
14307
+ # mutates Resolve (and the media-creators write new files). The
14308
+ # confirm-token two-step is handled by the consolidated tool; the
14309
+ # 'confirmation_required' shape is relayed to the panel as 200.
14310
+ if not _request_is_loopback(self):
14311
+ self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
14312
+ return
14313
+ try:
14314
+ result = _run_resolve_ai_op(body)
14315
+ ok = bool(result.get("success")) or result.get("status") == "confirmation_required"
14316
+ self._json(result, 200 if ok else 400)
14317
+ except Exception as exc:
14318
+ self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
14319
+ return
13956
14320
  if path == "/api/caps/reset_day":
13957
14321
  if not _request_is_loopback(self):
13958
14322
  self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.30.0"
83
+ VERSION = "2.31.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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.30.0"
14
+ VERSION = "2.31.0"
15
15
 
16
16
  import base64
17
17
  import os