davinci-resolve-mcp 2.66.0 → 2.67.1
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 +101 -0
- package/README.md +1 -1
- package/docs/SKILL.md +10 -0
- package/docs/kernels/render-deliver-kernel.md +46 -2
- package/docs/process/release-process.md +8 -1
- package/docs/reference/api-limitations.md +18 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +4 -1
- package/resolve-advanced/server/deliverable-spec-bridge.mjs +310 -0
- package/resolve-advanced/server/runner-apply-contract.mjs +16 -1
- package/resolve-advanced/server/tools/deliverable.mjs +20 -1
- package/src/granular/common.py +5 -1
- package/src/granular/project.py +22 -7
- package/src/server.py +271 -22
- package/src/utils/api_truth.py +43 -0
- package/src/utils/delivery_targets.py +846 -0
- package/src/utils/render_ids.py +68 -0
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* re_delivery_diff — old vs new render: frame-count/duration Δ + spec drift
|
|
11
11
|
* render_manifest — build (checksums + frame counts) / reconcile actual outputs
|
|
12
12
|
* expand_deliverable — model texted/textless/stems/slate/leader entities + their rules
|
|
13
|
+
* spec_from_authored — authored deliverable vocabulary → deliverable_qc spec + loudness target
|
|
13
14
|
*/
|
|
14
15
|
import { z } from 'zod';
|
|
15
16
|
import { deliverableQc, loudnessQc, reframeBlankingCheck, conformCompleteness, reDeliveryDiff } from '../deliverable-qc.mjs';
|
|
16
17
|
import { buildManifest, reconcileManifest } from '../render-manifest.mjs';
|
|
17
18
|
import { expandDeliverable } from '../deliverable-entities.mjs';
|
|
19
|
+
import { deliverableSpecFromAuthored, deliverableSpecsFromAuthored } from '../deliverable-spec-bridge.mjs';
|
|
18
20
|
|
|
19
21
|
const specSchema = z.object({}).passthrough();
|
|
20
22
|
|
|
@@ -64,10 +66,21 @@ const expandSchema = z.object({
|
|
|
64
66
|
entities: z.array(z.enum(['texted', 'textless', 'stems_ME', 'slate', 'leader'])).optional(),
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
const specFromAuthoredSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
deliverable: specSchema
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('One authored deliverable: { id?, video:{codec,res,fps,color}, audio:{config,loudness}, container?, naming? }'),
|
|
74
|
+
deliverables: z.array(specSchema).optional().describe('An authored deliverables: list'),
|
|
75
|
+
})
|
|
76
|
+
.refine((a) => !!a.deliverable !== !!a.deliverables, {
|
|
77
|
+
message: 'pass exactly one of deliverable or deliverables',
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
export const deliverableTool = {
|
|
68
81
|
name: 'deliverable',
|
|
69
82
|
description:
|
|
70
|
-
'Deliverable QC / compliance (Cluster D) — the #1 reject-preventer. MEASURE/report-only (gate: review — never auto-pass-clear). Actions: deliverable_qc (ffprobe a render vs its spec → pass/fail per field: codec/raster/fps/scan/color tags/audio layout/naming/duration), loudness_qc (ebur128 integrated LUFS + true-peak dBTP + LRA vs target), reframe_blanking_check (letterbox/pillarbox + active-picture bounds + illegal edge pixels), conform_completeness (all online, handles, duration == reference frame-exact), re_delivery_diff (old vs new render: frame/duration Δ + spec drift), render_manifest (build checksums+frame-counts / reconcile actual outputs), expand_deliverable (model texted/textless/stems/slate/leader entities + rules). Needs ffmpeg/ffprobe on PATH for the file actions.',
|
|
83
|
+
'Deliverable QC / compliance (Cluster D) — the #1 reject-preventer. MEASURE/report-only (gate: review — never auto-pass-clear). Actions: deliverable_qc (ffprobe a render vs its spec → pass/fail per field: codec/raster/fps/scan/color tags/audio layout/naming/duration), loudness_qc (ebur128 integrated LUFS + true-peak dBTP + LRA vs target), reframe_blanking_check (letterbox/pillarbox + active-picture bounds + illegal edge pixels), conform_completeness (all online, handles, duration == reference frame-exact), re_delivery_diff (old vs new render: frame/duration Δ + spec drift), render_manifest (build checksums+frame-counts / reconcile actual outputs), expand_deliverable (model texted/textless/stems/slate/leader entities + rules), spec_from_authored (authored deliverable vocabulary — codec display names, "1920x1080", "-16 LUFS", naming templates — into a deliverable_qc spec + loudness_qc target, reporting anything it could not map). Needs ffmpeg/ffprobe on PATH for the file actions.',
|
|
71
84
|
async handler({ action, args }) {
|
|
72
85
|
if (action === 'deliverable_qc') {
|
|
73
86
|
const p = deliverableQcSchema.parse(args);
|
|
@@ -100,6 +113,12 @@ export const deliverableTool = {
|
|
|
100
113
|
const p = expandSchema.parse(args);
|
|
101
114
|
return expandDeliverable({ name: p.name, spec: p.spec, entities: p.entities });
|
|
102
115
|
}
|
|
116
|
+
if (action === 'spec_from_authored') {
|
|
117
|
+
const p = specFromAuthoredSchema.parse(args);
|
|
118
|
+
return p.deliverables
|
|
119
|
+
? { deliverables: deliverableSpecsFromAuthored(p.deliverables) }
|
|
120
|
+
: deliverableSpecFromAuthored(p.deliverable);
|
|
121
|
+
}
|
|
103
122
|
throw new Error(`Unknown deliverable action: ${action}`);
|
|
104
123
|
},
|
|
105
124
|
};
|
package/src/granular/common.py
CHANGED
|
@@ -44,6 +44,10 @@ from src.utils.layout_presets import (
|
|
|
44
44
|
)
|
|
45
45
|
from src.utils.object_inspection import inspect_object, print_object_help
|
|
46
46
|
from src.utils.platform import get_platform, get_resolve_paths
|
|
47
|
+
from src.utils.render_ids import (
|
|
48
|
+
render_codec_id_from_codecs,
|
|
49
|
+
render_format_id_from_formats,
|
|
50
|
+
)
|
|
47
51
|
from src.utils.resolve_connection import connect_resolve
|
|
48
52
|
from src.utils.project_properties import (
|
|
49
53
|
get_all_project_properties,
|
|
@@ -81,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
81
85
|
handlers=[logging.StreamHandler()],
|
|
82
86
|
)
|
|
83
87
|
|
|
84
|
-
VERSION = "2.
|
|
88
|
+
VERSION = "2.67.1"
|
|
85
89
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
86
90
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
87
91
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/granular/project.py
CHANGED
|
@@ -1225,7 +1225,7 @@ def get_render_codecs(format_name: str) -> Dict[str, Any]:
|
|
|
1225
1225
|
"""Get available codecs for a given render format.
|
|
1226
1226
|
|
|
1227
1227
|
Args:
|
|
1228
|
-
format_name: Render format name (e.g. '
|
|
1228
|
+
format_name: Render format id or display name (e.g. 'mov', 'QuickTime').
|
|
1229
1229
|
"""
|
|
1230
1230
|
resolve = get_resolve()
|
|
1231
1231
|
if resolve is None:
|
|
@@ -1233,8 +1233,11 @@ def get_render_codecs(format_name: str) -> Dict[str, Any]:
|
|
|
1233
1233
|
project = resolve.GetProjectManager().GetCurrentProject()
|
|
1234
1234
|
if not project:
|
|
1235
1235
|
return {"error": "No project currently open"}
|
|
1236
|
-
|
|
1237
|
-
|
|
1236
|
+
# GetRenderCodecs needs the format *id*; GetRenderFormats returns
|
|
1237
|
+
# {display_name: id}, so an unnormalized display name yields {} (issue #59).
|
|
1238
|
+
format_id = render_format_id_from_formats(project.GetRenderFormats() or {}, format_name)
|
|
1239
|
+
codecs = project.GetRenderCodecs(format_id)
|
|
1240
|
+
return {"format": format_name, "format_id": format_id, "codecs": codecs if codecs else {}}
|
|
1238
1241
|
|
|
1239
1242
|
|
|
1240
1243
|
@mcp.tool()
|
|
@@ -1255,8 +1258,9 @@ def set_current_render_format_and_codec(format_name: str, codec_name: str) -> Di
|
|
|
1255
1258
|
"""Set the render format and codec.
|
|
1256
1259
|
|
|
1257
1260
|
Args:
|
|
1258
|
-
format_name: Render format (e.g. '
|
|
1259
|
-
codec_name: Codec name (e.g. '
|
|
1261
|
+
format_name: Render format id or display name (e.g. 'mov', 'QuickTime').
|
|
1262
|
+
codec_name: Codec id or display name (e.g. 'ProRes422HQ',
|
|
1263
|
+
'Apple ProRes 422 HQ', 'H.264').
|
|
1260
1264
|
"""
|
|
1261
1265
|
resolve = get_resolve()
|
|
1262
1266
|
if resolve is None:
|
|
@@ -1264,8 +1268,19 @@ def set_current_render_format_and_codec(format_name: str, codec_name: str) -> Di
|
|
|
1264
1268
|
project = resolve.GetProjectManager().GetCurrentProject()
|
|
1265
1269
|
if not project:
|
|
1266
1270
|
return {"error": "No project currently open"}
|
|
1267
|
-
|
|
1268
|
-
|
|
1271
|
+
# Both arguments must be *ids*. The display names Resolve shows in the UI are
|
|
1272
|
+
# the dict keys, not the values, so passing them through raw is silently
|
|
1273
|
+
# rejected for every format where label != id (QuickTime/ProRes, not mp4).
|
|
1274
|
+
format_id = render_format_id_from_formats(project.GetRenderFormats() or {}, format_name)
|
|
1275
|
+
codec_id = render_codec_id_from_codecs(project.GetRenderCodecs(format_id) or {}, codec_name)
|
|
1276
|
+
result = project.SetCurrentRenderFormatAndCodec(format_id, codec_id)
|
|
1277
|
+
return {
|
|
1278
|
+
"success": bool(result),
|
|
1279
|
+
"format": format_name,
|
|
1280
|
+
"codec": codec_name,
|
|
1281
|
+
"format_id": format_id,
|
|
1282
|
+
"codec_id": codec_id,
|
|
1283
|
+
}
|
|
1269
1284
|
|
|
1270
1285
|
|
|
1271
1286
|
@mcp.tool()
|
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.
|
|
14
|
+
VERSION = "2.67.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -49,6 +49,11 @@ from src.utils.cut_ir import build_cut_list as _build_cut_list
|
|
|
49
49
|
from src.utils.page_lock import open_page_serialized as _open_page_serialized
|
|
50
50
|
from src.utils.proc import safe_run
|
|
51
51
|
from src.utils.readback import verify_by_readback, verification_stats as _verification_stats
|
|
52
|
+
from src.utils.render_ids import (
|
|
53
|
+
render_codec_id_from_codecs as _render_codec_id_from_codecs,
|
|
54
|
+
render_format_id_from_formats as _render_format_id_from_formats,
|
|
55
|
+
)
|
|
56
|
+
from src.utils import delivery_targets as _delivery_targets
|
|
52
57
|
from src.utils.update_check import (
|
|
53
58
|
check_for_updates,
|
|
54
59
|
clear_update_prompt_preferences,
|
|
@@ -15233,6 +15238,9 @@ _RENDER_KERNEL_ACTIONS = [
|
|
|
15233
15238
|
"quick_export_capabilities",
|
|
15234
15239
|
"safe_quick_export",
|
|
15235
15240
|
"export_render_boundary_report",
|
|
15241
|
+
"list_delivery_targets",
|
|
15242
|
+
"resolve_delivery_target",
|
|
15243
|
+
"prepare_delivery_job",
|
|
15236
15244
|
]
|
|
15237
15245
|
|
|
15238
15246
|
|
|
@@ -15256,24 +15264,6 @@ def _render_formats(proj):
|
|
|
15256
15264
|
return _ser(formats)
|
|
15257
15265
|
|
|
15258
15266
|
|
|
15259
|
-
def _render_format_id_from_formats(formats: Any, fmt: str) -> str:
|
|
15260
|
-
if not isinstance(fmt, str) or not fmt or not isinstance(formats, dict):
|
|
15261
|
-
return fmt
|
|
15262
|
-
if fmt in formats:
|
|
15263
|
-
return formats.get(fmt) or fmt
|
|
15264
|
-
if fmt in formats.values():
|
|
15265
|
-
return fmt
|
|
15266
|
-
needle = fmt.lower()
|
|
15267
|
-
for label, format_id in formats.items():
|
|
15268
|
-
label_text = str(label)
|
|
15269
|
-
id_text = str(format_id)
|
|
15270
|
-
if label_text.lower() == needle:
|
|
15271
|
-
return id_text or fmt
|
|
15272
|
-
if id_text.lower() == needle:
|
|
15273
|
-
return id_text
|
|
15274
|
-
return fmt
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
15267
|
def _render_format_id(proj, fmt: str, formats: Optional[Dict[str, Any]] = None) -> str:
|
|
15278
15268
|
if formats is None:
|
|
15279
15269
|
formats = _render_formats(proj)
|
|
@@ -15294,6 +15284,15 @@ def _render_format_requested(requested: Optional[List[Any]], label: str, extensi
|
|
|
15294
15284
|
return False
|
|
15295
15285
|
|
|
15296
15286
|
|
|
15287
|
+
def _render_codec_id(proj, fmt: str, codec: str, formats: Optional[Dict[str, Any]] = None) -> str:
|
|
15288
|
+
"""Resolve a codec display name to its id for `fmt` (itself label-or-id tolerant)."""
|
|
15289
|
+
try:
|
|
15290
|
+
codecs = proj.GetRenderCodecs(_render_format_id(proj, fmt, formats)) or {}
|
|
15291
|
+
except Exception:
|
|
15292
|
+
return codec
|
|
15293
|
+
return _render_codec_id_from_codecs(codecs, codec)
|
|
15294
|
+
|
|
15295
|
+
|
|
15297
15296
|
def _render_codecs(proj, fmt: str, formats: Optional[Dict[str, Any]] = None):
|
|
15298
15297
|
try:
|
|
15299
15298
|
return _ser(proj.GetRenderCodecs(_render_format_id(proj, fmt, formats)) or {})
|
|
@@ -15490,7 +15489,33 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
|
|
|
15490
15489
|
before = _render_settings_snapshot(proj)
|
|
15491
15490
|
format_success = None
|
|
15492
15491
|
if p.get("format") and p.get("codec"):
|
|
15493
|
-
|
|
15492
|
+
formats = _render_formats(proj)
|
|
15493
|
+
format_id = _render_format_id_from_formats(formats, p["format"])
|
|
15494
|
+
codec_id = _render_codec_id(proj, format_id, p["codec"], formats)
|
|
15495
|
+
format_success = bool(proj.SetCurrentRenderFormatAndCodec(format_id, codec_id))
|
|
15496
|
+
if not format_success:
|
|
15497
|
+
# Refuse to queue. Continuing here would apply settings, return a real
|
|
15498
|
+
# job id, and report success=True for a job that renders in the
|
|
15499
|
+
# PREVIOUSLY set format/codec — with format_success=False the only
|
|
15500
|
+
# signal, buried in the payload.
|
|
15501
|
+
return _err(
|
|
15502
|
+
f"Could not set render format/codec: {p['format']} / {p['codec']}",
|
|
15503
|
+
code="RENDER_FORMAT_CODEC_REJECTED",
|
|
15504
|
+
category="invalid_input",
|
|
15505
|
+
reason="Resolve rejected the format/codec pair; no render job was queued.",
|
|
15506
|
+
remediation=(
|
|
15507
|
+
"Check render(action='get_formats') and "
|
|
15508
|
+
"render(action='get_codecs', params={'format': ...}) for what this "
|
|
15509
|
+
"machine, license, and plugin set actually support."
|
|
15510
|
+
),
|
|
15511
|
+
state={
|
|
15512
|
+
"requested_format": p["format"],
|
|
15513
|
+
"requested_codec": p["codec"],
|
|
15514
|
+
"resolved_format_id": format_id,
|
|
15515
|
+
"resolved_codec_id": codec_id,
|
|
15516
|
+
"available_codecs": _render_codecs(proj, format_id, formats),
|
|
15517
|
+
},
|
|
15518
|
+
)
|
|
15494
15519
|
settings_success = bool(proj.SetRenderSettings(settings))
|
|
15495
15520
|
job_id = proj.AddRenderJob() if settings_success else None
|
|
15496
15521
|
return {
|
|
@@ -15503,6 +15528,186 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
|
|
|
15503
15528
|
}
|
|
15504
15529
|
|
|
15505
15530
|
|
|
15531
|
+
# ── Delivery targets ────────────────────────────────────────────────────────
|
|
15532
|
+
# Named render intents. One definition emits both the Resolve render settings
|
|
15533
|
+
# and the ffprobe-shaped QC spec, so the advanced server can verify a rendered
|
|
15534
|
+
# file against the same intent that produced it. See src/utils/delivery_targets.py.
|
|
15535
|
+
|
|
15536
|
+
|
|
15537
|
+
def _delivery_target_extras() -> Dict[str, Any]:
|
|
15538
|
+
"""User-defined targets from logs/delivery-targets.json; {} if absent/broken."""
|
|
15539
|
+
try:
|
|
15540
|
+
return _delivery_targets.load_user_targets(
|
|
15541
|
+
_delivery_targets.user_targets_path(project_dir)
|
|
15542
|
+
)
|
|
15543
|
+
except Exception as exc: # pragma: no cover - defensive; loader already tolerates
|
|
15544
|
+
logger.warning("could not load user delivery targets: %s", exc)
|
|
15545
|
+
return {}
|
|
15546
|
+
|
|
15547
|
+
|
|
15548
|
+
def _delivery_timeline_fps(proj) -> Optional[float]:
|
|
15549
|
+
"""Timeline frame rate for targets that inherit it. None when unavailable.
|
|
15550
|
+
|
|
15551
|
+
Distinct from `_timeline_fps(tl)` above, which takes a timeline and returns a
|
|
15552
|
+
(fps, err) tuple for timecode math.
|
|
15553
|
+
"""
|
|
15554
|
+
try:
|
|
15555
|
+
timeline = proj.GetCurrentTimeline()
|
|
15556
|
+
if not timeline:
|
|
15557
|
+
return None
|
|
15558
|
+
value = timeline.GetSetting("timelineFrameRate")
|
|
15559
|
+
return float(value) if value else None
|
|
15560
|
+
except Exception:
|
|
15561
|
+
return None
|
|
15562
|
+
|
|
15563
|
+
|
|
15564
|
+
def _resolve_delivery_target_live(proj, p: Dict[str, Any]):
|
|
15565
|
+
"""Resolve a named target against the LIVE format/codec matrix.
|
|
15566
|
+
|
|
15567
|
+
Returns (payload, error). Availability is machine/license/plugin dependent,
|
|
15568
|
+
so a target that cannot be satisfied here fails with the actual available
|
|
15569
|
+
lists rather than handing Resolve a name it will reject.
|
|
15570
|
+
"""
|
|
15571
|
+
name = p.get("target")
|
|
15572
|
+
if not name:
|
|
15573
|
+
return None, _err(
|
|
15574
|
+
"target is required",
|
|
15575
|
+
code="MISSING_TARGET",
|
|
15576
|
+
category="invalid_input",
|
|
15577
|
+
remediation="Call render(action='list_delivery_targets') to see the available names.",
|
|
15578
|
+
)
|
|
15579
|
+
extras = _delivery_target_extras()
|
|
15580
|
+
target = _delivery_targets.resolve_target(name, p.get("overrides"), extra_targets=extras)
|
|
15581
|
+
if target is None:
|
|
15582
|
+
return None, _err(
|
|
15583
|
+
f"Unknown delivery target: {name}",
|
|
15584
|
+
code="UNKNOWN_DELIVERY_TARGET",
|
|
15585
|
+
category="invalid_input",
|
|
15586
|
+
remediation="Call render(action='list_delivery_targets') to see the available names.",
|
|
15587
|
+
state={"known_targets": sorted(_delivery_targets.list_targets(extras))},
|
|
15588
|
+
)
|
|
15589
|
+
|
|
15590
|
+
formats = _render_formats(proj)
|
|
15591
|
+
format_id = _delivery_targets.select_available(target.format_candidates, formats)
|
|
15592
|
+
if not format_id:
|
|
15593
|
+
return None, _err(
|
|
15594
|
+
f"Delivery target '{target.id}' needs a render format this machine does not expose",
|
|
15595
|
+
code="DELIVERY_TARGET_FORMAT_UNAVAILABLE",
|
|
15596
|
+
category="unsupported",
|
|
15597
|
+
reason="Render format availability depends on the Resolve version, license, and installed IO plugins.",
|
|
15598
|
+
remediation="Check render(action='get_formats'); a codec plugin may need installing, or Resolve restarting.",
|
|
15599
|
+
state={"target": target.id, "tried": list(target.format_candidates), "available_formats": formats},
|
|
15600
|
+
)
|
|
15601
|
+
|
|
15602
|
+
codecs = _render_codecs(proj, format_id, formats)
|
|
15603
|
+
codec_id = _delivery_targets.select_available(target.codec_candidates, codecs)
|
|
15604
|
+
if not codec_id:
|
|
15605
|
+
return None, _err(
|
|
15606
|
+
f"Delivery target '{target.id}' needs a codec this machine does not expose for {format_id}",
|
|
15607
|
+
code="DELIVERY_TARGET_CODEC_UNAVAILABLE",
|
|
15608
|
+
category="unsupported",
|
|
15609
|
+
reason="Codec availability depends on the Resolve version, license, and installed IO plugins.",
|
|
15610
|
+
remediation="Check render(action='get_codecs', params={'format': ...}); a codec plugin may need installing, or Resolve restarting.",
|
|
15611
|
+
state={
|
|
15612
|
+
"target": target.id,
|
|
15613
|
+
"format_id": format_id,
|
|
15614
|
+
"tried": list(target.codec_candidates),
|
|
15615
|
+
"available_codecs": codecs,
|
|
15616
|
+
},
|
|
15617
|
+
)
|
|
15618
|
+
|
|
15619
|
+
fps = _delivery_timeline_fps(proj)
|
|
15620
|
+
qc_spec = _delivery_targets.to_qc_spec(target, timeline_fps=fps)
|
|
15621
|
+
return (
|
|
15622
|
+
{
|
|
15623
|
+
"target": target.id,
|
|
15624
|
+
"label": target.label,
|
|
15625
|
+
"tier": target.tier,
|
|
15626
|
+
"format_id": format_id,
|
|
15627
|
+
"codec_id": codec_id,
|
|
15628
|
+
"timeline_fps": fps,
|
|
15629
|
+
"settings": _delivery_targets.to_render_settings(target, timeline_fps=fps),
|
|
15630
|
+
"qc_spec": qc_spec,
|
|
15631
|
+
"qc_note": (
|
|
15632
|
+
None
|
|
15633
|
+
if qc_spec
|
|
15634
|
+
else "This target renders an image sequence; deliverable_qc probes a single file, so it has no QC spec."
|
|
15635
|
+
),
|
|
15636
|
+
"notes": list(target.notes),
|
|
15637
|
+
},
|
|
15638
|
+
None,
|
|
15639
|
+
)
|
|
15640
|
+
|
|
15641
|
+
|
|
15642
|
+
def _list_delivery_targets(proj, p: Dict[str, Any]):
|
|
15643
|
+
tier = p.get("tier")
|
|
15644
|
+
if tier is not None and tier not in _delivery_targets.TIERS:
|
|
15645
|
+
return _err(
|
|
15646
|
+
f"Unknown tier: {tier}",
|
|
15647
|
+
code="UNKNOWN_DELIVERY_TIER",
|
|
15648
|
+
category="invalid_input",
|
|
15649
|
+
state={"known_tiers": list(_delivery_targets.TIERS)},
|
|
15650
|
+
)
|
|
15651
|
+
extras = _delivery_target_extras()
|
|
15652
|
+
listing = _delivery_targets.list_targets(extras, tier=tier)
|
|
15653
|
+
|
|
15654
|
+
if p.get("check_availability"):
|
|
15655
|
+
# Resolve every target against the live matrix so the caller sees what
|
|
15656
|
+
# this machine/license can actually render, not just what ships.
|
|
15657
|
+
formats = _render_formats(proj)
|
|
15658
|
+
codec_cache: Dict[str, Any] = {}
|
|
15659
|
+
for target_id, entry in listing.items():
|
|
15660
|
+
target = _delivery_targets.resolve_target(target_id, extra_targets=extras)
|
|
15661
|
+
format_id = _delivery_targets.select_available(target.format_candidates, formats)
|
|
15662
|
+
entry["available"] = False
|
|
15663
|
+
entry["format_id"] = format_id
|
|
15664
|
+
entry["codec_id"] = None
|
|
15665
|
+
if not format_id:
|
|
15666
|
+
entry["unavailable_reason"] = "format not exposed by this Resolve install"
|
|
15667
|
+
continue
|
|
15668
|
+
if format_id not in codec_cache:
|
|
15669
|
+
codec_cache[format_id] = _render_codecs(proj, format_id, formats)
|
|
15670
|
+
codec_id = _delivery_targets.select_available(target.codec_candidates, codec_cache[format_id])
|
|
15671
|
+
entry["codec_id"] = codec_id
|
|
15672
|
+
if not codec_id:
|
|
15673
|
+
entry["unavailable_reason"] = f"no matching codec for format '{format_id}'"
|
|
15674
|
+
continue
|
|
15675
|
+
entry["available"] = True
|
|
15676
|
+
|
|
15677
|
+
return _ok(
|
|
15678
|
+
targets=listing,
|
|
15679
|
+
schema_version=_delivery_targets.SCHEMA_VERSION,
|
|
15680
|
+
tiers=list(_delivery_targets.TIERS),
|
|
15681
|
+
availability_checked=bool(p.get("check_availability")),
|
|
15682
|
+
)
|
|
15683
|
+
|
|
15684
|
+
|
|
15685
|
+
def _prepare_delivery_job(proj, p: Dict[str, Any]):
|
|
15686
|
+
"""Resolve a named target, then queue a render job for it.
|
|
15687
|
+
|
|
15688
|
+
Target settings are the base; anything in `settings` wins, so a caller can
|
|
15689
|
+
pin TargetDir/CustomName (or override a dimension) without restating the target.
|
|
15690
|
+
"""
|
|
15691
|
+
resolved, err = _resolve_delivery_target_live(proj, p)
|
|
15692
|
+
if err:
|
|
15693
|
+
return err
|
|
15694
|
+
settings = dict(resolved["settings"])
|
|
15695
|
+
settings.update(p.get("settings") or {})
|
|
15696
|
+
prepared = _prepare_render_job(
|
|
15697
|
+
proj,
|
|
15698
|
+
{
|
|
15699
|
+
**{k: v for k, v in p.items() if k not in ("target", "overrides", "settings")},
|
|
15700
|
+
"settings": settings,
|
|
15701
|
+
"format": resolved["format_id"],
|
|
15702
|
+
"codec": resolved["codec_id"],
|
|
15703
|
+
},
|
|
15704
|
+
)
|
|
15705
|
+
if isinstance(prepared, dict) and not prepared.get("error"):
|
|
15706
|
+
prepared["delivery_target"] = resolved["target"]
|
|
15707
|
+
prepared["qc_spec"] = resolved["qc_spec"]
|
|
15708
|
+
return prepared
|
|
15709
|
+
|
|
15710
|
+
|
|
15506
15711
|
def _render_job_lifecycle_probe(proj, p: Dict[str, Any]):
|
|
15507
15712
|
prepared = _prepare_render_job(proj, {**p, "dry_run": False, "require_temp_target": True})
|
|
15508
15713
|
if prepared.get("error") or not prepared.get("success"):
|
|
@@ -15610,6 +15815,16 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
15610
15815
|
quick_export_capabilities() -> {presets, safe_params, guards}
|
|
15611
15816
|
safe_quick_export(preset, target_dir?|params?, custom_name?, dry_run?, allow_render?) -> {success, status}
|
|
15612
15817
|
export_render_boundary_report(include_matrix?, max_pairs?, include_quick_export?) -> {capabilities, settings, matrix?}
|
|
15818
|
+
list_delivery_targets(tier?, check_availability?) -> {targets, tiers, schema_version}
|
|
15819
|
+
resolve_delivery_target(target, overrides?) -> {format_id, codec_id, settings, qc_spec}
|
|
15820
|
+
prepare_delivery_job(target, target_dir, overrides?, settings?, custom_name?, dry_run?) -> {success, job_id, qc_spec}
|
|
15821
|
+
|
|
15822
|
+
Delivery targets are named render intents (`prores422hq_master`, `youtube`,
|
|
15823
|
+
`tiktok`, ...). One definition emits both the Resolve render settings and an
|
|
15824
|
+
ffprobe-shaped QC spec, so the advanced server's `deliverable_qc` can verify a
|
|
15825
|
+
rendered file against the same intent that produced it. Format/codec are
|
|
15826
|
+
resolved against the live matrix, so an intent unavailable on this
|
|
15827
|
+
machine/license fails with the actual available lists.
|
|
15613
15828
|
"""
|
|
15614
15829
|
p = params or {}
|
|
15615
15830
|
_, proj, err = _check()
|
|
@@ -15644,14 +15859,48 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
15644
15859
|
return {"codecs": _render_codecs(proj, p["format"])}
|
|
15645
15860
|
elif action == "get_format_and_codec":
|
|
15646
15861
|
return _ser(proj.GetCurrentRenderFormatAndCodec())
|
|
15862
|
+
elif action == "list_delivery_targets":
|
|
15863
|
+
return _list_delivery_targets(proj, p)
|
|
15864
|
+
elif action == "resolve_delivery_target":
|
|
15865
|
+
_resolved, _target_err = _resolve_delivery_target_live(proj, p)
|
|
15866
|
+
return _target_err if _target_err else _ok(**_resolved)
|
|
15867
|
+
elif action == "prepare_delivery_job":
|
|
15868
|
+
return _prepare_delivery_job(proj, p)
|
|
15647
15869
|
elif action == "set_format_and_codec":
|
|
15648
|
-
|
|
15870
|
+
_formats = _render_formats(proj)
|
|
15871
|
+
_format_id = _render_format_id_from_formats(_formats, p["format"])
|
|
15872
|
+
_codec_id = _render_codec_id(proj, _format_id, p["codec"], _formats)
|
|
15873
|
+
if not proj.SetCurrentRenderFormatAndCodec(_format_id, _codec_id):
|
|
15874
|
+
# Availability is machine/license/plugin dependent, so name what this
|
|
15875
|
+
# install actually exposes instead of just reporting False.
|
|
15876
|
+
return _err(
|
|
15877
|
+
f"Could not set render format/codec: {p['format']} / {p['codec']}",
|
|
15878
|
+
code="RENDER_FORMAT_CODEC_REJECTED",
|
|
15879
|
+
category="invalid_input",
|
|
15880
|
+
reason="Resolve rejected the format/codec pair.",
|
|
15881
|
+
remediation=(
|
|
15882
|
+
"Check render(action='get_formats') and "
|
|
15883
|
+
"render(action='get_codecs', params={'format': ...}). If the codec comes "
|
|
15884
|
+
"from an IO plugin, confirm it is installed and Resolve has been restarted."
|
|
15885
|
+
),
|
|
15886
|
+
state={
|
|
15887
|
+
"requested_format": p["format"],
|
|
15888
|
+
"requested_codec": p["codec"],
|
|
15889
|
+
"resolved_format_id": _format_id,
|
|
15890
|
+
"resolved_codec_id": _codec_id,
|
|
15891
|
+
"available_codecs": _render_codecs(proj, _format_id, _formats),
|
|
15892
|
+
},
|
|
15893
|
+
)
|
|
15894
|
+
return {"success": True, "format_id": _format_id, "codec_id": _codec_id}
|
|
15649
15895
|
elif action == "get_mode":
|
|
15650
15896
|
return {"mode": proj.GetCurrentRenderMode()}
|
|
15651
15897
|
elif action == "set_mode":
|
|
15652
15898
|
return {"success": bool(proj.SetCurrentRenderMode(p["mode"]))}
|
|
15653
15899
|
elif action == "get_resolutions":
|
|
15654
|
-
|
|
15900
|
+
_formats = _render_formats(proj)
|
|
15901
|
+
_format_id = _render_format_id_from_formats(_formats, p["format"])
|
|
15902
|
+
_codec_id = _render_codec_id(proj, _format_id, p["codec"], _formats)
|
|
15903
|
+
return {"resolutions": _ser(proj.GetRenderResolutions(_format_id, _codec_id))}
|
|
15655
15904
|
elif action == "get_settings":
|
|
15656
15905
|
missing = _requires_method(proj, "GetRenderSettings", "unknown")
|
|
15657
15906
|
if missing:
|
package/src/utils/api_truth.py
CHANGED
|
@@ -593,6 +593,49 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
593
593
|
"issue": 90,
|
|
594
594
|
"mitigation": ["_ensure_lut_in_master"],
|
|
595
595
|
},
|
|
596
|
+
{
|
|
597
|
+
"symbol": "Project.GetRenderCodecs",
|
|
598
|
+
"object": "Project",
|
|
599
|
+
"signature": "(renderFormat) -> {codec description: codec name}",
|
|
600
|
+
"reality": "Returns {description: id} — the human-readable description is "
|
|
601
|
+
"the KEY and the id Resolve actually accepts is the VALUE. "
|
|
602
|
+
"SetCurrentRenderFormatAndCodec, GetRenderCodecs and "
|
|
603
|
+
"GetRenderResolutions all require the id, so passing the "
|
|
604
|
+
"description a user sees in the Deliver page is rejected. "
|
|
605
|
+
"Verified live on Studio 19.1.3.7: ('mov', 'Apple ProRes 422 "
|
|
606
|
+
"HQ') -> False while ('mov', 'ProRes422HQ') -> True, and "
|
|
607
|
+
"('mp4', 'H.264') -> False while ('mp4', 'H264') -> True. It "
|
|
608
|
+
"affects every family, not only the ones whose id differs "
|
|
609
|
+
"obviously. Mirrors the same trap in GetRenderFormats, which "
|
|
610
|
+
"returns {format: extension}.",
|
|
611
|
+
"recommended": "Normalize both arguments through the live maps before "
|
|
612
|
+
"calling: src.utils.render_ids.render_format_id_from_formats "
|
|
613
|
+
"and render_codec_id_from_codecs accept a description or an "
|
|
614
|
+
"id and return the id.",
|
|
615
|
+
"tags": ["render", "deliver", "silent-failure", "id-vs-label"],
|
|
616
|
+
"submit": "bug",
|
|
617
|
+
"issue": 59,
|
|
618
|
+
"mitigation": ["_render_format_id", "_render_codec_id"],
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"symbol": "Project.SetCurrentRenderFormatAndCodec",
|
|
622
|
+
"object": "Project",
|
|
623
|
+
"signature": "(format, codec) -> bool",
|
|
624
|
+
"reality": "Some render formats expose NO codecs at all — "
|
|
625
|
+
"GetRenderCodecs('wav') and GetRenderCodecs('gif') both return "
|
|
626
|
+
"{} on Studio 19.1.3.7 — and the call then rejects every codec "
|
|
627
|
+
"value, including the empty string, the format id itself, and "
|
|
628
|
+
"any plausible name ('Linear PCM'). There is no documented way "
|
|
629
|
+
"to select such a format through this API, so an audio-only WAV "
|
|
630
|
+
"deliverable is not expressible in scripting.",
|
|
631
|
+
"recommended": "Check GetRenderCodecs(format) first; when it is empty, "
|
|
632
|
+
"treat the format as unreachable through this API rather "
|
|
633
|
+
"than guessing a codec value. Render audio-only via "
|
|
634
|
+
"ExportVideo=False on a format that does expose codecs, or "
|
|
635
|
+
"drive it from a saved render preset.",
|
|
636
|
+
"tags": ["render", "deliver", "audio", "unsupported"],
|
|
637
|
+
"submit": "missing",
|
|
638
|
+
},
|
|
596
639
|
]
|
|
597
640
|
|
|
598
641
|
|