davinci-resolve-mcp 2.103.4 → 2.103.5

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,54 @@
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.103.5
6
+
7
+ Two fixes that fell out of auditing the code around this week's releases — the
8
+ same failure classes as #161 and #164, found one tier up from where each was
9
+ originally fixed.
10
+
11
+ **Cache reuse was permanently poisoned on most real installs.** The v2.103.3
12
+ transcription fix taught batch jobs that a declined transcription is not a clip
13
+ failure — but the cache layer had the same default-ON blindness.
14
+ `_report_missing_layers` counted any non-success transcript as a missing layer,
15
+ and the capability gate only screens for a missing backend, not for the stock
16
+ configuration: Whisper installed, `allow_model_download` unset. On such a
17
+ machine every analysis writes a declined "skipped" transcript, every cached
18
+ report carries `missing_layers: ['transcription']`, and `find_reusable_report`
19
+ returns `reusable: False` — forever. Every analyze call silently re-ran full
20
+ analysis (frame extraction included) and produced the same skipped transcript
21
+ again. An unfixable loop, reproduced end-to-end before the fix and green after.
22
+
23
+ A transcript-less report is now a missing layer only when a re-run could supply
24
+ the transcript: the cached payload shows a real attempt that failed (a timeout
25
+ retry may succeed), or the current options would now actually run a backend
26
+ (mock or HTTP backends, or `allow_model_download=true`). Because the check is
27
+ recomputed per request, flipping `allow_model_download` on later correctly
28
+ refuses reuse and finally produces the transcript.
29
+
30
+ **Absolute recordFrames below the timeline start are now refused.** Issue #164
31
+ documented that `recordFrame` counts from Resolve's global frame zero and that
32
+ content placed before the timeline start reads back correctly while rendering
33
+ as ~0 frames. The wrapper's `record_frame_mode='relative'` default shields
34
+ callers — but `record_frame_mode='absolute'` passed any value straight through,
35
+ so an absolute-mode caller with relative-style values reproduced the silent
36
+ stub through this server's own tools. `_normalize_record_frame` (both the
37
+ compound and granular copies) now rejects an absolute value below the
38
+ timeline's start frame with an error naming the convention; internal
39
+ absolute-mode flows (`ripple_insert` cursors) derive their frames from
40
+ timeline reads and cannot trip it. The Resolve UI cannot place content there,
41
+ so no legitimate call is lost.
42
+
43
+ ### Fixed
44
+
45
+ - A declined transcription (no `allow_model_download` opt-in, unavailable or
46
+ not-implemented backend) no longer marks cached analysis reports
47
+ incomplete, so report reuse works on default installs again. Opting into
48
+ model downloads later invalidates reuse and produces the transcript.
49
+ - `record_frame_mode='absolute'` values below the timeline start frame are
50
+ refused with a remediation instead of silently placing content the render
51
+ engine never visits (#164).
52
+
5
53
  ## What's New in v2.103.4
6
54
 
7
55
  **A frame-numbering trap, documented where agents will look it up.** Issue #164
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.103.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.103.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.103.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.103.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.103.4 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.103.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.103.4"
40
+ VERSION = "2.103.5"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (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.103.4",
3
+ "version": "2.103.5",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.103.4"
90
+ VERSION = "2.103.5"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
@@ -572,7 +572,22 @@ def _normalize_record_frame(ci, index, timeline_start_frame=None):
572
572
  }
573
573
 
574
574
  start = _frame_int(timeline_start_frame)
575
- if start in (None, 0) or mode == "absolute":
575
+ if mode == "absolute":
576
+ # recordFrame counts from Resolve's global frame zero; a value below
577
+ # the timeline start renders as ~0 frames while every readback agrees
578
+ # (issue #164). No legitimate placement exists there — refuse.
579
+ if start not in (None, 0) and rf < start:
580
+ return None, {
581
+ "error": (
582
+ f"clip_infos[{index}] recordFrame {rf} is before the timeline "
583
+ f"start frame {start}. recordFrame is timeline-absolute, so "
584
+ "content placed there reads back correctly but renders as ~0 "
585
+ "frames. Use record_frame_mode='relative' (default) or pass an "
586
+ f"absolute frame >= {start}."
587
+ )
588
+ }
589
+ return rf, None
590
+ if start in (None, 0):
576
591
  return rf, None
577
592
  if mode == "auto":
578
593
  return (start + rf) if rf < start else rf, None
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.103.4"
14
+ VERSION = "2.103.5"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2894,7 +2894,24 @@ def _normalize_record_frame(
2894
2894
  )
2895
2895
 
2896
2896
  start = _frame_int(timeline_start_frame)
2897
- if start in (None, 0) or mode == "absolute":
2897
+ if mode == "absolute":
2898
+ # recordFrame counts from Resolve's global frame zero. An absolute
2899
+ # value below the timeline's own start frame places content the render
2900
+ # engine never visits: every readback agrees, the render reports
2901
+ # Complete, and the output is a near-empty stub (issue #164; see the
2902
+ # api_truth recordFrame timeline-absolute origin entry). The Resolve UI
2903
+ # cannot place content there, so there is no legitimate case — refuse.
2904
+ if start not in (None, 0) and rf < start:
2905
+ return None, _err(
2906
+ f"clip_infos[{index}] recordFrame {rf} is before the timeline start "
2907
+ f"frame {start}. recordFrame is timeline-absolute (counted from frame "
2908
+ "zero, not from the timeline's start), so content placed there reads "
2909
+ "back correctly but renders as ~0 frames. Use the default "
2910
+ "record_frame_mode='relative' with an offset from the timeline start, "
2911
+ f"or pass an absolute frame >= {start}."
2912
+ )
2913
+ return rf, None
2914
+ if start in (None, 0):
2898
2915
  return rf, None
2899
2916
  if mode == "auto":
2900
2917
  return (start + rf) if rf < start else rf, None
@@ -1884,7 +1884,9 @@ API_TRUTH: List[Dict[str, Any]] = [
1884
1884
  "media_pool.append_to_timeline defaults to "
1885
1885
  "record_frame_mode='relative' and adds the start frame, "
1886
1886
  "so pass record_frame_mode='absolute' only for raw "
1887
- "Resolve frame numbers. When driving the API directly, "
1887
+ "Resolve frame numbers and since v2.103.5 an absolute "
1888
+ "value below the timeline start is refused outright. "
1889
+ "When driving the API directly, "
1888
1890
  "never treat JobStatus Complete as proof a render "
1889
1891
  "worked — check the output file's duration, not just "
1890
1892
  "that the job finished.",
@@ -227,6 +227,30 @@ def transcription_attempt_failed(transcript: Any, *, enabled: bool) -> bool:
227
227
  return status not in TRANSCRIPTION_UNATTEMPTED_STATUSES
228
228
 
229
229
 
230
+ def transcription_options_would_attempt(transcription: Dict[str, Any]) -> bool:
231
+ """True when these options would actually run a transcription backend.
232
+
233
+ Mirrors _transcribe's early-outs. The local backends (whisper_cli,
234
+ mlx_whisper — also what a backend of None resolves to) refuse to run
235
+ without allow_model_download=true, whisper_cpp is not_implemented, and
236
+ the resolve backend is refused by design; mock and HTTP-provider
237
+ backends always attempt. Used by _report_missing_layers to decide
238
+ whether re-running an analysis could produce a transcript a cached
239
+ report lacks — if it could not, the cached report is as complete as a
240
+ fresh run would be. Known blind spot: backend None on a machine whose
241
+ only configured backend is an HTTP provider reports False here; name
242
+ the http_* backend explicitly to get cache invalidation.
243
+ """
244
+ backend = transcription.get("backend")
245
+ if backend in {"mock", "local_mock"}:
246
+ return True
247
+ if isinstance(backend, str) and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX):
248
+ return True
249
+ if backend in {"whisper_cpp", "resolve"}:
250
+ return False
251
+ return _coerce_bool(transcription.get("allow_model_download"), default=False)
252
+
253
+
230
254
  def _annotate_clip_transcript_failure(clip_result: Dict[str, Any], transcript: Any) -> None:
231
255
  """Mark a clip failed when requested transcription did not complete.
232
256
 
@@ -3665,7 +3689,19 @@ def _report_missing_layers(report: Dict[str, Any], depth: str, options: Dict[str
3665
3689
  if _coerce_bool(transcription.get("enabled"), default=DEFAULT_TRANSCRIPTION_ENABLED):
3666
3690
  transcript = report.get("transcription") or {}
3667
3691
  if not transcript.get("success") or transcript.get("status") == "skipped":
3668
- missing.append("transcription")
3692
+ # A transcript-less report is a missing layer only when a re-run
3693
+ # could supply the transcript: either the cached payload shows a
3694
+ # real attempt that failed (a retry may fix a timeout), or the
3695
+ # current options would now actually run a backend. Transcription
3696
+ # is enabled by default while allow_model_download is not, so on a
3697
+ # stock install every report carries a declined "skipped" payload —
3698
+ # counting that as missing made every cached report reusable=False
3699
+ # forever, silently defeating cache reuse with full re-analysis
3700
+ # that could never produce the transcript either.
3701
+ if transcription_attempt_failed(
3702
+ transcript, enabled=True
3703
+ ) or transcription_options_would_attempt(transcription):
3704
+ missing.append("transcription")
3669
3705
  vision = options.get("vision") or {}
3670
3706
  if _coerce_bool(vision.get("enabled"), default=False):
3671
3707
  visual = report.get("visual") or {}