davinci-resolve-mcp 2.64.0 → 2.66.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,86 @@
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.66.0
6
+
7
+ Generalizes the HTTP transcription backend introduced in v2.65.0 into a
8
+ configuration-driven registry, so additional local, network, or cloud-backed
9
+ adapters no longer require MCP source changes. Contributed in PR #97 by
10
+ @double2tea.
11
+
12
+ ### Changed
13
+
14
+ - **Pluggable HTTP transcription providers** (PR #97, @double2tea) — the
15
+ MLX-specific router backend is replaced by an ordered registry of HTTP
16
+ transcription providers registered via
17
+ `DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS` (a JSON array). Each entry
18
+ requires `id` and `base_url`; optional adapter fields cover `label`, `model`,
19
+ `health_path`, `transcribe_path`, `health_field`, `health_value`, `headers`,
20
+ `request_body`, `field_map`, and `response_field`. Configured providers are
21
+ selected as stable `http:<id>` backend names and preferred in transcription
22
+ capability ordering. Auth headers are sent on health and transcription
23
+ requests but kept out of capability reports, and malformed configuration
24
+ fails fast. Response handling now accepts a transcript object, a JSON-encoded
25
+ transcript string, or plain text under the configured `response_field`.
26
+ Audiobox is documented as one adapter example rather than a core requirement.
27
+
28
+ ### Removed
29
+
30
+ - The `DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL` / `DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL`
31
+ environment variables added in v2.65.0 are superseded by the generic provider
32
+ registry above. To keep an Audiobox/MLX router, register it as a provider:
33
+ `[{"id":"audiobox-local","base_url":"http://127.0.0.1:8000","request_body":{"provider":"mlx"}}]`.
34
+
35
+ ### Validation
36
+
37
+ - `tests/test_media_analysis.py` and the analysis caps/runs/store suites pass
38
+ (153 on the merged tree); static checks and drift guards pass.
39
+ - No DaVinci Resolve scripting behavior changed: the change is confined to the
40
+ stdlib HTTP transcription path and is gated behind an env var. Live Resolve
41
+ validation not required.
42
+
43
+ ## What's New in v2.65.0
44
+
45
+ Bundles two community contributions from @double2tea: an optional HTTP
46
+ transcription backend and a localized, build-aware control panel. Both are
47
+ opt-in and change no default behavior — with neither configured, existing
48
+ transcription backends and the English panel work exactly as before.
49
+
50
+ ### Added
51
+
52
+ - **MLX Audio Router transcription backend** (PR #95, @double2tea) — an optional
53
+ HTTP transcription backend that runs outside the Resolve MCP Python
54
+ environment. Enabled only when `DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL` is set and a
55
+ bounded `GET /health` probe succeeds; an optional
56
+ `DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL` overrides the router's default model.
57
+ When configured and healthy it is preferred in transcription capability
58
+ ordering, and its response is normalized into the existing JSON/SRT/VTT
59
+ transcript artifacts. Standard-library only (`urllib`); the MCP server never
60
+ installs, starts, or downloads anything on the router's behalf, and existing
61
+ Whisper backends are untouched when no URL is set.
62
+ - **Control panel localization (English / Simplified Chinese)** (PR #96,
63
+ @double2tea) — a persistent language switch on the local control panel.
64
+ English remains the authored, canonical UI; the browser stores only the
65
+ selected locale in `localStorage` and the initial locale follows browser
66
+ preferences. Translation runs client-side over the DOM (text nodes plus
67
+ `aria-label`/`title`/`placeholder`) and is fully reversible; no server API or
68
+ persisted project data changes.
69
+ - **Build-aware AI console** (PR #96, @double2tea) — the Resolve AI console now
70
+ uses the runtime capability payload to disable actions the connected Resolve
71
+ build cannot execute, distinguishing "Requires Resolve 21+" from "Unavailable
72
+ on this build" and surfacing the Resolve 20 transcription methods
73
+ (`TranscribeAudio` / `ClearTranscription`). Narrow-screen navigation and the
74
+ mobile footer were also improved.
75
+
76
+ ### Validation
77
+
78
+ - Static checks and drift guards pass; `tests/test_media_analysis.py` (110),
79
+ `tests/test_control_panel_i18n.py` + `tests/test_control_panel_ai_capabilities.py`
80
+ + `tests/test_open_control_panel.py` (14) pass on the merged tree.
81
+ - No DaVinci Resolve scripting behavior changed: PR #95 is a self-contained
82
+ HTTP/stdlib backend gated behind an env var, and PR #96 changes only the
83
+ control panel's client-side HTML/JS. Live Resolve validation not required.
84
+
5
85
  ## What's New in v2.64.0
6
86
 
7
87
  Adds opt-in support for DaVinci Resolve's **Network** external-scripting mode
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.64.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.66.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-34%20(341%20full)-blue.svg)](#server-modes)
@@ -79,6 +79,52 @@ which ffmpeg && ffmpeg -version 2>&1 | head -1
79
79
  which whisper 2>/dev/null || which whisper-cpp 2>/dev/null || python3 -c "import whisper" 2>/dev/null
80
80
  ```
81
81
 
82
+ One or more HTTP transcription adapters can be registered without installing a
83
+ transcription module into the Resolve MCP Python environment. Providers are
84
+ selected as `http:<id>`, in configuration order:
85
+
86
+ ```bash
87
+ export DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS='[
88
+ {
89
+ "id": "audiobox-local",
90
+ "label": "Audiobox local",
91
+ "base_url": "http://127.0.0.1:8000",
92
+ "model": "mlx-community/Qwen3-ASR-1.7B-8bit",
93
+ "request_body": {"provider": "mlx"}
94
+ },
95
+ {
96
+ "id": "studio-asr",
97
+ "label": "Studio ASR",
98
+ "base_url": "https://asr.example.com",
99
+ "headers": {"Authorization": "Bearer replace-me"},
100
+ "health_path": "/ready",
101
+ "transcribe_path": "/v1/transcribe",
102
+ "field_map": {"audio": "input_path"},
103
+ "response_field": "result"
104
+ }
105
+ ]'
106
+ ```
107
+
108
+ Each provider requires `id` and `base_url`. Optional fields are `label`,
109
+ `model`, `health_path`, `transcribe_path`, `health_field`, `health_value`,
110
+ `headers`, `request_body`, `field_map`, and `response_field`. Authentication
111
+ headers are used for health and transcription requests but are omitted from
112
+ capability reports.
113
+
114
+ The default adapter contract uses `GET /health` with `{"status":"ok"}` and
115
+ `POST /stt`. Its JSON request includes `audio`, `output_path`, `format`,
116
+ `verbose`, `allow_download`, and optional `model` and `language` fields.
117
+ `request_body` adds adapter-specific values, while `field_map` renames standard
118
+ request fields. The response may return a transcript object, a JSON-encoded
119
+ transcript string, or plain text under `response_field` (default
120
+ `transcript`). The normalized transcript object uses `text` and `segments`.
121
+
122
+ [Audiobox MLX Audio Router](https://github.com/double2tea/Audiobox) is one
123
+ adapter implementation, not a built-in backend requirement. Other local,
124
+ network, or cloud-backed adapters can be added through configuration without
125
+ changing MCP source code. Model download behavior follows the existing
126
+ `allow_model_download` transcription option.
127
+
82
128
  FFprobe is required. If missing:
83
129
  - macOS: `brew install ffmpeg`
84
130
  - Linux: `sudo apt install ffmpeg` or `sudo dnf install ffmpeg`
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.64.0"
39
+ VERSION = "2.66.0"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (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.64.0",
3
+ "version": "2.66.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -49,6 +49,7 @@ from src.utils.analysis_memory import read_panel_state, write_panel_state
49
49
  from src.utils import brain_edits as _brain_edits
50
50
  from src.utils import timeline_versioning as _timeline_versioning
51
51
  from src.utils import timeline_brain_db as _timeline_brain_db
52
+ from src.control_panel_i18n import localization_script
52
53
 
53
54
 
54
55
  HTML = r"""<!doctype html>
@@ -616,6 +617,33 @@ HTML = r"""<!doctype html>
616
617
  gap: var(--space-2);
617
618
  min-width: 0;
618
619
  }
620
+ .language-switch {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ min-height: 32px;
624
+ padding: 2px;
625
+ border: 1px solid var(--border-default);
626
+ border-radius: var(--radius-sm);
627
+ background: var(--bg-elevated-1);
628
+ }
629
+ .language-switch button {
630
+ min-height: 26px;
631
+ padding: 0 8px;
632
+ border: 0;
633
+ border-radius: 2px;
634
+ background: transparent;
635
+ color: var(--text-tertiary);
636
+ font-size: 11px;
637
+ font-weight: 600;
638
+ }
639
+ .language-switch button:hover {
640
+ background: var(--bg-hover);
641
+ color: var(--text-primary);
642
+ }
643
+ .language-switch button.active {
644
+ background: var(--accent-brand-muted);
645
+ color: var(--accent-brand-hover);
646
+ }
619
647
  .version-badge {
620
648
  position: relative;
621
649
  display: inline-flex;
@@ -2505,16 +2533,57 @@ HTML = r"""<!doctype html>
2505
2533
  .ai-op-btn.danger { background: #b4452f; }
2506
2534
  .ai-caps-grid {
2507
2535
  display: grid;
2508
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
2536
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
2509
2537
  gap: var(--space-2);
2510
2538
  margin-top: var(--space-2);
2511
2539
  }
2512
2540
  .ai-caps-item {
2513
- display: flex;
2541
+ display: grid;
2542
+ grid-template-columns: 9px minmax(0, 1fr) auto;
2514
2543
  align-items: center;
2515
- gap: 8px;
2544
+ column-gap: 8px;
2545
+ row-gap: 2px;
2546
+ padding: var(--space-2) 0;
2547
+ border-bottom: 1px solid var(--border-subtle);
2516
2548
  font-size: var(--text-xs);
2517
2549
  }
2550
+ .ai-build-summary {
2551
+ display: flex;
2552
+ align-items: center;
2553
+ justify-content: space-between;
2554
+ gap: var(--space-3);
2555
+ padding: var(--space-3);
2556
+ border: 1px solid var(--border-default);
2557
+ border-radius: var(--radius-sm);
2558
+ background: var(--bg-elevated-1);
2559
+ margin-bottom: var(--space-3);
2560
+ }
2561
+ .ai-build-summary strong { color: var(--text-primary); }
2562
+ .ai-build-count { color: var(--text-secondary); font-size: var(--ops-text-label); }
2563
+ .ai-caps-extra { grid-column: 2 / -1; }
2564
+ .ai-caps-status {
2565
+ grid-column: 3;
2566
+ grid-row: 1;
2567
+ color: var(--text-tertiary);
2568
+ font-size: 10px;
2569
+ white-space: nowrap;
2570
+ }
2571
+ .ai-caps-status.available { color: var(--accent-success); }
2572
+ .ai-section-unavailable > :not(.caps-section-head),
2573
+ .ai-section-unavailable .caps-section-hint {
2574
+ display: none;
2575
+ }
2576
+ .ai-section-unavailable .caps-section-head {
2577
+ flex-direction: row;
2578
+ align-items: center;
2579
+ justify-content: space-between;
2580
+ margin-bottom: 0;
2581
+ }
2582
+ .ai-section-status {
2583
+ color: var(--accent-warning);
2584
+ font-size: 10px;
2585
+ font-weight: 600;
2586
+ }
2518
2587
  .ai-caps-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
2519
2588
  .ai-caps-dot.on { background: #34a853; }
2520
2589
  .ai-caps-dot.off { background: rgba(255,255,255,0.25); }
@@ -3903,7 +3972,19 @@ HTML = r"""<!doctype html>
3903
3972
  .settings-grid { grid-template-columns: 1fr; }
3904
3973
  }
3905
3974
  @media (max-width: 620px) {
3975
+ main { padding-bottom: var(--space-3); }
3976
+ .lab-footer {
3977
+ position: static;
3978
+ height: auto;
3979
+ min-height: 112px;
3980
+ }
3906
3981
  .overview-grid { grid-template-columns: 1fr; }
3982
+ .nav-left { flex: 0 0 auto; }
3983
+ .wordmark span:first-child { display: none; }
3984
+ .nav-links > .github-icon-link { display: none; }
3985
+ .version-badge .version-label { display: none; }
3986
+ .version-badge { padding: 0 8px; }
3987
+ .language-switch button { padding: 0 6px; }
3907
3988
  }
3908
3989
  </style>
3909
3990
  </head>
@@ -3969,6 +4050,10 @@ HTML = r"""<!doctype html>
3969
4050
  </div>
3970
4051
  </nav>
3971
4052
  <div class="nav-links">
4053
+ <div class="language-switch" role="group" aria-label="Interface language">
4054
+ <button type="button" data-locale="zh-CN" aria-pressed="false">中文</button>
4055
+ <button type="button" data-locale="en" aria-pressed="false">EN</button>
4056
+ </div>
3972
4057
  <button id="versionBadge" class="version-badge" type="button" aria-label="MCP version and updates" title="MCP version">
3973
4058
  <span class="version-label">MCP</span>
3974
4059
  <span class="version-number" id="versionNumber">…</span>
@@ -4296,7 +4381,7 @@ HTML = r"""<!doctype html>
4296
4381
  <section class="span-12">
4297
4382
  <div class="section-top">
4298
4383
  <div>
4299
- <h2>Resolve 21 AI Console</h2>
4384
+ <h2>Resolve AI Console</h2>
4300
4385
  <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>
4301
4386
  </div>
4302
4387
  </div>
@@ -4305,8 +4390,8 @@ HTML = r"""<!doctype html>
4305
4390
  <div class="caps-section-hint">Checking which AI methods this Resolve build exposes…</div>
4306
4391
  </div>
4307
4392
 
4308
- <div class="caps-section" style="margin-top:12px;">
4309
- <div class="caps-section-head"><div class="caps-section-title">Governance</div>
4393
+ <div class="caps-section" id="aiGovernanceSection" style="margin-top:12px;">
4394
+ <div class="caps-section-head"><div class="caps-section-title">Governance <span class="ai-section-status"></span></div>
4310
4395
  <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>
4311
4396
  <div id="aiGovTiers" class="caps-preset-cards" role="radiogroup" aria-label="AI governance tier"></div>
4312
4397
  <div class="review-view-toggle" id="aiGovMode" role="radiogroup" aria-label="Governance mode" style="margin-top:10px;">
@@ -4349,8 +4434,8 @@ HTML = r"""<!doctype html>
4349
4434
  </div>
4350
4435
  </div>
4351
4436
 
4352
- <div class="caps-section" style="margin-top:12px;">
4353
- <div class="caps-section-head"><div class="caps-section-title">Motion deblur</div>
4437
+ <div class="caps-section" id="aiMotionDeblurSection" style="margin-top:12px;">
4438
+ <div class="caps-section-head"><div class="caps-section-title">Motion deblur <span class="ai-section-status"></span></div>
4354
4439
  <div class="caps-section-hint">Renders new deblurred media. Creates files; asks for confirmation. Leave fields blank for Resolve defaults.</div></div>
4355
4440
  <div class="settings-grid">
4356
4441
  <label>Format <input id="aiDeblurFormat" type="text" placeholder="mov"></label>
@@ -4362,8 +4447,8 @@ HTML = r"""<!doctype html>
4362
4447
  </div>
4363
4448
  </div>
4364
4449
 
4365
- <div class="caps-section" style="margin-top:12px;">
4366
- <div class="caps-section-head"><div class="caps-section-title">Speech generator</div>
4450
+ <div class="caps-section" id="aiSpeechSection" style="margin-top:12px;">
4451
+ <div class="caps-section-head"><div class="caps-section-title">Speech generator <span class="ai-section-status"></span></div>
4367
4452
  <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>
4368
4453
  <div class="settings-grid">
4369
4454
  <label style="grid-column:1/-1;">Text <textarea id="aiSpeechText" rows="2" placeholder="Text to synthesize"></textarea></label>
@@ -4378,8 +4463,8 @@ HTML = r"""<!doctype html>
4378
4463
  </div>
4379
4464
  </div>
4380
4465
 
4381
- <div class="caps-section" style="margin-top:12px;">
4382
- <div class="caps-section-head"><div class="caps-section-title">Session</div>
4466
+ <div class="caps-section" id="aiSessionSection" style="margin-top:12px;">
4467
+ <div class="caps-section-head"><div class="caps-section-title">Session <span class="ai-section-status"></span></div>
4383
4468
  <div class="caps-section-hint">Quiet Resolve's background tasks for this session before heavy work. Resets on restart.</div></div>
4384
4469
  <div class="ai-op-row"><button class="ai-op-btn ghost" data-ai-op="disable_background_tasks">Disable background tasks</button></div>
4385
4470
  </div>
@@ -4960,6 +5045,8 @@ HTML = r"""<!doctype html>
4960
5045
  </footer>
4961
5046
 
4962
5047
  <script>
5048
+ /* CONTROL_PANEL_I18N */
5049
+
4963
5050
  const state = {
4964
5051
  boot: null,
4965
5052
  projects: null,
@@ -5846,6 +5933,7 @@ HTML = r"""<!doctype html>
5846
5933
  renderOverview();
5847
5934
  renderDiagnostics();
5848
5935
  renderProjects();
5936
+ if (_aiConsoleInit) renderAiConsole();
5849
5937
  refreshRecentRootsDropdown();
5850
5938
  }
5851
5939
 
@@ -7795,23 +7883,33 @@ HTML = r"""<!doctype html>
7795
7883
  clear_audio_classification: 'clear_audio_classification',
7796
7884
  analyze_for_intellisearch: 'analyze_for_intellisearch',
7797
7885
  analyze_for_slate: 'analyze_for_slate',
7886
+ transcribe_audio: 'transcribe_audio',
7887
+ clear_transcription: 'clear_transcription',
7798
7888
  remove_motion_blur: 'remove_motion_blur',
7799
7889
  generate_speech: 'generate_speech',
7800
7890
  disable_background_tasks: 'disable_background_tasks',
7801
7891
  };
7892
+ const AI_OP_REQUIRES_21 = new Set([
7893
+ 'perform_audio_classification', 'clear_audio_classification',
7894
+ 'analyze_for_intellisearch', 'analyze_for_slate', 'remove_motion_blur',
7895
+ 'generate_speech', 'disable_background_tasks',
7896
+ ]);
7802
7897
  let _aiConsoleInit = false;
7803
7898
 
7804
7899
  function renderAiConsole() {
7805
7900
  const feats = (state.boot?.resolve?.ai_features) || {};
7806
7901
  const features = feats.features || {};
7807
7902
  const requiresExtra = feats.requires_extra || {};
7903
+ const version = state.boot?.resolve?.version_string || 'unknown';
7808
7904
  const capsEl = $('aiConsoleCaps');
7809
7905
  if (capsEl) {
7810
7906
  if (state.boot?.resolve?.available !== true) {
7811
7907
  capsEl.innerHTML = '<div class="caps-section-hint">Resolve is not connected. Open a project in DaVinci Resolve, then reload.</div>';
7812
7908
  } else {
7813
- const items = Object.keys(AI_OP_LABELS)
7814
- .filter(op => op in AI_OP_FEATURE)
7909
+ const ops = Object.keys(AI_OP_LABELS).filter(op => op in AI_OP_FEATURE);
7910
+ const availableCount = ops.filter(op => !!features[AI_OP_FEATURE[op]]).length;
7911
+ const items = ops
7912
+ .sort((a, b) => Number(!!features[AI_OP_FEATURE[b]]) - Number(!!features[AI_OP_FEATURE[a]]))
7815
7913
  .map(op => {
7816
7914
  const key = AI_OP_FEATURE[op];
7817
7915
  const on = !!features[key];
@@ -7819,13 +7917,45 @@ HTML = r"""<!doctype html>
7819
7917
  return `<div class="ai-caps-item"><span class="ai-caps-dot ${on ? 'on' : 'off'}"></span>`
7820
7918
  + `<span>${escapeHtml(AI_OP_LABELS[op])}</span>`
7821
7919
  + (extra ? `<span class="ai-caps-extra">· needs ${escapeHtml(extra)}</span>` : '')
7920
+ + `<span class="ai-caps-status ${on ? 'available' : ''}">${on ? 'Available now' : (AI_OP_REQUIRES_21.has(op) ? 'Requires Resolve 21+' : 'Unavailable on this build')}</span>`
7822
7921
  + `</div>`;
7823
7922
  }).join('');
7824
- capsEl.innerHTML = `<div class="caps-section-head"><div class="caps-section-title">Available on this Resolve build</div>`
7825
- + `<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>`
7923
+ capsEl.innerHTML = `<div class="ai-build-summary"><strong>DaVinci Resolve ${escapeHtml(version)}</strong><span class="ai-build-count">${availableCount} of ${ops.length} AI console actions available now</span></div>`
7924
+ + `<div class="caps-section-head"><div class="caps-section-title">Available on this Resolve build</div>`
7925
+ + `<div class="caps-section-hint">Unavailable actions are disabled. Resolve 21 methods may also require the named Extra from Extras Download Manager.</div></div>`
7826
7926
  + `<div class="ai-caps-grid">${items}</div>`;
7827
7927
  }
7828
7928
  }
7929
+ document.querySelectorAll('#panel-aiconsole .ai-op-btn').forEach(btn => {
7930
+ const feature = AI_OP_FEATURE[btn.dataset.aiOp];
7931
+ const available = feature ? !!features[feature] : false;
7932
+ btn.disabled = !available;
7933
+ btn.title = available ? '' : (AI_OP_REQUIRES_21.has(btn.dataset.aiOp)
7934
+ ? 'Requires DaVinci Resolve 21 or newer'
7935
+ : 'Unavailable on this Resolve build');
7936
+ });
7937
+ const speakerDetection = $('aiSpeakerDetection');
7938
+ if (speakerDetection) {
7939
+ const resolveMajor = Number(state.boot?.resolve?.version?.[0] || 0);
7940
+ speakerDetection.disabled = resolveMajor < 21;
7941
+ speakerDetection.title = resolveMajor < 21 ? 'Speaker detection requires DaVinci Resolve 21 or newer' : '';
7942
+ }
7943
+ const sectionAvailability = {
7944
+ aiGovernanceSection: !!features.remove_motion_blur || !!features.generate_speech,
7945
+ aiMotionDeblurSection: !!features.remove_motion_blur,
7946
+ aiSpeechSection: !!features.generate_speech,
7947
+ aiSessionSection: !!features.disable_background_tasks,
7948
+ };
7949
+ Object.entries(sectionAvailability).forEach(([id, available]) => {
7950
+ const section = $(id);
7951
+ if (!section) return;
7952
+ section.classList.toggle('ai-section-unavailable', !available);
7953
+ const status = section.querySelector('.ai-section-status');
7954
+ if (status) status.textContent = available ? '' : 'Requires Resolve 21+';
7955
+ section.querySelectorAll('input, textarea, select, button').forEach(control => {
7956
+ control.disabled = !available;
7957
+ });
7958
+ });
7829
7959
  // Slate color dropdown (once).
7830
7960
  const sel = $('aiSlateColor');
7831
7961
  if (sel && !sel.options.length) {
@@ -7925,7 +8055,7 @@ HTML = r"""<!doctype html>
7925
8055
  refreshResolveAiOps().catch(() => {});
7926
8056
  refreshGovernance().catch(() => {});
7927
8057
  } finally {
7928
- buttons.forEach(b => { b.disabled = false; });
8058
+ renderAiConsole();
7929
8059
  }
7930
8060
  }
7931
8061
 
@@ -7961,6 +8091,9 @@ HTML = r"""<!doctype html>
7961
8091
  <div class="caps-preset-card-stats">${stats}</div>
7962
8092
  </button>`;
7963
8093
  }).join('');
8094
+ if ($('aiGovernanceSection')?.classList.contains('ai-section-unavailable')) {
8095
+ el.querySelectorAll('button').forEach(button => { button.disabled = true; });
8096
+ }
7964
8097
  }
7965
8098
  function renderGovUsage() {
7966
8099
  const el = $('aiGovUsage');
@@ -11780,6 +11913,8 @@ HTML = r"""<!doctype html>
11780
11913
  </html>
11781
11914
  """
11782
11915
 
11916
+ HTML = HTML.replace("/* CONTROL_PANEL_I18N */", localization_script())
11917
+
11783
11918
 
11784
11919
  def _safe_call(obj: Any, method_name: str, *args: Any) -> Tuple[Any, Optional[str]]:
11785
11920
  if obj is None or not hasattr(obj, method_name):
@@ -11891,6 +12026,8 @@ def _resolve_ai_features(resolve: Any) -> Dict[str, Any]:
11891
12026
  "generate_speech": has(project, "GenerateSpeech"),
11892
12027
  "perform_audio_classification": has(folder, "PerformAudioClassification"),
11893
12028
  "clear_audio_classification": has(folder, "ClearAudioClassification"),
12029
+ "transcribe_audio": has(folder, "TranscribeAudio"),
12030
+ "clear_transcription": has(folder, "ClearTranscription"),
11894
12031
  "analyze_for_intellisearch": has(folder, "AnalyzeForIntellisearch"),
11895
12032
  "analyze_for_slate": has(folder, "AnalyzeForSlate"),
11896
12033
  "remove_motion_blur": has(folder, "RemoveMotionBlur"),