davinci-resolve-mcp 2.68.2 → 2.69.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/scripts/doctor.py CHANGED
@@ -203,6 +203,121 @@ print(json.dumps(payload))
203
203
  return payload
204
204
 
205
205
 
206
+ def detect_edition(probe: dict[str, Any]) -> str | None:
207
+ """"Studio" / "Free" from the connected product name, or None if unknown.
208
+
209
+ Only knowable when something answered. Guessing from an install path would
210
+ be wrong on any machine running both, which is common — the sandboxed free
211
+ edition and a direct-download Studio coexist happily.
212
+ """
213
+ product = str(probe.get("product") or "")
214
+ if not product:
215
+ return None
216
+ return "Studio" if "studio" in product.lower() else "Free"
217
+
218
+
219
+ def bridge_checks(probe: dict[str, Any]) -> list[dict[str, str]]:
220
+ """Report the free-edition in-app bridge as a first-class path.
221
+
222
+ The bridge shipped in v2.68.0 and nothing in the diagnostics mentioned it,
223
+ so a free-edition user running doctor saw a wall of failures and no way
224
+ through — which is precisely the conclusion the docs used to push them to.
225
+ """
226
+ results: list[dict[str, str]] = []
227
+ installer = REPO / "scripts" / "install_resolve_bridge.py"
228
+ enabled = os.environ.get("DAVINCI_RESOLVE_BRIDGE", "").strip().lower() not in ("", "0", "false", "no", "off")
229
+
230
+ installed_at: list[str] = []
231
+ try:
232
+ sys.path.insert(0, str(REPO / "scripts"))
233
+ import install_resolve_bridge as _bridge # type: ignore
234
+
235
+ for folder in _bridge.script_targets():
236
+ if (folder / "resolve_bridge.py").exists():
237
+ installed_at.append(str(folder))
238
+ except Exception:
239
+ # Never let a diagnostic tool be the thing that crashes.
240
+ installed_at = []
241
+
242
+ if installed_at:
243
+ check(results, "OK", "Free-edition bridge", f"installed: {installed_at[0]}")
244
+ elif installer.exists():
245
+ check(
246
+ results, "INFO", "Free-edition bridge",
247
+ "not installed. Only needed on the FREE edition (Studio uses external "
248
+ f"scripting directly). Install: {PYTHON} {installer}",
249
+ )
250
+ else:
251
+ check(results, "WARN", "Free-edition bridge", f"installer missing: {installer}")
252
+
253
+ edition = detect_edition(probe)
254
+ if edition:
255
+ check(results, "OK", "Resolve edition", edition)
256
+ elif installed_at or enabled:
257
+ check(results, "INFO", "Resolve edition",
258
+ "unknown — nothing answered, so the edition cannot be read")
259
+
260
+ if enabled:
261
+ connected = bool(probe.get("resolve_connected"))
262
+ check(
263
+ results, "OK" if connected else "WARN", "DAVINCI_RESOLVE_BRIDGE",
264
+ "set — the bridge is the only transport tried"
265
+ + ("" if connected else ". Nothing answered: in Resolve run "
266
+ "Workspace > Scripts > resolve_bridge (a saved project must be open)."),
267
+ )
268
+ elif installed_at:
269
+ check(
270
+ results, "INFO", "DAVINCI_RESOLVE_BRIDGE",
271
+ "not set — the bridge is installed but will not be used. "
272
+ "export DAVINCI_RESOLVE_BRIDGE=1 to enable it.",
273
+ )
274
+ return results
275
+
276
+
277
+ #: Optional extras and what each unlocks. Absent is INFO, never FAIL — the core
278
+ #: install is deliberately small and every feature below refuses honestly with
279
+ #: its own install line rather than degrading into a guess.
280
+ OPTIONAL_EXTRAS = (
281
+ ("numpy", "colour pre-balance, reference-still matching, sound-density audit", "pip install numpy"),
282
+ ("librosa", "beat / bar / phrase detection for music-driven cutting", "pip install librosa"),
283
+ ("whisper", "transcription, and the word-level tools built on it", "pip install -U openai-whisper"),
284
+ ("open_clip", "visual similarity and find_similar", "pip install open_clip_torch"),
285
+ ("transformers", "CLAP audio embeddings", "pip install transformers"),
286
+ ("cv2", "additional frame analysis", "pip install opencv-python"),
287
+ )
288
+
289
+
290
+ def extras_checks() -> list[dict[str, str]]:
291
+ """Report which optional extras are installed and what each would unlock.
292
+
293
+ Absence is informational. The point is that a user should not have to
294
+ discover an extra by hitting a refusal mid-workflow — "setup too hard" was
295
+ one of the loudest complaints this release set out to answer, and shipping
296
+ features behind an undocumented `pip install` is the same failure.
297
+ """
298
+ import importlib.util
299
+
300
+ results: list[dict[str, str]] = []
301
+ missing: list[str] = []
302
+ for module, unlocks, install in OPTIONAL_EXTRAS:
303
+ try:
304
+ present = importlib.util.find_spec(module) is not None
305
+ except (ImportError, ValueError):
306
+ present = False
307
+ if present:
308
+ check(results, "OK", f"Extra: {module}", unlocks)
309
+ else:
310
+ missing.append(module)
311
+ check(results, "INFO", f"Extra: {module}", f"not installed — {unlocks} ({install})")
312
+ if missing:
313
+ check(
314
+ results, "INFO", "Optional extras",
315
+ f"{len(missing)} of {len(OPTIONAL_EXTRAS)} absent. None are required; each "
316
+ "feature refuses with its install line rather than guessing.",
317
+ )
318
+ return results
319
+
320
+
206
321
  def collect(
207
322
  resolve_host: str | None = None,
208
323
  resolve_timeout: float | None = None,
@@ -240,17 +355,28 @@ def collect(
240
355
  str(probe["connection_error"]),
241
356
  )
242
357
  else:
358
+ # This is also exactly what the free edition looks like: the module
359
+ # imports fine and scriptapp refuses, because Blackmagic gates
360
+ # *external* scripting to Studio. Sending someone to toggle a
361
+ # preference that cannot help them is a dead end, so name the bridge
362
+ # here too.
243
363
  check(
244
364
  results,
245
365
  "WARN",
246
366
  "Resolve scripting connection",
247
- "Module import worked, but scriptapp returned no object. Use "
248
- "External scripting = Local, or Network with --resolve-host "
249
- "set to the Resolve host IP, then restart Resolve.",
367
+ "Module import worked, but scriptapp returned no object. On Studio: "
368
+ "External scripting = Local, or Network with --resolve-host set to "
369
+ "the Resolve host IP, then restart Resolve. On the FREE edition "
370
+ "external scripting is gated off entirely — use the in-app bridge "
371
+ "(see 'Free-edition bridge' below).",
250
372
  )
251
373
  else:
252
374
  check(results, "FAIL", "DaVinciResolveScript import", str(probe.get("error")))
253
375
 
376
+ results.extend(bridge_checks(probe))
377
+
378
+ results.extend(extras_checks())
379
+
254
380
  check(results, "OK", "MCP server version", version_from_server())
255
381
  check(results, "OK", "MCP git head", git_head())
256
382
  check(results, "OK", "Git status", git_summary())
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.68.2"
88
+ VERSION = "2.69.0"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  logger.info(f"Detected platform: {get_platform()}")