davinci-resolve-mcp 3.3.0 → 3.4.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 +39 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/control-panel.md +13 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +175 -7
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
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 v3.4.0 — review a bin one frame at a time in the control panel
|
|
6
|
+
|
|
7
|
+
Contributed by @tpellet (#230), their first contribution here.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Serial source review in the control panel.** Reviewing a bin meant opening
|
|
12
|
+
each clip separately. The review bin now has an **Enlarge / review** button
|
|
13
|
+
per card that opens a full-size frame with previous/next navigation, so a bin
|
|
14
|
+
is walked once rather than clicked through a card at a time.
|
|
15
|
+
- **Include / Exclude / Unreviewed per clip, and an independent star rating.**
|
|
16
|
+
Keyboard-driven — `I`, `X`, `U` and the arrow keys — with Include and Exclude
|
|
17
|
+
advancing automatically. Selection and rating are separate fields: rating a
|
|
18
|
+
clip does not decide it, and excluding one does not discard its rating.
|
|
19
|
+
- **A selection filter** — all / non-excluded / excluded — alongside the
|
|
20
|
+
existing bin filter, and a selection chip on every card, so the state of a
|
|
21
|
+
pass is visible without opening anything.
|
|
22
|
+
- **Decisions survive a reload.** Both fields go through the existing
|
|
23
|
+
correction store, so they persist the way clip notes already did and are
|
|
24
|
+
visible to everything else that reads corrections. Notes are untouched.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Review thumbnails are letterboxed rather than cropped** (`object-fit:
|
|
29
|
+
cover` → `contain`). A cropped thumbnail hides exactly what a source review
|
|
30
|
+
is for: framing, headroom, and what is at the edges of frame.
|
|
31
|
+
|
|
32
|
+
### Gating
|
|
33
|
+
|
|
34
|
+
- **Every save is verified, not assumed.** The panel re-reads the clip after
|
|
35
|
+
each write and refuses to advance if the value it reads back is not the one
|
|
36
|
+
it sent, so a failed save cannot be walked past. `apply_clip_correction`
|
|
37
|
+
validates server-side as well: `user.selection` must be one of the three
|
|
38
|
+
literals, and `user.rating` must be an `int` from 0 to 5 — `type(value) is
|
|
39
|
+
not int` deliberately, so a JSON `true` is rejected rather than silently
|
|
40
|
+
stored as a rating of 1.
|
|
41
|
+
- **No source media is touched and no Resolve edit is made.** Previews are
|
|
42
|
+
analyzed frames that already exist on disk; there is no conversion step.
|
|
43
|
+
|
|
5
44
|
## What's New in v3.3.0 — ask the server what the native Resolve API contains
|
|
6
45
|
|
|
7
46
|
Contributed by @legionsound (#229), the second of the two branches queued in #207.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v3.
|
|
15
|
+
> 本翻译对应 v3.4.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -101,6 +101,19 @@ detection has run (`detect_entities` + a one-frame-per-cluster confirmation
|
|
|
101
101
|
in chat), a `Recurring across this bin` card lists the labeled people,
|
|
102
102
|
places, and objects with their shot counts.
|
|
103
103
|
|
|
104
|
+
Use **Enlarge / review** on a card to inspect its complete analyzed frame without
|
|
105
|
+
cropping. Previous/Next (or arrow keys) follow the visible bin order. **Include**
|
|
106
|
+
(`I`) and **Exclude** (`X`) save the selection and advance after readback;
|
|
107
|
+
**Unreviewed** (`U`) clears the decision without advancing. Ratings save independently
|
|
108
|
+
and do not clear exclusions. The selection filter can show all, non-excluded,
|
|
109
|
+
or excluded clips; it applies to the bin browser, not search results.
|
|
110
|
+
|
|
111
|
+
Selections are stored as `user.selection` corrections alongside existing notes
|
|
112
|
+
and ratings. They express review preferences, not automatic timeline edits.
|
|
113
|
+
Previews use existing analyzed frames, so their resolution depends on the analysis;
|
|
114
|
+
this does not generate proxies or stream original media. Existing clip detail
|
|
115
|
+
controls remain available for notes and metadata edits.
|
|
116
|
+
|
|
104
117
|
### Clip detail
|
|
105
118
|
|
|
106
119
|

|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "3.
|
|
40
|
+
VERSION = "3.4.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
|
@@ -1658,7 +1658,7 @@ HTML = r"""<!doctype html>
|
|
|
1658
1658
|
aspect-ratio: 16 / 9;
|
|
1659
1659
|
background: var(--lab-workspace-letterbox);
|
|
1660
1660
|
border-radius: var(--radius-sm);
|
|
1661
|
-
object-fit:
|
|
1661
|
+
object-fit: contain;
|
|
1662
1662
|
display: block;
|
|
1663
1663
|
}
|
|
1664
1664
|
.review-thumb.placeholder {
|
|
@@ -4265,6 +4265,11 @@ HTML = r"""<!doctype html>
|
|
|
4265
4265
|
<select id="reviewBinFilter" aria-label="Filter by bin">
|
|
4266
4266
|
<option value="">All bins</option>
|
|
4267
4267
|
</select>
|
|
4268
|
+
<select id="sourceSelectionFilter" aria-label="Filter by source selection">
|
|
4269
|
+
<option value="all">All selections</option>
|
|
4270
|
+
<option value="non-excluded">Non-excluded</option>
|
|
4271
|
+
<option value="excluded">Excluded</option>
|
|
4272
|
+
</select>
|
|
4268
4273
|
<div class="review-view-toggle" role="tablist" aria-label="View mode">
|
|
4269
4274
|
<button id="reviewViewGridBtn" class="active" data-view-mode="grid" type="button">Grid</button>
|
|
4270
4275
|
<button id="reviewViewListBtn" data-view-mode="list" type="button">List</button>
|
|
@@ -8822,11 +8827,159 @@ HTML = r"""<!doctype html>
|
|
|
8822
8827
|
sorted.map(b => `<option value="${escapeHtml(b)}" ${b === current ? 'selected' : ''}>${escapeHtml(b)}</option>`).join('');
|
|
8823
8828
|
}
|
|
8824
8829
|
|
|
8830
|
+
let sourceSelectionFilter = 'all';
|
|
8831
|
+
function matchesSourceSelection(clip) {
|
|
8832
|
+
const selection = clip.user_selection || 'Unreviewed';
|
|
8833
|
+
return sourceSelectionFilter === 'all'
|
|
8834
|
+
|| (sourceSelectionFilter === 'excluded' ? selection === 'Exclude' : selection !== 'Exclude');
|
|
8835
|
+
}
|
|
8836
|
+
|
|
8837
|
+
// Source preview reviews the current bin/filter order using persisted corrections.
|
|
8838
|
+
const sourcePreview = document.createElement('dialog');
|
|
8839
|
+
sourcePreview.setAttribute('aria-label', 'Source preview');
|
|
8840
|
+
sourcePreview.style.cssText = 'max-width:96vw;max-height:96vh;background:var(--bg-elevated-1);color:var(--text-primary)';
|
|
8841
|
+
sourcePreview.innerHTML = `<button type="button" data-preview-close>Close</button>
|
|
8842
|
+
<h3 data-preview-title></h3><div data-preview-controls>
|
|
8843
|
+
<button type="button" data-preview-step="-1">← Previous</button>
|
|
8844
|
+
<button type="button" data-preview-selection="Include">Include & next (I)</button>
|
|
8845
|
+
<button type="button" data-preview-selection="Exclude">Exclude & next (X)</button>
|
|
8846
|
+
<button type="button" data-preview-selection="Unreviewed">Unreviewed (U)</button>
|
|
8847
|
+
<button type="button" data-preview-step="1">Next →</button>
|
|
8848
|
+
<span>Rating:</span>${[0,1,2,3,4,5].map(n => `<button type="button" data-preview-rating="${n}" aria-label="${n ? n + ' stars' : 'Clear stars'}">${n ? '★'.repeat(n) : 'Clear'}</button>`).join('')}
|
|
8849
|
+
</div><p data-preview-status role="status"></p>
|
|
8850
|
+
<img data-preview-image alt="" style="display:block;max-width:90vw;max-height:70vh;object-fit:contain">`;
|
|
8851
|
+
document.body.append(sourcePreview);
|
|
8852
|
+
const previewImage = sourcePreview.querySelector('[data-preview-image]');
|
|
8853
|
+
const previewStatus = sourcePreview.querySelector('[data-preview-status]');
|
|
8854
|
+
let previewQueue = [], previewIndex = -1, previewBusy = false, previewReady = false, previewGeneration = 0;
|
|
8855
|
+
|
|
8856
|
+
function lockSourcePreview(busy) {
|
|
8857
|
+
previewBusy = busy;
|
|
8858
|
+
sourcePreview.querySelectorAll('[data-preview-controls] button').forEach(button => {
|
|
8859
|
+
button.disabled = busy || (!button.dataset.previewStep && !previewReady);
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
|
|
8863
|
+
function displaySourceReview(data, clip) {
|
|
8864
|
+
const selection = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.selection') || 'Unreviewed';
|
|
8865
|
+
const rating = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.rating') || 0;
|
|
8866
|
+
clip.user_selection = selection;
|
|
8867
|
+
sourcePreview.querySelectorAll('[data-preview-rating]').forEach(button => {
|
|
8868
|
+
button.setAttribute('aria-pressed', String(Number(button.dataset.previewRating) === rating));
|
|
8869
|
+
});
|
|
8870
|
+
sourcePreview.querySelectorAll('[data-preview-selection]').forEach(button => {
|
|
8871
|
+
button.setAttribute('aria-pressed', String(button.dataset.previewSelection === selection));
|
|
8872
|
+
});
|
|
8873
|
+
previewStatus.textContent = `${selection} · ${rating} stars — saved. ←/→ navigate; I include; X exclude; U clear selection.`;
|
|
8874
|
+
}
|
|
8875
|
+
|
|
8876
|
+
async function showSourcePreview(index) {
|
|
8877
|
+
if (previewBusy || index < 0 || index >= previewQueue.length) return;
|
|
8878
|
+
previewIndex = index;
|
|
8879
|
+
previewReady = false;
|
|
8880
|
+
const clip = previewQueue[index], generation = ++previewGeneration;
|
|
8881
|
+
lockSourcePreview(true);
|
|
8882
|
+
sourcePreview.querySelector('[data-preview-title]').textContent = `${index + 1} / ${previewQueue.length} — ${clip.clip_name || clip.clip_id}`;
|
|
8883
|
+
previewStatus.textContent = 'Loading preview and saved review…';
|
|
8884
|
+
previewImage.style.visibility = 'hidden';
|
|
8885
|
+
previewImage.alt = clip.clip_name || clip.clip_id;
|
|
8886
|
+
try {
|
|
8887
|
+
if (!clip.representative_frame_index) throw new Error('No analyzed frame available. Use Previous/Next to continue.');
|
|
8888
|
+
const loaded = new Promise((resolve, reject) => {
|
|
8889
|
+
previewImage.onload = resolve;
|
|
8890
|
+
previewImage.onerror = () => reject(new Error('Preview unavailable. Use Previous/Next to continue.'));
|
|
8891
|
+
});
|
|
8892
|
+
previewImage.src = `/api/clips/${encodeURIComponent(clip.clip_id)}/frames/${clip.representative_frame_index}`;
|
|
8893
|
+
const [data] = await Promise.all([api(`/api/clips/${encodeURIComponent(clip.clip_id)}`, {cache:'no-store'}), loaded]);
|
|
8894
|
+
if (generation !== previewGeneration) return;
|
|
8895
|
+
if (!data.success) throw new Error(data.error || 'Could not load saved review');
|
|
8896
|
+
displaySourceReview(data, clip);
|
|
8897
|
+
previewImage.style.visibility = 'visible';
|
|
8898
|
+
previewReady = true;
|
|
8899
|
+
} catch (error) {
|
|
8900
|
+
if (generation === previewGeneration) previewStatus.textContent = error.message;
|
|
8901
|
+
} finally {
|
|
8902
|
+
if (generation === previewGeneration) lockSourcePreview(false);
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
async function saveSourceReview(field, value) {
|
|
8907
|
+
if (previewBusy || !previewReady || previewIndex < 0) return;
|
|
8908
|
+
const clip = previewQueue[previewIndex], generation = previewGeneration;
|
|
8909
|
+
lockSourcePreview(true);
|
|
8910
|
+
previewStatus.textContent = 'Saving…';
|
|
8911
|
+
try {
|
|
8912
|
+
const result = await api(`/api/clips/${encodeURIComponent(clip.clip_id)}/corrections`, {
|
|
8913
|
+
method:'POST', body:JSON.stringify({entity_type:'clip', entity_uuid:clip.clip_id,
|
|
8914
|
+
field_path:field, new_value:value, author:'control_panel', reason:'source preview review'})});
|
|
8915
|
+
if (!result.success) throw new Error(result.error || 'Save failed');
|
|
8916
|
+
const data = await api(`/api/clips/${encodeURIComponent(clip.clip_id)}`, {cache:'no-store'});
|
|
8917
|
+
if (!data.success || readCorrectionValue(data.corrections, 'clip', clip.clip_id, field) !== value) {
|
|
8918
|
+
throw new Error('Saved review could not be verified; retry before continuing.');
|
|
8919
|
+
}
|
|
8920
|
+
clip.user_selection = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.selection') || 'Unreviewed';
|
|
8921
|
+
const listedClip = state.review.clipList?.clips?.find(item => item.clip_id === clip.clip_id);
|
|
8922
|
+
if (listedClip) listedClip.user_selection = clip.user_selection;
|
|
8923
|
+
renderReviewBin();
|
|
8924
|
+
if (generation !== previewGeneration) return;
|
|
8925
|
+
displaySourceReview(data, clip);
|
|
8926
|
+
lockSourcePreview(false);
|
|
8927
|
+
if (field === 'user.selection') {
|
|
8928
|
+
if (!matchesSourceSelection(clip)) {
|
|
8929
|
+
previewQueue.splice(previewIndex, 1);
|
|
8930
|
+
if (previewQueue.length) await showSourcePreview(Math.min(previewIndex, previewQueue.length - 1));
|
|
8931
|
+
else sourcePreview.close();
|
|
8932
|
+
} else if (value !== 'Unreviewed' && previewIndex + 1 < previewQueue.length) {
|
|
8933
|
+
await showSourcePreview(previewIndex + 1);
|
|
8934
|
+
}
|
|
8935
|
+
}
|
|
8936
|
+
} catch (error) {
|
|
8937
|
+
if (generation === previewGeneration) previewStatus.textContent = 'Save failed: ' + error.message;
|
|
8938
|
+
} finally {
|
|
8939
|
+
if (generation === previewGeneration) lockSourcePreview(false);
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
|
|
8943
|
+
sourcePreview.querySelector('[data-preview-close]').onclick = () => sourcePreview.close();
|
|
8944
|
+
sourcePreview.addEventListener('close', () => {
|
|
8945
|
+
++previewGeneration;
|
|
8946
|
+
previewReady = false;
|
|
8947
|
+
lockSourcePreview(false);
|
|
8948
|
+
renderReviewBin();
|
|
8949
|
+
});
|
|
8950
|
+
sourcePreview.addEventListener('click', event => {
|
|
8951
|
+
const button = event.target.closest('button');
|
|
8952
|
+
if (!button) return;
|
|
8953
|
+
if (button.dataset.previewStep) showSourcePreview(previewIndex + Number(button.dataset.previewStep));
|
|
8954
|
+
if (button.dataset.previewSelection) saveSourceReview('user.selection', button.dataset.previewSelection);
|
|
8955
|
+
if (button.dataset.previewRating != null) saveSourceReview('user.rating', Number(button.dataset.previewRating));
|
|
8956
|
+
});
|
|
8957
|
+
sourcePreview.addEventListener('keydown', event => {
|
|
8958
|
+
if (event.target.matches('input,textarea,select') || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
8959
|
+
const actions = {
|
|
8960
|
+
ArrowLeft: () => showSourcePreview(previewIndex - 1),
|
|
8961
|
+
ArrowRight: () => showSourcePreview(previewIndex + 1),
|
|
8962
|
+
i: () => saveSourceReview('user.selection', 'Include'),
|
|
8963
|
+
x: () => saveSourceReview('user.selection', 'Exclude'),
|
|
8964
|
+
u: () => saveSourceReview('user.selection', 'Unreviewed'),
|
|
8965
|
+
};
|
|
8966
|
+
const action = actions[event.key] || actions[event.key.toLowerCase()];
|
|
8967
|
+
if (action) { event.preventDefault(); event.stopPropagation(); if (!event.repeat) action(); }
|
|
8968
|
+
});
|
|
8969
|
+
$('reviewBinGrid').addEventListener('click', event => {
|
|
8970
|
+
const button = event.target.closest('[data-source-preview]');
|
|
8971
|
+
if (!button) return;
|
|
8972
|
+
event.stopImmediatePropagation();
|
|
8973
|
+
previewQueue = filteredClips().slice();
|
|
8974
|
+
sourcePreview.showModal();
|
|
8975
|
+
showSourcePreview(previewQueue.findIndex(clip => clip.clip_id === button.dataset.sourcePreview));
|
|
8976
|
+
}, true);
|
|
8977
|
+
|
|
8825
8978
|
function filteredClips() {
|
|
8826
8979
|
const data = state.review.clipList;
|
|
8827
8980
|
if (!data || !data.clips) return [];
|
|
8828
8981
|
const binFilter = state.review.binFilter || '';
|
|
8829
|
-
return data.clips.filter(c => !binFilter || c.bin_path === binFilter);
|
|
8982
|
+
return data.clips.filter(c => (!binFilter || c.bin_path === binFilter) && matchesSourceSelection(c));
|
|
8830
8983
|
}
|
|
8831
8984
|
|
|
8832
8985
|
function renderReviewBin() {
|
|
@@ -8850,8 +9003,8 @@ HTML = r"""<!doctype html>
|
|
|
8850
9003
|
}
|
|
8851
9004
|
const clips = filteredClips();
|
|
8852
9005
|
if (!clips.length) {
|
|
8853
|
-
if (state.review.binFilter) {
|
|
8854
|
-
if (summary) summary.textContent =
|
|
9006
|
+
if (state.review.binFilter || sourceSelectionFilter !== 'all') {
|
|
9007
|
+
if (summary) summary.textContent = 'No analyzed clips match these filters.';
|
|
8855
9008
|
if (grid) grid.innerHTML = '';
|
|
8856
9009
|
} else {
|
|
8857
9010
|
if (summary) summary.textContent = 'Nothing analyzed yet.';
|
|
@@ -8868,8 +9021,8 @@ HTML = r"""<!doctype html>
|
|
|
8868
9021
|
Array.from(selected).forEach(id => { if (!visibleIds.has(id)) selected.delete(id); });
|
|
8869
9022
|
if (summary) {
|
|
8870
9023
|
const total = data.clips.length;
|
|
8871
|
-
const base = state.review.binFilter
|
|
8872
|
-
? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'} · bin:
|
|
9024
|
+
const base = state.review.binFilter || sourceSelectionFilter !== 'all'
|
|
9025
|
+
? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'}${state.review.binFilter ? ' · bin: ' + state.review.binFilter : ''}`
|
|
8873
9026
|
: `${total} analyzed clip${total === 1 ? '' : 's'} in this project\u2019s analysis root.`;
|
|
8874
9027
|
if (selected.size > 0) {
|
|
8875
9028
|
summary.innerHTML = `<span>${escapeHtml(base)}</span>
|
|
@@ -8901,8 +9054,9 @@ HTML = r"""<!doctype html>
|
|
|
8901
9054
|
<span class="select-box">${isSelected ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>' : ''}</span>
|
|
8902
9055
|
</button>
|
|
8903
9056
|
${thumb}
|
|
9057
|
+
<button type="button" class="secondary" data-source-preview="${escapeHtml(clip.clip_id || '')}">Enlarge / review</button>
|
|
8904
9058
|
<div class="review-clip-card-name">${escapeHtml(clip.clip_name || '')}</div>
|
|
8905
|
-
<div class="review-clip-card-meta"><span>${dur}</span><span>${shots}</span>${primaryUse}${chip}</div>
|
|
9059
|
+
<div class="review-clip-card-meta"><span>${dur}</span><span>${shots}</span>${primaryUse}${chip}<span class="review-chip">${escapeHtml(clip.user_selection || 'Unreviewed')}</span></div>
|
|
8906
9060
|
${clip.clip_summary_oneliner ? `<div class="review-clip-card-oneliner">${escapeHtml(clip.clip_summary_oneliner)}</div>` : ''}
|
|
8907
9061
|
</div>`;
|
|
8908
9062
|
}).join('');
|
|
@@ -11481,6 +11635,7 @@ HTML = r"""<!doctype html>
|
|
|
11481
11635
|
openClipDetail(card.dataset.clipId).catch(alertError);
|
|
11482
11636
|
});
|
|
11483
11637
|
$('reviewBinGrid').addEventListener('keydown', event => {
|
|
11638
|
+
if (event.target.closest('[data-source-preview]')) return;
|
|
11484
11639
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
11485
11640
|
const card = event.target.closest('.review-clip-card');
|
|
11486
11641
|
if (card && card.dataset.clipId) {
|
|
@@ -11886,6 +12041,10 @@ HTML = r"""<!doctype html>
|
|
|
11886
12041
|
const q = $('reviewSearchInput').value.trim();
|
|
11887
12042
|
if (q) runReviewSearch(q).catch(alertError);
|
|
11888
12043
|
});
|
|
12044
|
+
$('sourceSelectionFilter').addEventListener('change', event => {
|
|
12045
|
+
sourceSelectionFilter = event.target.value;
|
|
12046
|
+
renderReviewBin();
|
|
12047
|
+
});
|
|
11889
12048
|
// Bin dropdown.
|
|
11890
12049
|
$('reviewBinFilter').addEventListener('change', event => {
|
|
11891
12050
|
state.review.binFilter = event.target.value || '';
|
|
@@ -13433,7 +13592,10 @@ def _v2_clip_summary_card(clip_slug: str, clip_dir: str, report: Dict[str, Any])
|
|
|
13433
13592
|
if not isinstance(oneliner, str) or not oneliner:
|
|
13434
13593
|
oneliner = summary[:140] + ("…" if len(summary) > 140 else "")
|
|
13435
13594
|
rep_index = _v2_pick_representative_frame_index(report)
|
|
13595
|
+
corrections = _v2_read_corrections_for_dir(clip_dir).get("current", {})
|
|
13596
|
+
selection = corrections.get(f"clip:{clip_block.get('clip_id')}:user.selection", {}).get("value", "Unreviewed")
|
|
13436
13597
|
return {
|
|
13598
|
+
"user_selection": selection,
|
|
13437
13599
|
"clip_id": clip_block.get("clip_id"),
|
|
13438
13600
|
"clip_slug": clip_slug,
|
|
13439
13601
|
"clip_dir": clip_dir,
|
|
@@ -13938,6 +14100,12 @@ def apply_clip_correction(project_root: str, clip_id: str, body: Dict[str, Any])
|
|
|
13938
14100
|
clip_dir = _v2_find_clip_dir(project_root, clip_id)
|
|
13939
14101
|
if not clip_dir:
|
|
13940
14102
|
return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
|
|
14103
|
+
field_path = body.get("field_path") or body.get("fieldPath")
|
|
14104
|
+
value = body.get("new_value") if "new_value" in body else body.get("newValue", body.get("value"))
|
|
14105
|
+
if field_path == "user.selection" and value not in ("Include", "Exclude", "Unreviewed"):
|
|
14106
|
+
return {"success": False, "error": "Selection must be Include, Exclude, or Unreviewed"}
|
|
14107
|
+
if field_path == "user.rating" and (type(value) is not int or not 0 <= value <= 5):
|
|
14108
|
+
return {"success": False, "error": "Rating must be an integer from 0 to 5"}
|
|
13941
14109
|
from src.server import _v2_update_field
|
|
13942
14110
|
entity_type = body.get("entity_type") or body.get("entityType") or "shot"
|
|
13943
14111
|
params: Dict[str, Any] = dict(body)
|
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 = "3.
|
|
90
|
+
VERSION = "3.4.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()}")
|