davinci-resolve-mcp 2.113.0 → 2.115.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/docs/reference/api-limitations.md +9 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +60 -15
- package/resolve-advanced/server/editorial.mjs +8 -4
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +22 -5
- package/resolve-advanced/vendor/drp-format/cut-media.js +56 -7
- package/resolve-advanced/vendor/drp-format/media-timemap.js +8 -4
- package/resolve-advanced/vendor/drp-format/templates/media-clip-r19.drp +0 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +31 -1
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 33 missing capabilities,
|
|
15
|
+
**Totals:** 33 missing capabilities, 44 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -526,10 +526,17 @@ values, or automation-hostile modal prompts.
|
|
|
526
526
|
- **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
|
|
527
527
|
- **Tags:** timeline, import, silent-failure, unreliable-return
|
|
528
528
|
|
|
529
|
+
### Audio tracks cannot be grown in an imported timeline; Fairlight strips live in the pool Sm2Sequence.FieldsBlob
|
|
530
|
+
|
|
531
|
+
- **Object:** `Sm2TiTrack (audio) / FLStudioModelBA`
|
|
532
|
+
- **Behavior:** An audio track added to a SeqContainer by cloning imports fine, reads back fine (track count, items, everything), and renders SILENT. The per-timeline Fairlight model (FLStudioModelBA, ~7KB compressed to ~420KB, inside the media pool's Sm2Sequence.FieldsBlob) holds one strip per audio track, and a track without a strip is mute. Measured by elimination on 19.1.3.7: clip byte-identical to a live-authored one, track byte-identical (SubType is the CHANNEL FORMAT code - 1=mono - not an ordinal), pool entry shared with a playing A1 clip - still silent; only a timeline whose template was CAPTURED with the tracks plays.
|
|
533
|
+
- **Workaround / current handling:** Never clone audio tracks offline. Capture the template with the audio tracks already present (the r19 media template carries 8 mono tracks with valid strips; drt.assemble audioOnly cuts land on them and render at native level). Refuse placements beyond the captured ceiling. Audio aliveness is readback-blind: verify by rendered RMS, not structure.
|
|
534
|
+
- **Tags:** fairlight, audio, import, silent-failure, drt
|
|
535
|
+
|
|
529
536
|
### MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form
|
|
530
537
|
|
|
531
538
|
- **Object:** `Sm2TimeMap (per-clip retime blob)`
|
|
532
|
-
- **Behavior:** Resolve 21 encodes a retimed clip's KeyframesBA as protobuf points; Resolve 19.1.3 encodes it as a keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the protobuf form — the clip reads back and plays at 100% with no warning (measured: identical timelines, one per form; protobuf → source 0..96 over 96 frames, keyed → source 0..48 over 96 frames and a live 50% render). The map spans the WHOLE source stretched by 1/speed; the clip's <In>/<Duration> window into it in RECORD frames (srcIn converts by /speed).
|
|
539
|
+
- **Behavior:** Resolve 21 encodes a retimed clip's KeyframesBA as protobuf points; Resolve 19.1.3 encodes it as a keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the protobuf form — the clip reads back and plays at 100% with no warning (measured: identical timelines, one per form; protobuf → source 0..96 over 96 frames, keyed → source 0..48 over 96 frames and a live 50% render). The map spans the WHOLE source stretched by 1/speed; the clip's <In>/<Duration> window into it in RECORD frames (srcIn converts by /speed). REVERSE is the same envelope with the Y endpoints swapped - kf0=(0,YMax), kf1=(XMax,0) - and In then measures from the source END: (frames - srcIn - dur*speed)/speed (measured: a reversed srcIn-24 dur-48 cut reads back source 71->23).
|
|
533
540
|
- **Workaround / current handling:** Author retimes for pre-21 hosts with the keyed form (drt.assemble cuts[].speed does this; encoder byte-exact against a live 19.1.3.7 harvest). Treat any cross-generation timemap as unverified until a readback shows the retimed source range.
|
|
534
541
|
- **Tags:** retime, import, silent-failure, drt
|
|
535
542
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.115.0"
|
|
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
|
@@ -202,13 +202,14 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
202
202
|
* timeline runs 24fps with origin 86400, so rec/src frames convert as
|
|
203
203
|
* round(frames × 24 / nominalFps). Placement anchors the EARLIEST video
|
|
204
204
|
* event at the origin. Honesty ledger in the returned report: flattened
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* reason), authored vs
|
|
205
|
+
* zero-speed freezes (constant retimes — forward AND reverse — are AUTHORED
|
|
206
|
+
* as real Sm2TimeMaps, r19 keyed form, readback/render-verified), authored vs
|
|
208
207
|
* dropped transitions (cross-dissolves are AUTHORED when the predecessor
|
|
209
208
|
* abuts the cut and both sides have handle media — render-verified on
|
|
210
209
|
* 19.1.3.7; otherwise dropped with the reason, as a cut at the boundary),
|
|
211
|
-
* and
|
|
210
|
+
* and audio: A-track events are AUTHORED as audioOnly cuts on their own
|
|
211
|
+
* audio tracks (render-verified; the template carries 8 Fairlight-valid
|
|
212
|
+
* audio tracks) — their presence suppresses the A1 convenience mirror.
|
|
212
213
|
*
|
|
213
214
|
* @param {Array} events - normalized events (parseInterchange shape)
|
|
214
215
|
* @param {object} opts
|
|
@@ -227,11 +228,13 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
227
228
|
throw new TypeError('eventsToAssembleSpec: sourceMap {reel: {mediaFilePath, spec}} is required');
|
|
228
229
|
}
|
|
229
230
|
const ORIGIN = 86400;
|
|
230
|
-
const
|
|
231
|
-
const
|
|
231
|
+
const isAudio = (t) => /^A\d*$/.test(String(t || ''));
|
|
232
|
+
const vids = events.filter((e) => !isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
233
|
+
const auds = events.filter((e) => isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
234
|
+
const audioSkipped = events.length - vids.length - auds.length;
|
|
232
235
|
if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
|
|
233
236
|
|
|
234
|
-
const unmapped = [...new Set(vids.map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
|
|
237
|
+
const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
|
|
235
238
|
if (unmapped.length) {
|
|
236
239
|
throw new Error(
|
|
237
240
|
`eventsToAssembleSpec: unmapped source reel(s): ${unmapped.join(', ')} — ` +
|
|
@@ -258,15 +261,17 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
258
261
|
const vTrack = trackNum(e.track);
|
|
259
262
|
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
|
|
260
263
|
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: '
|
|
264
|
+
const spd = Math.abs(e.speed ?? 100);
|
|
265
|
+
if (!(spd > 0)) {
|
|
266
|
+
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'zero speed (freeze) not supported — played forward at 100%' });
|
|
264
267
|
} else {
|
|
265
|
-
//
|
|
266
|
-
// (r19 keyed form; render
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
// Constant speed, forward or reverse: authored as a real Sm2TimeMap
|
|
269
|
+
// on the cut (r19 keyed form; readback/render-verified on 19.1.3.7 —
|
|
270
|
+
// reverse reads back source 71→23 for a srcIn-24 dur-48 cut). Audio
|
|
271
|
+
// for retimed cuts is video-only downstream.
|
|
272
|
+
if (spd !== 100) cut.speed = spd / 100;
|
|
273
|
+
if (e.reverse) cut.reverse = true;
|
|
274
|
+
authoredRetimes.push({ index: e.index, source: e.source, speed: spd, ...(e.reverse ? { reverse: true } : {}) });
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
if (e.transition) {
|
|
@@ -286,6 +291,44 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
286
291
|
perSource.get(e.source).push(cut);
|
|
287
292
|
}
|
|
288
293
|
|
|
294
|
+
// Audio events become explicit audioOnly cuts on their own audio tracks —
|
|
295
|
+
// this SUPPRESSES the convenience A1 mirror downstream (explicit audio
|
|
296
|
+
// wins). Render-verified on 19.1.3.7: an offline A3 cut plays at the
|
|
297
|
+
// native control level through the captured 8-audio-track template's
|
|
298
|
+
// Fairlight strips. Retimed audio is not authored (no audio timemap yet).
|
|
299
|
+
const audioTrackNum = (t) => { const m = /^A(\d+)?$/.exec(String(t)); return m && m[1] ? parseInt(m[1], 10) : 1; };
|
|
300
|
+
const audioPlacements = [];
|
|
301
|
+
const audioRetimesSkipped = [];
|
|
302
|
+
for (const e of auds) {
|
|
303
|
+
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
304
|
+
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
305
|
+
const durationFrames = recOut - recIn;
|
|
306
|
+
if (durationFrames <= 0) continue;
|
|
307
|
+
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
308
|
+
audioRetimesSkipped.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'audio retime not supported — played at 100%' });
|
|
309
|
+
}
|
|
310
|
+
const track = audioTrackNum(e.track);
|
|
311
|
+
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), audioOnly: true, track };
|
|
312
|
+
audioPlacements.push({ start: recIn, end: recOut, index: e.index, track });
|
|
313
|
+
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
314
|
+
perSource.get(e.source).push(cut);
|
|
315
|
+
}
|
|
316
|
+
audioPlacements.sort((a, b) => a.start - b.start);
|
|
317
|
+
const audByTrack = new Map();
|
|
318
|
+
for (const pl of audioPlacements) {
|
|
319
|
+
if (!audByTrack.has(pl.track)) audByTrack.set(pl.track, []);
|
|
320
|
+
audByTrack.get(pl.track).push(pl);
|
|
321
|
+
}
|
|
322
|
+
for (const [trk, pls] of audByTrack) {
|
|
323
|
+
for (let i = 1; i < pls.length; i += 1) {
|
|
324
|
+
if (pls[i].start < pls[i - 1].end) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`eventsToAssembleSpec: audio events ${pls[i - 1].index} and ${pls[i].index} ` +
|
|
327
|
+
`overlap on audio track ${trk} after frame conversion — one track cannot hold both.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
289
332
|
// Overlap is judged PER VIDEO TRACK — V2 stacking over V1 is legitimate
|
|
290
333
|
// conform geometry (render-verified: an upper-track clip covers the lower).
|
|
291
334
|
placements.sort((a, b) => a.start - b.start);
|
|
@@ -348,6 +391,8 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
348
391
|
videoEvents: vids.length,
|
|
349
392
|
sources: media.length,
|
|
350
393
|
audioEventsSkipped: audioSkipped,
|
|
394
|
+
authoredAudioEvents: audioPlacements.length,
|
|
395
|
+
audioRetimesSkipped,
|
|
351
396
|
upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1).length,
|
|
352
397
|
flattenedRetimes,
|
|
353
398
|
authoredRetimes,
|
|
@@ -91,7 +91,9 @@ export function parseEDL(text, opts = {}) {
|
|
|
91
91
|
const head = tokens.slice(0, tokens.indexOf(tcs[tcs.length - 4]));
|
|
92
92
|
const [eventNum, reel, channel, transition] = head;
|
|
93
93
|
const dur = transition && transition !== 'C' ? Number(head[4]) : 0;
|
|
94
|
-
const
|
|
94
|
+
const chan = String(channel || '');
|
|
95
|
+
const am = /^A(\d+)?$/i.exec(chan);
|
|
96
|
+
const track = am && !/V/i.test(chan) ? (am[1] && am[1] !== '1' ? `A${am[1]}` : 'A') : (/A/i.test(chan) && !/V/i.test(chan) ? 'A' : 'V');
|
|
95
97
|
events.push(
|
|
96
98
|
evt({
|
|
97
99
|
index: Number(eventNum),
|
|
@@ -117,11 +119,13 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
117
119
|
const events = [];
|
|
118
120
|
let idx = 1;
|
|
119
121
|
let vNum = 0;
|
|
122
|
+
let aNum = 0;
|
|
120
123
|
for (const track of tracks) {
|
|
121
124
|
const isAudio = track.kind === 'Audio';
|
|
122
|
-
if (
|
|
123
|
-
// First
|
|
124
|
-
|
|
125
|
+
if (isAudio) aNum += 1; else vNum += 1;
|
|
126
|
+
// First track of each kind keeps the bare letter (compat); higher tracks
|
|
127
|
+
// are numbered ('V2', 'A2', …).
|
|
128
|
+
const kind = isAudio ? (aNum === 1 ? 'A' : `A${aNum}`) : (vNum === 1 ? 'V' : `V${vNum}`);
|
|
125
129
|
let rec = 0;
|
|
126
130
|
for (const child of track.children || []) {
|
|
127
131
|
const schema = child.OTIO_SCHEMA || '';
|
|
@@ -46,7 +46,7 @@ const assembleSchema = z.object({
|
|
|
46
46
|
spec: z
|
|
47
47
|
.object({})
|
|
48
48
|
.passthrough()
|
|
49
|
-
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed? (
|
|
49
|
+
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
50
50
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
51
51
|
targetAppVersion: z
|
|
52
52
|
.union([z.string(), z.number()])
|
|
@@ -114,19 +114,36 @@ async function assembleTimeline(spec = {}) {
|
|
|
114
114
|
sources.forEach((src, i) => {
|
|
115
115
|
(src.cuts || []).forEach((cut) => {
|
|
116
116
|
const out = mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut };
|
|
117
|
-
if (cut.
|
|
117
|
+
if (cut.audioOnly) {
|
|
118
|
+
out.srcMeta = {
|
|
119
|
+
name: src.mediaFilePath.split('/').pop(),
|
|
120
|
+
mediaFilePath: src.mediaFilePath,
|
|
121
|
+
fps: Math.round(src.spec.fps || 24),
|
|
122
|
+
frameCount: src.spec.frameCount,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (cut.reverse || (cut.speed !== undefined && cut.speed !== 1)) {
|
|
118
126
|
// Constant-speed retime (forward only). The Sm2TimeMap spans the
|
|
119
127
|
// whole source stretched by 1/speed; the clip windows into it with
|
|
120
128
|
// RECORD-domain In/Duration (measured live on 19.1.3.7), so the
|
|
121
129
|
// source-domain srcIn converts by /speed here.
|
|
122
|
-
|
|
130
|
+
const speed = cut.speed ?? 1;
|
|
131
|
+
if (!(speed > 0)) throw new RangeError('assembleTimeline: cut.speed must be > 0 (use cut.reverse for backwards)');
|
|
123
132
|
const fps = Math.round(src.spec.fps || 24);
|
|
124
133
|
out.timemap = buildConstantSpeedTimemapKeyed({
|
|
125
|
-
speed
|
|
126
|
-
uniqueId: randomUUID(),
|
|
134
|
+
speed, sourceFrames: src.spec.frameCount, fps,
|
|
135
|
+
uniqueId: randomUUID(), reverse: !!cut.reverse,
|
|
127
136
|
}).toString('hex');
|
|
128
|
-
|
|
137
|
+
// Record-domain In. Forward: srcIn/speed. Reverse: the clip plays
|
|
138
|
+
// source [srcIn, srcIn + dur*speed) BACKWARDS — the map descends
|
|
139
|
+
// from the source tail, so In measures from the END:
|
|
140
|
+
// (sourceFrames - srcIn - dur*speed)/speed (measured live: In=0
|
|
141
|
+
// on a reversed full-speed clip reads back source 191→143).
|
|
142
|
+
out.srcIn = cut.reverse
|
|
143
|
+
? Math.round((src.spec.frameCount - (cut.srcIn ?? 0) - cut.durationFrames * speed) / speed)
|
|
144
|
+
: Math.round((cut.srcIn ?? 0) / speed);
|
|
129
145
|
delete out.speed;
|
|
146
|
+
delete out.reverse;
|
|
130
147
|
}
|
|
131
148
|
allCuts.push(out);
|
|
132
149
|
});
|
|
@@ -38,6 +38,10 @@ const { clipDbId, setClipStart, setClipDuration, setClipIn } = require('./splice
|
|
|
38
38
|
* track is the 1-based VIDEO track (default 1); missing tracks are grown as
|
|
39
39
|
* empty clones. Cuts on track > 1 are placed VIDEO-ONLY — their audio would
|
|
40
40
|
* overlap the track-1 cuts' audio on A1 (the template has a single A1).
|
|
41
|
+
* audioOnly:true makes a cut an AUDIO clip placement instead (track then
|
|
42
|
+
* indexes AUDIO tracks, grown as needed) — and the presence of ANY
|
|
43
|
+
* audioOnly cut suppresses the implicit A1 mirror of track-1 video cuts
|
|
44
|
+
* (explicit audio wins over the convenience mirror).
|
|
41
45
|
* @param {string} [opts.timelineUuid]
|
|
42
46
|
* @returns {Promise<{buffer: Buffer, cutCount: number, clipDbIds: string[]}>}
|
|
43
47
|
*/
|
|
@@ -79,6 +83,26 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
79
83
|
// element instead of the donor's.
|
|
80
84
|
c = c.replace(/<MediaRef>[0-9a-f-]{36}<\/MediaRef>/, `<MediaRef>${cut.mediaRef}</MediaRef>`);
|
|
81
85
|
}
|
|
86
|
+
if (cut.audioOnly && cut.srcMeta) {
|
|
87
|
+
// An audio-only clone must carry ITS OWN source identity — a live
|
|
88
|
+
// A2-placed clip (harvested via AppendToTimeline mediaType:2) differs
|
|
89
|
+
// from a donor clone in Name, MediaFilePath, MediaFrameRate (the
|
|
90
|
+
// template donor's was 29.97!), the identity MediaTimemapBA extent,
|
|
91
|
+
// and FieldsBlob. A clone keeping donor values imports and reads back
|
|
92
|
+
// fine but renders SILENT off A1.
|
|
93
|
+
const { name, mediaFilePath, fps, frameCount } = cut.srcMeta;
|
|
94
|
+
c = c.replace(/<Name>[\s\S]*?<\/Name>/, `<Name>${name}</Name>`);
|
|
95
|
+
c = c.replace(/<MediaFilePath>[\s\S]*?<\/MediaFilePath>/, `<MediaFilePath>${mediaFilePath}</MediaFilePath>`);
|
|
96
|
+
const rate = Buffer.alloc(16);
|
|
97
|
+
rate.writeDoubleLE(fps, 0);
|
|
98
|
+
c = c.replace(/<MediaFrameRate>[0-9a-fA-F]*<\/MediaFrameRate>/, `<MediaFrameRate>${rate.toString('hex')}</MediaFrameRate>`);
|
|
99
|
+
const idm = Buffer.alloc(9);
|
|
100
|
+
idm.writeUInt8(0x02, 0);
|
|
101
|
+
idm.writeDoubleBE((frameCount - 1) / fps, 1);
|
|
102
|
+
c = c.replace(/<MediaTimemapBA>[0-9a-fA-F]*<\/MediaTimemapBA>/, `<MediaTimemapBA>${idm.toString('hex')}</MediaTimemapBA>`);
|
|
103
|
+
// Generic audio-clip FieldsBlob, verbatim from the live A2 harvest.
|
|
104
|
+
c = c.replace(/<FieldsBlob>[0-9a-fA-F]*<\/FieldsBlob>/, '<FieldsBlob>0000000200000005800a022001</FieldsBlob>');
|
|
105
|
+
}
|
|
82
106
|
if (cut.timemap) {
|
|
83
107
|
// Constant-speed retime: swap the identity MediaTimemapBA for the
|
|
84
108
|
// caller-built Sm2TimeMap (r19 keyed form render/readback-verified;
|
|
@@ -96,19 +120,44 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
96
120
|
if (!clips.length) continue; // audio-less media: nothing to cut on A1
|
|
97
121
|
const donor = clips[0];
|
|
98
122
|
if (trackType === 'audio') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
const audioCuts = cuts.filter((cut) => cut.audioOnly);
|
|
124
|
+
if (audioCuts.length) {
|
|
125
|
+
// Explicit audio placements: grow audio tracks and place per track;
|
|
126
|
+
// the implicit A1 mirror is suppressed (explicit audio wins).
|
|
127
|
+
const maxA = Math.max(1, ...audioCuts.map((cut) => cut.track ?? 1));
|
|
128
|
+
if (maxA > tracks.length) {
|
|
129
|
+
// Audio tracks CANNOT be grown by cloning: the per-timeline
|
|
130
|
+
// Fairlight model (FLStudioModelBA inside the pool's
|
|
131
|
+
// Sm2Sequence.FieldsBlob) holds one strip per track, and a cloned
|
|
132
|
+
// track without a strip imports fine, reads back fine, and renders
|
|
133
|
+
// SILENT (measured). The r19 media template is captured with 8
|
|
134
|
+
// audio tracks (valid strips included); beyond that, refuse.
|
|
135
|
+
throw new Error(
|
|
136
|
+
`cutSourceIntoClips: audio track ${maxA} exceeds the template's ${tracks.length} ` +
|
|
137
|
+
'audio tracks — audio tracks cannot be grown offline (no Fairlight strip = silent). ' +
|
|
138
|
+
'Re-capture a template with more audio tracks to raise the ceiling.');
|
|
139
|
+
}
|
|
140
|
+
for (let t = 1; t <= tracks.length; t += 1) {
|
|
141
|
+
const mine = audioCuts.filter((cut) => (cut.track ?? 1) === t);
|
|
142
|
+
if (t > 1 && !mine.length) continue;
|
|
143
|
+
tracks[t - 1] = setItemsInner(tracks[t - 1], mine.map((cut) => cloneCut(donor, cut)).join(''));
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// A1 mirrors track-1 video cuts only; higher video tracks stay
|
|
147
|
+
// video-only, and so do RETIMED cuts (the audio clone would need its
|
|
148
|
+
// own timemap and pitch handling — video-only is stated, not silent).
|
|
149
|
+
const a1 = cuts.filter((cut) => (cut.track ?? 1) === 1 && !cut.timemap).map((cut) => cloneCut(donor, cut));
|
|
150
|
+
tracks[0] = setItemsInner(tracks[0], a1.join(''));
|
|
151
|
+
}
|
|
104
152
|
xml = replaceTrackVec(xml, trackType, match, tracks);
|
|
105
153
|
continue;
|
|
106
154
|
}
|
|
107
|
-
const
|
|
155
|
+
const vCuts = cuts.filter((cut) => !cut.audioOnly);
|
|
156
|
+
const maxTrack = Math.max(1, ...vCuts.map((cut) => cut.track ?? 1));
|
|
108
157
|
const cloneSource = tracks[0];
|
|
109
158
|
while (tracks.length < maxTrack) tracks.push(emptyTrackClone(cloneSource));
|
|
110
159
|
for (let t = 1; t <= tracks.length; t += 1) {
|
|
111
|
-
const mine =
|
|
160
|
+
const mine = vCuts.filter((cut) => (cut.track ?? 1) === t);
|
|
112
161
|
if (t > 1 && !mine.length) continue; // grown-empty or untouched track keeps its items
|
|
113
162
|
const clones = mine.map((cut) => cloneCut(donor, cut));
|
|
114
163
|
for (const c of clones) clipDbIds.push(clipDbId(c));
|
|
@@ -227,8 +227,8 @@ function buildConstantSpeedTimemap({ speed, sourceDurationSec, uniqueId, recordD
|
|
|
227
227
|
* @param {string} p.uniqueId - fresh uuid (bare, no braces).
|
|
228
228
|
* @returns {Buffer}
|
|
229
229
|
*/
|
|
230
|
-
function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueId }) {
|
|
231
|
-
if (!(speed > 0)) throw new RangeError('buildConstantSpeedTimemapKeyed: speed must be > 0 (reverse
|
|
230
|
+
function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueId, reverse = false }) {
|
|
231
|
+
if (!(speed > 0)) throw new RangeError('buildConstantSpeedTimemapKeyed: speed must be > 0 (pass reverse:true for backwards)');
|
|
232
232
|
if (!Number.isInteger(sourceFrames) || sourceFrames < 1) throw new TypeError('buildConstantSpeedTimemapKeyed: sourceFrames must be a positive integer');
|
|
233
233
|
const YMax = (sourceFrames - 1) / fps;
|
|
234
234
|
const XMax = (sourceFrames / speed - 1) / fps;
|
|
@@ -241,9 +241,13 @@ function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueI
|
|
|
241
241
|
{ key: 'XIn', type: T_DOUBLE, subType: 0, value: 0 },
|
|
242
242
|
{ key: 'X', type: T_DOUBLE, subType: 0, value: X },
|
|
243
243
|
] }).toString('hex');
|
|
244
|
+
// Reverse (harvested from a live 19.1.3.7 -100% XMEML retime): the SAME
|
|
245
|
+
// envelope with the Y endpoints swapped — kf0=(0, YMax), kf1=(XMax, 0),
|
|
246
|
+
// a descending line. Forward keeps kf1 Y = XMax*speed (harvest: +half-frame
|
|
247
|
+
// convention falls out of the arithmetic, byte-exact either way).
|
|
244
248
|
const keyframes = encodeKeyedDict({ hdr: 1, entries: [
|
|
245
|
-
{ key: '1', type: T_BYTES, subType: 0, value: kf(XMax, XMax * speed) },
|
|
246
|
-
{ key: '0', type: T_BYTES, subType: 0, value: kf(0, 0) },
|
|
249
|
+
{ key: '1', type: T_BYTES, subType: 0, value: reverse ? kf(XMax, 0) : kf(XMax, XMax * speed) },
|
|
250
|
+
{ key: '0', type: T_BYTES, subType: 0, value: reverse ? kf(0, YMax) : kf(0, 0) },
|
|
247
251
|
] }).toString('hex');
|
|
248
252
|
return encodeKeyedDict({ hdr: 1, entries: [
|
|
249
253
|
{ key: 'YMax', type: T_DOUBLE, subType: 0, value: YMax },
|
|
Binary file
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.115.0"
|
|
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()}")
|
package/src/server.py
CHANGED
package/src/utils/api_truth.py
CHANGED
|
@@ -1914,6 +1914,32 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1914
1914
|
"structural readback cannot see this class.",
|
|
1915
1915
|
"tags": ["media-pool", "import", "render", "silent-failure", "readback"],
|
|
1916
1916
|
},
|
|
1917
|
+
{
|
|
1918
|
+
"symbol": "Audio tracks cannot be grown in an imported timeline; Fairlight strips live in the pool Sm2Sequence.FieldsBlob",
|
|
1919
|
+
"object": "Sm2TiTrack (audio) / FLStudioModelBA",
|
|
1920
|
+
"reality": "An audio track added to a SeqContainer by cloning "
|
|
1921
|
+
"imports fine, reads back fine (track count, items, "
|
|
1922
|
+
"everything), and renders SILENT. The per-timeline "
|
|
1923
|
+
"Fairlight model (FLStudioModelBA, ~7KB compressed to "
|
|
1924
|
+
"~420KB, inside the media pool's Sm2Sequence.FieldsBlob) "
|
|
1925
|
+
"holds one strip per audio track, and a track without a "
|
|
1926
|
+
"strip is mute. Measured by elimination on 19.1.3.7: "
|
|
1927
|
+
"clip byte-identical to a live-authored one, track "
|
|
1928
|
+
"byte-identical (SubType is the CHANNEL FORMAT code - "
|
|
1929
|
+
"1=mono - not an ordinal), pool entry shared with a "
|
|
1930
|
+
"playing A1 clip - still silent; only a timeline whose "
|
|
1931
|
+
"template was CAPTURED with the tracks plays.",
|
|
1932
|
+
"recommended": "Never clone audio tracks offline. Capture the "
|
|
1933
|
+
"template with the audio tracks already present "
|
|
1934
|
+
"(the r19 media template carries 8 mono tracks with "
|
|
1935
|
+
"valid strips; drt.assemble audioOnly cuts land on "
|
|
1936
|
+
"them and render at native level). Refuse placements "
|
|
1937
|
+
"beyond the captured ceiling. Audio aliveness is "
|
|
1938
|
+
"readback-blind: verify by rendered RMS, not "
|
|
1939
|
+
"structure.",
|
|
1940
|
+
"tags": ["fairlight", "audio", "import", "silent-failure", "drt"],
|
|
1941
|
+
"submit": "bug",
|
|
1942
|
+
},
|
|
1917
1943
|
{
|
|
1918
1944
|
"symbol": "MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form",
|
|
1919
1945
|
"object": "Sm2TimeMap (per-clip retime blob)",
|
|
@@ -1927,7 +1953,11 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1927
1953
|
"source 0..48 over 96 frames and a live 50% render). "
|
|
1928
1954
|
"The map spans the WHOLE source stretched by 1/speed; "
|
|
1929
1955
|
"the clip's <In>/<Duration> window into it in RECORD "
|
|
1930
|
-
"frames (srcIn converts by /speed)."
|
|
1956
|
+
"frames (srcIn converts by /speed). REVERSE is the same "
|
|
1957
|
+
"envelope with the Y endpoints swapped - kf0=(0,YMax), "
|
|
1958
|
+
"kf1=(XMax,0) - and In then measures from the source "
|
|
1959
|
+
"END: (frames - srcIn - dur*speed)/speed (measured: a "
|
|
1960
|
+
"reversed srcIn-24 dur-48 cut reads back source 71->23).",
|
|
1931
1961
|
"recommended": "Author retimes for pre-21 hosts with the keyed "
|
|
1932
1962
|
"form (drt.assemble cuts[].speed does this; encoder "
|
|
1933
1963
|
"byte-exact against a live 19.1.3.7 harvest). Treat "
|