davinci-resolve-mcp 2.39.0 → 2.40.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 +30 -0
- package/README.md +1 -1
- package/docs/guides/control-panel.md +121 -52
- package/docs/process/release-process.md +11 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/regen_panel_screenshots.py +75 -0
- package/src/analysis_dashboard.py +195 -41
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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.40.0
|
|
6
|
+
|
|
7
|
+
Control panel UX overhaul + docs refresh, from a full live audit of every
|
|
8
|
+
panel view, with drift guards so neither can silently go stale again.
|
|
9
|
+
|
|
10
|
+
- **Added** a governance mode toggle (Advisory / Enforce) to the AI Console,
|
|
11
|
+
with corrected copy (the old text predated v2.39.0 enforce mode), and a
|
|
12
|
+
Recent-runs list on the AI-ops ledger showing each run's actor.
|
|
13
|
+
- **Added** chat-first onboarding: empty Overview, Review, and Inventory
|
|
14
|
+
states show plain-language guidance with a copyable suggested prompt
|
|
15
|
+
instead of zero-walls and MCP action names.
|
|
16
|
+
- **Added** History as a first-class Media menu item, deep-linkable at
|
|
17
|
+
`#analysis/review/history`; documented the full hash deep-link scheme.
|
|
18
|
+
- **Changed** the top-level "Analysis" menu to "Media" (it collided with the
|
|
19
|
+
Preferences page of the same name); "Analyze" is now "Inventory". Full
|
|
20
|
+
vocabulary pass: humanized readiness chips and frame-sampling labels, no
|
|
21
|
+
internal codenames, file names, or absolute paths in UI copy.
|
|
22
|
+
- **Fixed** the README's linked screenshot rendering broken in the Docs
|
|
23
|
+
reader: badge parsing no longer claims local linked images, and a new
|
|
24
|
+
`/api/doc_asset/` route serves repo doc images (path-constrained).
|
|
25
|
+
- **Fixed** poll hygiene: timers pause while the tab is hidden, panel-state
|
|
26
|
+
polling backs off when unfocused, and unchanged inventory polls return a
|
|
27
|
+
tiny 200 instead of a 304 that Chrome logged as an aborted request.
|
|
28
|
+
- **Docs**: `docs/guides/control-panel.md` fully rewritten for the current
|
|
29
|
+
IA with all screenshots regenerated from the live panel
|
|
30
|
+
(`scripts/regen_panel_screenshots.py`). A new drift-guard test fails the
|
|
31
|
+
suite — and the publish workflow, which now runs all three static guards
|
|
32
|
+
on every tag — when the guide drifts from the panel's navigation or
|
|
33
|
+
screenshots.
|
|
34
|
+
|
|
5
35
|
## What's New in v2.39.0
|
|
6
36
|
|
|
7
37
|
Governance enforce mode and actor identity — the staged Phase 3 of the
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Local Control Panel
|
|
2
2
|
|
|
3
|
-
The DaVinci Resolve MCP ships with a local, single-user browser control panel
|
|
4
|
-
inspecting Resolve state, running source-safe media analysis, drilling into
|
|
5
|
-
analyzed clips and shots, fixing analysis output inline,
|
|
6
|
-
preferences.
|
|
3
|
+
The DaVinci Resolve MCP ships with a local, single-user browser control panel
|
|
4
|
+
for inspecting Resolve state, running source-safe media analysis, drilling into
|
|
5
|
+
analyzed clips and shots, fixing analysis output inline, reviewing timeline
|
|
6
|
+
edit history, driving Resolve's local AI operations, and managing preferences.
|
|
7
7
|
|
|
8
8
|
The panel is a local HTTP server (default `http://127.0.0.1:8765`). It does not
|
|
9
9
|
expose a network listener beyond loopback and does not modify source media.
|
|
@@ -21,8 +21,29 @@ npx davinci-resolve-mcp control-panel
|
|
|
21
21
|
resolve_control(action="open_control_panel")
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Once running,
|
|
25
|
-
|
|
24
|
+
Once running, `resolve_control(action="control_panel_status")` checks the
|
|
25
|
+
pidfile and `resolve_control(action="close_control_panel")` stops it.
|
|
26
|
+
|
|
27
|
+
## Navigation and deep links
|
|
28
|
+
|
|
29
|
+
Top-level sections: **Overview**, **Media** (Inventory / Review / History),
|
|
30
|
+
**AI Console**, **Setup** (Resolve / MCP / Storage / Tools / Media Pool
|
|
31
|
+
History), **Docs**, and **Preferences** (Analysis / Caps + Safety / Metadata
|
|
32
|
+
and Markers / Paths and Workflow / MCP Updates). The project selector on the
|
|
33
|
+
right scopes the panel to an analysis context; **View All Projects** opens a
|
|
34
|
+
read-only browser over the Resolve project database with confirm-gated
|
|
35
|
+
loading.
|
|
36
|
+
|
|
37
|
+
Every view is addressable by URL hash, so links can be bookmarked or pasted
|
|
38
|
+
into chat:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
#overview #aiconsole
|
|
42
|
+
#analysis/media #analysis/review
|
|
43
|
+
#analysis/review/history #analysis/review/clip/<clip_id>
|
|
44
|
+
#analysis/review/clip/<clip_id>/shot/<n> #analysis/review/clip/<clip_id>/transcript
|
|
45
|
+
#diagnostics/mcp #preferences/caps
|
|
46
|
+
```
|
|
26
47
|
|
|
27
48
|
## Tour
|
|
28
49
|
|
|
@@ -30,78 +51,125 @@ the pidfile or `resolve_control(action="close_control_panel")` to stop it.
|
|
|
30
51
|
|
|
31
52
|

|
|
32
53
|
|
|
33
|
-
The
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
The at-a-glance summary for the active project: Resolve connection state,
|
|
55
|
+
source clip counts, analysis progress, search-index status, and the
|
|
56
|
+
source-media safety posture. When the project has no source clips yet, the
|
|
57
|
+
view shows a get-started card with a suggested chat prompt and a
|
|
58
|
+
**Copy prompt** button — the panel is summoned by conversation, and its empty
|
|
59
|
+
states point back into it.
|
|
36
60
|
|
|
37
|
-
###
|
|
61
|
+
### Media → Inventory
|
|
38
62
|
|
|
39
|
-

|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
Inventory walks the Resolve Media Pool read-only and filters to source clips so
|
|
66
|
+
timelines, compounds, titles, and generated items stay out of analysis queues.
|
|
67
|
+
Filters cover bin, media type, clip status, and analysis status, with optional
|
|
68
|
+
auto-refresh polling. From here you can select analyzable clips, copy a
|
|
69
|
+
ready-made analysis prompt for your chat session, or launch a supported chat
|
|
70
|
+
client directly.
|
|
45
71
|
|
|
46
|
-
###
|
|
72
|
+
### Media → Review
|
|
47
73
|
|
|
48
74
|

|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
Review is the browser for analyzed clips. The readiness strip summarizes the
|
|
77
|
+
evidence base (analyzed / superseded / vision-pending / warnings) with
|
|
78
|
+
humanized source-trust and analysis-layer chips. Cards show a representative
|
|
79
|
+
thumbnail, duration, shot count, status, and summary one-liner; grid and list
|
|
80
|
+
layouts are available, and full-text search covers clips, summaries, tags, and
|
|
81
|
+
transcripts once the search index is built.
|
|
53
82
|
|
|
54
83
|
### Clip detail
|
|
55
84
|
|
|
56
85
|

|
|
57
86
|
|
|
58
|
-
The clip view shows the full summary, tags, editorial notes, and
|
|
59
|
-
every detected shot
|
|
60
|
-
Resolve, jump to the transcript, or click into any shot
|
|
61
|
-
breakdown.
|
|
87
|
+
The clip view shows the full summary, tags, star rating, editorial notes, and
|
|
88
|
+
a contact sheet of every detected shot. From here you can open the clip in
|
|
89
|
+
Resolve, jump to the transcript, or click into any shot.
|
|
62
90
|
|
|
63
91
|
### Transcript
|
|
64
92
|
|
|
65
93
|

|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
with words` re-runs the configured local transcription backend (Whisper by
|
|
72
|
-
default) without re-doing visual analysis, and `corrections.json` captures
|
|
73
|
-
inline edits so they survive future re-runs.
|
|
95
|
+
Each segment shows its start time, the cleaned sentence, and the original
|
|
96
|
+
machine transcription beneath. Inline edits are preserved across re-analysis,
|
|
97
|
+
and `Re-transcribe with words` re-runs the configured local transcription
|
|
98
|
+
backend (Whisper by default) without re-doing visual analysis.
|
|
74
99
|
|
|
75
100
|
### Shot detail with inline correction
|
|
76
101
|
|
|
77
|
-

|
|
78
103
|
|
|
79
|
-
The shot view shows
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
editable inline
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
The shot view shows the analysis fields for the shot alongside the frames the
|
|
105
|
+
vision pass actually sampled, each labeled with why it was chosen ("Shot
|
|
106
|
+
start", "After cut", "Flash frame", "Motion peak"). Subjective fields are
|
|
107
|
+
editable inline; edits are kept with the clip's analysis and merged on top of
|
|
108
|
+
future re-analysis so human notes survive fresh vision runs. `Open in Resolve`
|
|
109
|
+
jumps straight to the clip in the source viewer with the shot's mark in/out
|
|
110
|
+
set.
|
|
86
111
|
|
|
87
|
-
###
|
|
112
|
+
### Media → History
|
|
88
113
|
|
|
89
|
-

|
|
90
115
|
|
|
91
|
-
The
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
The timeline edit history: archived timeline versions and brain edits per
|
|
117
|
+
timeline. Begin/end labeled runs to group related edits, archive the current
|
|
118
|
+
timeline before risky work, and select any timeline to inspect its version
|
|
119
|
+
chain and the edits between versions. Every destructive timeline edit made
|
|
120
|
+
through the MCP archives a version here automatically.
|
|
121
|
+
|
|
122
|
+
### AI Console
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
Runs Resolve's local AI operations (audio classification, IntelliSearch, slate
|
|
127
|
+
analysis, transcription, motion deblur, speech generation) on the current
|
|
128
|
+
Media Pool folder or a specific clip. Capability dots show what this Resolve
|
|
129
|
+
build exposes and which AI Extras are required. The **Governance** card sets
|
|
130
|
+
the per-session tier (off / lenient / standard / strict) for the two
|
|
131
|
+
media-creating ops and the governance **mode**: *Advisory* warns in the
|
|
132
|
+
confirm dialog; *Enforce* refuses an over-tier run until you raise the tier,
|
|
133
|
+
relax the mode, or consciously override. Usage gauges track runs, media
|
|
134
|
+
created, and render time against the active tier, and the AI-ops ledger
|
|
135
|
+
(Preferences → Caps + Safety) records every run with the acting instance.
|
|
136
|
+
|
|
137
|
+
### Setup
|
|
138
|
+
|
|
139
|
+

|
|
95
140
|
|
|
96
|
-
|
|
141
|
+
- **Resolve** — live connection, product/version, active page, project
|
|
142
|
+
identity, and Media Pool inventory.
|
|
143
|
+
- **MCP** — server identity, detected Resolve scripting paths, transport
|
|
144
|
+
status (with a start button for the networked mode), and per-client install
|
|
145
|
+
status with one-click install/repair for every supported harness.
|
|
146
|
+
- **Storage** — the analysis root, search index, and jobs database paths and
|
|
147
|
+
sizes for the active project.
|
|
148
|
+
- **Tools** — runtime helpers (ffprobe, ffmpeg, Whisper backends, OpenCV)
|
|
149
|
+
with ready/missing status and copyable install commands.
|
|
150
|
+
- **Media Pool History** — the provenance log for destructive media-pool
|
|
151
|
+
operations (deletes, replaces, relinks), kept separate from timeline brain
|
|
152
|
+
edits.
|
|
153
|
+
|
|
154
|
+
### Preferences
|
|
97
155
|
|
|
98
156
|

|
|
99
157
|
|
|
100
|
-
Server-wide
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
158
|
+
Server-wide defaults for this MCP install (dashboard-only conveniences stay in
|
|
159
|
+
the browser):
|
|
160
|
+
|
|
161
|
+
- **Analysis** — vision/transcription defaults, slate detection, source-trust
|
|
162
|
+
grading, depth, frame-sampling mode, summary style, report format.
|
|
163
|
+
- **Caps + Safety** — token and frame budget presets (minimal / standard /
|
|
164
|
+
generous / unlimited) with live usage gauges, per-clip/job/day caps, the
|
|
165
|
+
Resolve AI-ops ledger, and destructive-edit safety rails.
|
|
166
|
+
- **Metadata and Markers** — Resolve metadata write-back fields, timed-marker
|
|
167
|
+
defaults, marker types and colors, overwrite policy.
|
|
168
|
+
- **Paths and Workflow** — preferred analysis root, generated-media folder,
|
|
169
|
+
post-operation page behavior, and a read-only map of where files live.
|
|
170
|
+
- **MCP Updates** — update policy (prompt / notify / auto / never), release
|
|
171
|
+
channel (stable / beta / dev), check cadence, apply/rollback with update
|
|
172
|
+
history.
|
|
105
173
|
|
|
106
174
|
## Chat ↔ panel state sharing
|
|
107
175
|
|
|
@@ -115,8 +183,9 @@ active project's analysis root). MCP actions:
|
|
|
115
183
|
- `media_analysis(action="session_start_context")` — bootstrap a chat session
|
|
116
184
|
with the panel's current focus
|
|
117
185
|
|
|
118
|
-
The dashboard polls `/api/panel_state` every 2 seconds
|
|
119
|
-
|
|
186
|
+
The dashboard polls `/api/panel_state` every 2 seconds while it has focus,
|
|
187
|
+
backs off to roughly every 10 seconds when the window is unfocused, and stops
|
|
188
|
+
polling entirely while the tab is hidden.
|
|
120
189
|
|
|
121
190
|
## Open in Resolve
|
|
122
191
|
|
|
@@ -30,6 +30,11 @@ Every release bump must update all version surfaces:
|
|
|
30
30
|
- README current stats or latest-release summary when they changed
|
|
31
31
|
- `CHANGELOG.md` latest release entry
|
|
32
32
|
- `docs/SKILL.md` when tool discovery, examples, or behavior changed
|
|
33
|
+
- `docs/guides/control-panel.md` when the control panel UI changed, plus
|
|
34
|
+
regenerated screenshots: start the panel against a project with analysis
|
|
35
|
+
data, then run `venv/bin/python scripts/regen_panel_screenshots.py`.
|
|
36
|
+
The `tests.test_panel_docs_drift` guard fails the suite (and the publish
|
|
37
|
+
workflow) when the guide drifts from the panel's navigation or screenshots.
|
|
33
38
|
- Git tag, e.g. `v2.4.1`
|
|
34
39
|
- GitHub Release notes
|
|
35
40
|
|
|
@@ -43,12 +48,18 @@ Always run static checks before release:
|
|
|
43
48
|
```bash
|
|
44
49
|
venv/bin/python tests/test_import.py
|
|
45
50
|
venv/bin/python scripts/audit_api_parity.py
|
|
51
|
+
venv/bin/python -m unittest tests.test_static_undefined_names tests.test_action_list_drift tests.test_panel_docs_drift
|
|
46
52
|
node bin/davinci-resolve-mcp.mjs --help
|
|
47
53
|
node bin/davinci-resolve-mcp.mjs --version
|
|
48
54
|
npm pack --dry-run
|
|
49
55
|
git diff --check
|
|
50
56
|
```
|
|
51
57
|
|
|
58
|
+
The three drift guards (undefined names in `src/`, tool action lists vs
|
|
59
|
+
dispatch, control-panel guide vs panel navigation/screenshots) also run in the
|
|
60
|
+
`Publish npm package` workflow on every `v*` tag, so a stale doc or drifted
|
|
61
|
+
action list fails the publish — fix the drift rather than bypassing the gate.
|
|
62
|
+
|
|
52
63
|
Run focused unit tests for the changed surface. For recent timeline/marker
|
|
53
64
|
helpers, this usually includes:
|
|
54
65
|
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.40.0"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regenerate the control-panel doc screenshots from a live panel.
|
|
3
|
+
|
|
4
|
+
Drives the panel's hash deep links headlessly and writes 1280x800 captures
|
|
5
|
+
into docs/images/control-panel/. Run whenever the panel UI changes visibly.
|
|
6
|
+
|
|
7
|
+
Setup:
|
|
8
|
+
1. venv/bin/pip install playwright (chromium is fetched on first run if absent)
|
|
9
|
+
2. Start the panel bound to a project that HAS an analyzed clip, e.g.:
|
|
10
|
+
venv/bin/python -m src.control_panel --no-open
|
|
11
|
+
(Bind to a rich analysis context if the live Resolve project is empty.)
|
|
12
|
+
3. venv/bin/python scripts/regen_panel_screenshots.py [--clip-id <id>] [--port 8765]
|
|
13
|
+
|
|
14
|
+
The clip id defaults to the first analyzed clip reported by /api/clips.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import pathlib
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.request
|
|
23
|
+
|
|
24
|
+
OUT_DIR = pathlib.Path(__file__).resolve().parent.parent / "docs" / "images" / "control-panel"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def first_clip_id(base: str) -> str:
|
|
28
|
+
with urllib.request.urlopen(f"{base}/api/clips", timeout=10) as resp:
|
|
29
|
+
payload = json.load(resp)
|
|
30
|
+
clips = payload.get("clips") or []
|
|
31
|
+
if not clips:
|
|
32
|
+
sys.exit("No analyzed clips in the panel's project — bind the panel to a project with analysis data first.")
|
|
33
|
+
return clips[0]["clip_id"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> int:
|
|
37
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
38
|
+
parser.add_argument("--port", type=int, default=8765)
|
|
39
|
+
parser.add_argument("--clip-id", default=None)
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
base = f"http://127.0.0.1:{args.port}"
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from playwright.sync_api import sync_playwright
|
|
45
|
+
except ImportError:
|
|
46
|
+
sys.exit("playwright not installed: venv/bin/pip install playwright")
|
|
47
|
+
|
|
48
|
+
clip = args.clip_id or first_clip_id(base)
|
|
49
|
+
shots = [
|
|
50
|
+
("#overview", "01-overview.png", 3000),
|
|
51
|
+
("#analysis/review", "02-review-bin-grid.png", 3500),
|
|
52
|
+
(f"#analysis/review/clip/{clip}", "03-clip-detail.png", 4000),
|
|
53
|
+
(f"#analysis/review/clip/{clip}/transcript", "04-transcript.png", 4000),
|
|
54
|
+
(f"#analysis/review/clip/{clip}/shot/3", "05-shot-detail.png", 4500),
|
|
55
|
+
("#analysis/media", "06-inventory.png", 3500),
|
|
56
|
+
("#diagnostics/resolve", "07-diagnostics-resolve.png", 3000),
|
|
57
|
+
("#preferences/analysis", "08-preferences-analysis.png", 3000),
|
|
58
|
+
("#aiconsole", "09-ai-console.png", 3000),
|
|
59
|
+
("#analysis/review/history", "10-history.png", 3500),
|
|
60
|
+
]
|
|
61
|
+
with sync_playwright() as pw:
|
|
62
|
+
browser = pw.chromium.launch()
|
|
63
|
+
page = browser.new_page(viewport={"width": 1280, "height": 800})
|
|
64
|
+
for hash_, name, wait in shots:
|
|
65
|
+
page.goto(f"{base}/{hash_}")
|
|
66
|
+
page.wait_for_timeout(wait)
|
|
67
|
+
page.screenshot(path=str(OUT_DIR / name))
|
|
68
|
+
print("shot:", name)
|
|
69
|
+
browser.close()
|
|
70
|
+
print(f"done — {len(shots)} screenshots in {OUT_DIR}")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
raise SystemExit(main())
|
|
@@ -3753,6 +3753,7 @@ HTML = r"""<!doctype html>
|
|
|
3753
3753
|
justify-content: flex-start;
|
|
3754
3754
|
padding-bottom: 2px;
|
|
3755
3755
|
}
|
|
3756
|
+
.control-tabs select { max-width: 150px; min-width: 110px; }
|
|
3756
3757
|
.lab-navbar {
|
|
3757
3758
|
height: 88px;
|
|
3758
3759
|
flex-wrap: wrap;
|
|
@@ -3817,10 +3818,11 @@ HTML = r"""<!doctype html>
|
|
|
3817
3818
|
<nav class="control-tabs" aria-label="Control panel sections">
|
|
3818
3819
|
<button class="control-tab active" data-panel-target="overview">Overview</button>
|
|
3819
3820
|
<div class="control-nav-item">
|
|
3820
|
-
<button class="control-tab has-menu" data-panel-target="analysis">
|
|
3821
|
+
<button class="control-tab has-menu" data-panel-target="analysis">Media <span class="tab-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span></button>
|
|
3821
3822
|
<div class="nav-dropdown" role="menu" aria-label="Analysis pages">
|
|
3822
|
-
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="media" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m7 15 3-3 2 2 4-5 1 2"></path></svg></span>
|
|
3823
|
+
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="media" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m7 15 3-3 2 2 4-5 1 2"></path></svg></span>Inventory</button>
|
|
3823
3824
|
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="review" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="14" rx="2"></rect><circle cx="9" cy="10" r="2"></circle><path d="m21 17-5-5-9 9"></path></svg></span>Review</button>
|
|
3825
|
+
<button class="nav-dropdown-item" id="navHistoryItem" data-panel-target="analysis" data-subpage-target="review" data-review-view="history" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg></span>History</button>
|
|
3824
3826
|
</div>
|
|
3825
3827
|
</div>
|
|
3826
3828
|
<button class="control-tab" data-panel-target="aiconsole">AI Console</button>
|
|
@@ -3969,7 +3971,7 @@ HTML = r"""<!doctype html>
|
|
|
3969
3971
|
<section class="span-12 subpage active" data-subpage-scope="analysis" data-subpage="media">
|
|
3970
3972
|
<div class="section-top">
|
|
3971
3973
|
<div>
|
|
3972
|
-
<h2>
|
|
3974
|
+
<h2>Inventory</h2>
|
|
3973
3975
|
<p class="section-copy">Resolve media is inventoried read-only and filtered to source clips so timelines, compounds, titles, and generated items stay out of analysis queues.</p>
|
|
3974
3976
|
<div class="section-meta" id="resolveProject">Resolve connection pending</div>
|
|
3975
3977
|
<div class="copy-status" id="copyPromptStatus"></div>
|
|
@@ -4048,7 +4050,7 @@ HTML = r"""<!doctype html>
|
|
|
4048
4050
|
</div>
|
|
4049
4051
|
<div class="controls" id="reviewControls">
|
|
4050
4052
|
<button class="secondary" id="reviewRefreshBtn">Refresh</button>
|
|
4051
|
-
<button class="secondary" id="reviewHistoryBtn" title="Timeline edit history
|
|
4053
|
+
<button class="secondary" id="reviewHistoryBtn" title="Timeline edit history">History</button>
|
|
4052
4054
|
<button class="secondary" id="reviewBackBtn" style="display:none">← Back</button>
|
|
4053
4055
|
</div>
|
|
4054
4056
|
</div>
|
|
@@ -4057,7 +4059,6 @@ HTML = r"""<!doctype html>
|
|
|
4057
4059
|
<div class="readiness-card-header">
|
|
4058
4060
|
<span class="readiness-title">Project readiness</span>
|
|
4059
4061
|
<span id="readinessEvidenceBase" class="readiness-evidence"></span>
|
|
4060
|
-
<button id="readinessRefreshBtn" class="secondary" type="button" title="Refresh readiness summary">Refresh</button>
|
|
4061
4062
|
</div>
|
|
4062
4063
|
<div id="readinessSummaryRow" class="readiness-summary-row"></div>
|
|
4063
4064
|
<div id="readinessDetails" class="readiness-details"></div>
|
|
@@ -4181,8 +4182,12 @@ HTML = r"""<!doctype html>
|
|
|
4181
4182
|
|
|
4182
4183
|
<div class="caps-section" style="margin-top:12px;">
|
|
4183
4184
|
<div class="caps-section-head"><div class="caps-section-title">Governance</div>
|
|
4184
|
-
<div class="caps-section-hint">
|
|
4185
|
+
<div class="caps-section-hint">Per-session limits for the two media-creating ops (deblur, speech). In <strong>Advisory</strong> mode you're warned in the confirm dialog but never blocked; in <strong>Enforce</strong> mode an over-tier run is refused until you raise the tier, relax the mode, or consciously override. Pick the tier that matches the job.</div></div>
|
|
4185
4186
|
<div id="aiGovTiers" class="caps-preset-cards" role="radiogroup" aria-label="AI governance tier"></div>
|
|
4187
|
+
<div class="review-view-toggle" id="aiGovMode" role="radiogroup" aria-label="Governance mode" style="margin-top:10px;">
|
|
4188
|
+
<button type="button" data-gov-mode="advisory">Advisory</button>
|
|
4189
|
+
<button type="button" data-gov-mode="enforce">Enforce</button>
|
|
4190
|
+
</div>
|
|
4186
4191
|
<div id="aiGovUsage" class="caps-usage-gauges" style="margin-top:10px;"></div>
|
|
4187
4192
|
</div>
|
|
4188
4193
|
|
|
@@ -4527,6 +4532,7 @@ HTML = r"""<!doctype html>
|
|
|
4527
4532
|
</tr></thead>
|
|
4528
4533
|
<tbody id="resolveAiOpsRows"></tbody>
|
|
4529
4534
|
</table>
|
|
4535
|
+
<div id="resolveAiOpsRecent"></div>
|
|
4530
4536
|
</div>
|
|
4531
4537
|
</div>
|
|
4532
4538
|
|
|
@@ -4853,7 +4859,7 @@ HTML = r"""<!doctype html>
|
|
|
4853
4859
|
const VIEW_ALL_PROJECTS_VALUE = '__view_all_projects__';
|
|
4854
4860
|
const PANEL_LABELS = {
|
|
4855
4861
|
overview: 'Overview',
|
|
4856
|
-
analysis: '
|
|
4862
|
+
analysis: 'Media',
|
|
4857
4863
|
aiconsole: 'AI Console',
|
|
4858
4864
|
diagnostics: 'Setup',
|
|
4859
4865
|
projects: 'Projects',
|
|
@@ -4877,7 +4883,7 @@ HTML = r"""<!doctype html>
|
|
|
4877
4883
|
};
|
|
4878
4884
|
const SUBPAGE_LABELS = {
|
|
4879
4885
|
analysis: {
|
|
4880
|
-
media: '
|
|
4886
|
+
media: 'Inventory',
|
|
4881
4887
|
review: 'Review',
|
|
4882
4888
|
},
|
|
4883
4889
|
diagnostics: {
|
|
@@ -5153,6 +5159,10 @@ HTML = r"""<!doctype html>
|
|
|
5153
5159
|
openClipDetail(clipId, { writePanelState: false, pushHash: false }).catch(alertError);
|
|
5154
5160
|
}
|
|
5155
5161
|
}
|
|
5162
|
+
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'history') {
|
|
5163
|
+
reviewSetView('history');
|
|
5164
|
+
refreshHistoryTimelines().catch(alertError);
|
|
5165
|
+
}
|
|
5156
5166
|
// Deep links: #analysis/review/combined/<id1,id2,...>
|
|
5157
5167
|
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'combined' && route[3]) {
|
|
5158
5168
|
const ids = decodeURIComponent(route[3]).split(',').filter(Boolean);
|
|
@@ -5268,12 +5278,19 @@ HTML = r"""<!doctype html>
|
|
|
5268
5278
|
renderInfoRows('overviewStatusList', [
|
|
5269
5279
|
{ label: 'Resolve project', value: resolveProject, icon: ICONS.project, pill: { tone: 'pill-ok', label: 'Read-only' } },
|
|
5270
5280
|
{ label: 'Source clips', value: `${clipLabel(clips.length)} · ${readyClips} ready`, icon: ICONS.clips, pill: { tone: clipsTone, label: `${readyClips} ready` } },
|
|
5271
|
-
{ label: 'Sequences', value: `${sequences}
|
|
5281
|
+
{ label: 'Sequences', value: sequences ? `${sequences} timeline${sequences === 1 ? '' : 's'} detected (read-only)` : 'No timelines detected', icon: ICONS.sequences },
|
|
5272
5282
|
{ label: 'Clip media status', value: `${onlineClips} online · ${missingClips} missing/offline`, icon: ICONS.status, pill: { tone: mediaTone, label: missingClips ? `${missingClips} missing` : 'All online' } },
|
|
5273
5283
|
{ label: 'Analysis progress', value: `${analyzedClips} of ${clips.length} analyzed (${analyzedPct}%)`, icon: ICONS.analysis, pill: { tone: analysisTone, label: `${analyzedPct}%` }, meter: { percent: analyzedPct } },
|
|
5274
5284
|
{ label: 'Search index', value: index.detail, icon: ICONS.search, pill: { tone: index.tone, label: index.label } },
|
|
5275
5285
|
{ label: 'Safety', value: 'Source media is read-only; outputs stay in the active project analysis root.', icon: ICONS.safety, pill: { tone: 'pill-ok', label: 'Read-only' } },
|
|
5276
5286
|
]);
|
|
5287
|
+
if (!clips.length) {
|
|
5288
|
+
const list = $('overviewStatusList');
|
|
5289
|
+
if (list) list.insertAdjacentHTML('afterbegin', chatPromptCard(
|
|
5290
|
+
'This project has no source clips yet. Import media in Resolve, then ask your assistant to take it from there:',
|
|
5291
|
+
'Look at my current Resolve project, inventory the media, and analyze the source clips.'
|
|
5292
|
+
));
|
|
5293
|
+
}
|
|
5277
5294
|
}
|
|
5278
5295
|
}
|
|
5279
5296
|
|
|
@@ -5700,6 +5717,16 @@ HTML = r"""<!doctype html>
|
|
|
5700
5717
|
analyzeBtn.disabled = !hasAnalyzableClips;
|
|
5701
5718
|
copyBtn.disabled = !hasAnalyzableClips;
|
|
5702
5719
|
setAnalyzeClientButtonsEnabled(hasAnalyzableClips);
|
|
5720
|
+
if (!allClips.length) {
|
|
5721
|
+
body.className = 'empty';
|
|
5722
|
+
body.innerHTML = chatPromptCard(
|
|
5723
|
+
'The Media Pool has no source clips yet. Import media in Resolve, then ask your assistant to inventory and analyze it:',
|
|
5724
|
+
'Look at my current Resolve project, inventory the media, and analyze the source clips.'
|
|
5725
|
+
);
|
|
5726
|
+
updateMediaPollStatus();
|
|
5727
|
+
renderControlPanels();
|
|
5728
|
+
return;
|
|
5729
|
+
}
|
|
5703
5730
|
body.className = '';
|
|
5704
5731
|
const rows = clips.map((clip, index) => {
|
|
5705
5732
|
// The server normalizes 'succeeded'/'skipped' → 'analyzed' whenever the
|
|
@@ -6325,6 +6352,7 @@ HTML = r"""<!doctype html>
|
|
|
6325
6352
|
state.resolveMediaStale = false;
|
|
6326
6353
|
if (res.status === 304) return;
|
|
6327
6354
|
const payload = await res.json();
|
|
6355
|
+
if (payload && payload.unchanged) return;
|
|
6328
6356
|
if (!res.ok || payload.success === false) {
|
|
6329
6357
|
throw new Error(payload.error || res.statusText);
|
|
6330
6358
|
}
|
|
@@ -6467,6 +6495,21 @@ HTML = r"""<!doctype html>
|
|
|
6467
6495
|
}
|
|
6468
6496
|
}
|
|
6469
6497
|
|
|
6498
|
+
function chatPromptCard(message, prompt) {
|
|
6499
|
+
return `<div class="empty" style="padding:var(--space-3);border:1px dashed var(--border-default);border-radius:var(--radius-md);display:grid;gap:8px;justify-items:start;text-align:left;">
|
|
6500
|
+
<span>${escapeHtml(message)}</span>
|
|
6501
|
+
<code style="font-size:12px;color:var(--text-secondary);white-space:normal;">\u201c${escapeHtml(prompt)}\u201d</code>
|
|
6502
|
+
<button class="secondary" type="button" data-copy-chat-prompt="${escapeHtml(prompt)}">Copy prompt</button>
|
|
6503
|
+
</div>`;
|
|
6504
|
+
}
|
|
6505
|
+
document.addEventListener('click', async (e) => {
|
|
6506
|
+
const btn = e.target.closest('[data-copy-chat-prompt]');
|
|
6507
|
+
if (!btn) return;
|
|
6508
|
+
const ok = await writePromptToClipboard(btn.dataset.copyChatPrompt);
|
|
6509
|
+
btn.textContent = ok ? 'Copied \u2014 paste in your chat session' : 'Copy prompt';
|
|
6510
|
+
setTimeout(() => { btn.textContent = 'Copy prompt'; }, 2600);
|
|
6511
|
+
});
|
|
6512
|
+
|
|
6470
6513
|
async function copyMcpPrompt() {
|
|
6471
6514
|
const prompt = buildMcpPrompt();
|
|
6472
6515
|
await writePromptToClipboard(prompt);
|
|
@@ -6829,6 +6872,7 @@ HTML = r"""<!doctype html>
|
|
|
6829
6872
|
const interval = Number($('mediaPollInterval').value || 0);
|
|
6830
6873
|
if (enabled && interval > 0) {
|
|
6831
6874
|
state.mediaPollTimer = setInterval(() => {
|
|
6875
|
+
if (document.hidden) return;
|
|
6832
6876
|
refreshResolveMedia({ silent: true }).catch(error => {
|
|
6833
6877
|
console.warn('Resolve media poll failed', error);
|
|
6834
6878
|
updateMediaPollStatus(error.message || String(error));
|
|
@@ -7306,6 +7350,17 @@ HTML = r"""<!doctype html>
|
|
|
7306
7350
|
<td style="padding:4px 6px;">${b.bytes_created ? fmtBytes(b.bytes_created) : '—'}</td>
|
|
7307
7351
|
</tr>`;
|
|
7308
7352
|
}).join('');
|
|
7353
|
+
const recent = Array.isArray(data.recent) ? data.recent.slice(0, 8) : [];
|
|
7354
|
+
const recentEl = $('resolveAiOpsRecent');
|
|
7355
|
+
if (recentEl) {
|
|
7356
|
+
recentEl.innerHTML = recent.length ? '<div class="caps-section-hint" style="margin-top:8px;">Recent runs</div>'
|
|
7357
|
+
+ recent.map(r => {
|
|
7358
|
+
const when = (r.occurred_at || '').replace('T', ' ');
|
|
7359
|
+
const who = r.actor ? ` · <span title="instance:pid that performed the op">${escapeHtml(r.actor)}</span>` : '';
|
|
7360
|
+
const ok = r.success ? 'ok' : 'failed';
|
|
7361
|
+
return `<div style="font-size:12px; color:var(--text-secondary); padding:2px 0;">${escapeHtml(when)} · ${escapeHtml(r.op)} · ${ok}${who}</div>`;
|
|
7362
|
+
}).join('') : '';
|
|
7363
|
+
}
|
|
7309
7364
|
tableEl.style.display = '';
|
|
7310
7365
|
}
|
|
7311
7366
|
|
|
@@ -7471,7 +7526,7 @@ HTML = r"""<!doctype html>
|
|
|
7471
7526
|
standard: 'Sensible default',
|
|
7472
7527
|
strict: 'Tight leash',
|
|
7473
7528
|
};
|
|
7474
|
-
state.aiGov = state.aiGov || { tier: 'standard', thresholds: {}, usage: {} };
|
|
7529
|
+
state.aiGov = state.aiGov || { tier: 'standard', mode: 'advisory', thresholds: {}, usage: {} };
|
|
7475
7530
|
|
|
7476
7531
|
function aiGovFmt(dim, v) {
|
|
7477
7532
|
if (v == null) return '∞';
|
|
@@ -7515,14 +7570,26 @@ HTML = r"""<!doctype html>
|
|
|
7515
7570
|
</div>`;
|
|
7516
7571
|
}).join('');
|
|
7517
7572
|
}
|
|
7573
|
+
function renderGovMode() {
|
|
7574
|
+
const el = $('aiGovMode');
|
|
7575
|
+
if (!el) return;
|
|
7576
|
+
const mode = state.aiGov.mode || 'advisory';
|
|
7577
|
+
el.querySelectorAll('[data-gov-mode]').forEach(btn => {
|
|
7578
|
+
const active = btn.dataset.govMode === mode;
|
|
7579
|
+
btn.classList.toggle('active', active);
|
|
7580
|
+
btn.setAttribute('aria-checked', active ? 'true' : 'false');
|
|
7581
|
+
});
|
|
7582
|
+
}
|
|
7518
7583
|
async function refreshGovernance() {
|
|
7519
7584
|
const data = await api('/api/resolve_ai/governance').catch(() => ({ success: false }));
|
|
7520
7585
|
if (!data || !data.success) return;
|
|
7521
7586
|
state.aiGov.tier = data.tier;
|
|
7587
|
+
state.aiGov.mode = data.mode || 'advisory';
|
|
7522
7588
|
state.aiGov.thresholds = data.thresholds || {};
|
|
7523
7589
|
state.aiGov.usage = data.usage || {};
|
|
7524
7590
|
state.aiGov.tiersAvailable = data.tiers_available || {};
|
|
7525
7591
|
renderGovTiers();
|
|
7592
|
+
renderGovMode();
|
|
7526
7593
|
renderGovUsage();
|
|
7527
7594
|
}
|
|
7528
7595
|
async function setGovernanceTier(tier) {
|
|
@@ -7534,6 +7601,15 @@ HTML = r"""<!doctype html>
|
|
|
7534
7601
|
if (!res || !res.success) console.warn('governance save failed:', res && res.error);
|
|
7535
7602
|
await refreshGovernance();
|
|
7536
7603
|
}
|
|
7604
|
+
async function setGovernanceMode(mode) {
|
|
7605
|
+
state.aiGov.mode = mode;
|
|
7606
|
+
renderGovMode();
|
|
7607
|
+
const res = await api('/api/resolve_ai/governance', {
|
|
7608
|
+
method: 'POST', body: JSON.stringify({ mode }),
|
|
7609
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
7610
|
+
if (!res || !res.success) console.warn('governance mode save failed:', res && res.error);
|
|
7611
|
+
await refreshGovernance();
|
|
7612
|
+
}
|
|
7537
7613
|
|
|
7538
7614
|
function initAiConsole() {
|
|
7539
7615
|
if (_aiConsoleInit) { renderAiConsole(); refreshGovernance().catch(() => {}); return; }
|
|
@@ -7549,6 +7625,13 @@ HTML = r"""<!doctype html>
|
|
|
7549
7625
|
if (card && card.dataset.govTier) setGovernanceTier(card.dataset.govTier);
|
|
7550
7626
|
});
|
|
7551
7627
|
}
|
|
7628
|
+
const govModeEl = $('aiGovMode');
|
|
7629
|
+
if (govModeEl) {
|
|
7630
|
+
govModeEl.addEventListener('click', (ev) => {
|
|
7631
|
+
const btn = ev.target.closest('[data-gov-mode]');
|
|
7632
|
+
if (btn && btn.dataset.govMode) setGovernanceMode(btn.dataset.govMode);
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7552
7635
|
refreshGovernance().catch(() => {});
|
|
7553
7636
|
}
|
|
7554
7637
|
|
|
@@ -7979,7 +8062,7 @@ HTML = r"""<!doctype html>
|
|
|
7979
8062
|
else if (view === 'shot') meta.textContent = `Shot ${state.review.currentShotIndex} of ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
7980
8063
|
else if (view === 'transcript') meta.textContent = `Transcript · ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
7981
8064
|
else if (view === 'combined') meta.textContent = `Combined review · ${state.review.combinedData?.clip_count || '?'} clips`;
|
|
7982
|
-
else if (view === 'history') meta.textContent = 'Timeline history · versions and brain edits per timeline
|
|
8065
|
+
else if (view === 'history') meta.textContent = 'Timeline history · archived versions and brain edits per timeline';
|
|
7983
8066
|
}
|
|
7984
8067
|
if (opts.writePanelState !== false) {
|
|
7985
8068
|
writePanelStateAsync({
|
|
@@ -8037,24 +8120,23 @@ HTML = r"""<!doctype html>
|
|
|
8037
8120
|
].map(stat => `<div class="readiness-stat ${stat.kind}"><span class="stat-value">${stat.value}</span><span class="stat-label">${escapeHtml(stat.label)}</span></div>`).join('');
|
|
8038
8121
|
}
|
|
8039
8122
|
if (details) {
|
|
8123
|
+
const TRUST_LABELS = { trusted: 'Trusted source', unknown: 'Unverified source', untrusted: 'Untrusted source' };
|
|
8124
|
+
const LAYER_LABELS = {
|
|
8125
|
+
technical: 'Technical', motion: 'Motion', transcription: 'Transcript',
|
|
8126
|
+
vision: 'Vision', cut_analysis: 'Cut analysis', readthrough: 'Readthrough',
|
|
8127
|
+
};
|
|
8040
8128
|
const trustChips = Object.entries(trustDist)
|
|
8041
8129
|
.sort((a, b) => b[1] - a[1])
|
|
8042
|
-
.map(([k, v]) => `<span class="chip" title="
|
|
8130
|
+
.map(([k, v]) => `<span class="chip" title="clips by source trust">${escapeHtml(TRUST_LABELS[k] || k)} · ${v}</span>`)
|
|
8043
8131
|
.join('');
|
|
8044
|
-
const layerChips =
|
|
8045
|
-
.
|
|
8132
|
+
const layerChips = Object.entries(LAYER_LABELS)
|
|
8133
|
+
.filter(([layer]) => layers[layer])
|
|
8134
|
+
.map(([layer, label]) => `<span class="chip" title="clips with this analysis layer">${escapeHtml(label)} · ${layers[layer]}</span>`)
|
|
8046
8135
|
.join('');
|
|
8047
|
-
details.innerHTML =
|
|
8136
|
+
details.innerHTML = trustChips + layerChips;
|
|
8048
8137
|
}
|
|
8049
8138
|
}
|
|
8050
8139
|
|
|
8051
|
-
document.addEventListener('click', (e) => {
|
|
8052
|
-
const target = e.target;
|
|
8053
|
-
if (target && target.id === 'readinessRefreshBtn') {
|
|
8054
|
-
refreshReadinessCard().catch(() => {});
|
|
8055
|
-
}
|
|
8056
|
-
});
|
|
8057
|
-
|
|
8058
8140
|
function populateBinFilter() {
|
|
8059
8141
|
const data = state.review.clipList;
|
|
8060
8142
|
const select = $('reviewBinFilter');
|
|
@@ -8097,10 +8179,16 @@ HTML = r"""<!doctype html>
|
|
|
8097
8179
|
}
|
|
8098
8180
|
const clips = filteredClips();
|
|
8099
8181
|
if (!clips.length) {
|
|
8100
|
-
if (
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8182
|
+
if (state.review.binFilter) {
|
|
8183
|
+
if (summary) summary.textContent = `No analyzed clips in bin "${state.review.binFilter}".`;
|
|
8184
|
+
if (grid) grid.innerHTML = '';
|
|
8185
|
+
} else {
|
|
8186
|
+
if (summary) summary.textContent = 'Nothing analyzed yet.';
|
|
8187
|
+
if (grid) grid.innerHTML = chatPromptCard(
|
|
8188
|
+
'Review fills in as clips are analyzed. Ask your assistant to analyze this project\u2019s media:',
|
|
8189
|
+
'Analyze the source clips in my current Resolve project and build the search index.'
|
|
8190
|
+
);
|
|
8191
|
+
}
|
|
8104
8192
|
return;
|
|
8105
8193
|
}
|
|
8106
8194
|
const selected = state.review.selectedBinClipIds;
|
|
@@ -8111,7 +8199,7 @@ HTML = r"""<!doctype html>
|
|
|
8111
8199
|
const total = data.clips.length;
|
|
8112
8200
|
const base = state.review.binFilter
|
|
8113
8201
|
? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'} · bin: ${state.review.binFilter}`
|
|
8114
|
-
: `${total} analyzed clip${total === 1 ? '' : 's'} in
|
|
8202
|
+
: `${total} analyzed clip${total === 1 ? '' : 's'} in this project\u2019s analysis root.`;
|
|
8115
8203
|
if (selected.size > 0) {
|
|
8116
8204
|
summary.innerHTML = `<span>${escapeHtml(base)}</span>
|
|
8117
8205
|
<span class="bin-selection-toolbar">
|
|
@@ -9215,7 +9303,7 @@ HTML = r"""<!doctype html>
|
|
|
9215
9303
|
const safe = value == null ? '' : String(value);
|
|
9216
9304
|
return `<div class="review-notes-row" data-notes-scope="${scope}">
|
|
9217
9305
|
<div class="label">Notes</div>
|
|
9218
|
-
<textarea data-notes-input="${scope}" placeholder="Editorial notes — saved
|
|
9306
|
+
<textarea data-notes-input="${scope}" placeholder="Editorial notes — saved with this clip\u2019s analysis and preserved on re-analysis\u2026">${escapeHtml(safe)}</textarea>
|
|
9219
9307
|
<div class="controls">
|
|
9220
9308
|
<button class="secondary" data-notes-save="${scope}" type="button">Save notes</button>
|
|
9221
9309
|
<span class="saved" data-notes-saved="${scope}" style="opacity:0">Saved</span>
|
|
@@ -9402,13 +9490,21 @@ HTML = r"""<!doctype html>
|
|
|
9402
9490
|
extrasHtml += `<div class="group"><div class="group-title">QC flags</div><div class="chip-row" style="display:flex;flex-wrap:wrap;gap:4px">${qcFlags.map(f => `<span class="review-chip">${escapeHtml(String(f))}</span>`).join('')}</div></div>`;
|
|
9403
9491
|
}
|
|
9404
9492
|
if (Array.isArray(shot.frame_indices_used) && shot.frame_indices_used.length) {
|
|
9405
|
-
|
|
9493
|
+
const n = shot.frame_indices_used.length;
|
|
9494
|
+
extrasHtml += `<div class="group"><div class="group-title">Sampled frames</div><div class="value" style="font-size:12px;color:var(--text-secondary)" title="frame indices ${escapeHtml(shot.frame_indices_used.join(', '))}">${n} frame${n === 1 ? '' : 's'} sampled from this shot (shown below)</div></div>`;
|
|
9406
9495
|
}
|
|
9407
9496
|
const emptyHint = !hasAnyGroup && !editing
|
|
9408
9497
|
? `<div class="empty" style="padding:var(--space-3);border:1px dashed var(--border-default);border-radius:var(--radius-md);color:var(--text-tertiary);font-size:12px">Shot-level analysis fields aren't populated in the report. The clip-level analysis blocks (above on the clip detail page) cover this clip. Re-run analysis with a fuller vision pass to fill in per-shot Visual / Content / Editorial fields.</div>`
|
|
9409
9498
|
: '';
|
|
9410
9499
|
$('reviewShotFields').innerHTML = groupsHtml + extrasHtml + emptyHint;
|
|
9411
9500
|
const frames = data.frames || [];
|
|
9501
|
+
const FRAME_REASON_LABELS = {
|
|
9502
|
+
shot_start: 'Shot start', shot_end: 'Shot end', shot_progress: 'Mid-shot',
|
|
9503
|
+
shot_representative: 'Key frame', cut_after: 'After cut', cut_before: 'Before cut',
|
|
9504
|
+
flash_candidate: 'Flash frame', motion_peak: 'Motion peak', interval: 'Interval sample',
|
|
9505
|
+
scene_change: 'Scene change', first_usable: 'First usable', last_usable: 'Last usable',
|
|
9506
|
+
midpoint: 'Midpoint',
|
|
9507
|
+
};
|
|
9412
9508
|
$('reviewShotFrames').innerHTML = frames.map(f => {
|
|
9413
9509
|
const peak = f.motion_peak ? 'peak' : '';
|
|
9414
9510
|
const src = clipId ? `/api/clips/${encodeURIComponent(clipId)}/frames/${f.frame_index}` : '';
|
|
@@ -9416,7 +9512,7 @@ HTML = r"""<!doctype html>
|
|
|
9416
9512
|
${src ? `<img class="review-thumb" loading="lazy" src="${src}" alt="frame ${f.frame_index}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'review-thumb placeholder',textContent:'frame missing'}))">` : ''}
|
|
9417
9513
|
<div class="label">
|
|
9418
9514
|
<span>${f.time_seconds != null ? `${Number(f.time_seconds).toFixed(2)}s` : `#${f.frame_index}`}</span>
|
|
9419
|
-
<span>${escapeHtml(f.selection_reason || '')}</span>
|
|
9515
|
+
<span title="${escapeHtml(f.selection_reason || '')}">${escapeHtml(FRAME_REASON_LABELS[f.selection_reason] || f.selection_reason || '')}</span>
|
|
9420
9516
|
</div>
|
|
9421
9517
|
</div>`;
|
|
9422
9518
|
}).join('');
|
|
@@ -9562,7 +9658,11 @@ HTML = r"""<!doctype html>
|
|
|
9562
9658
|
|
|
9563
9659
|
function ensureReviewPanelStateTimer() {
|
|
9564
9660
|
if (state.review.panelStateTimer) return;
|
|
9661
|
+
state.review.panelStateTick = 0;
|
|
9565
9662
|
state.review.panelStateTimer = window.setInterval(() => {
|
|
9663
|
+
if (document.hidden) return;
|
|
9664
|
+
state.review.panelStateTick = (state.review.panelStateTick + 1) % 5;
|
|
9665
|
+
if (!document.hasFocus() && state.review.panelStateTick !== 0) return;
|
|
9566
9666
|
if (state.activePanel === 'analysis' && state.activeSubpages.analysis === 'review') {
|
|
9567
9667
|
pollPanelStateOnce().catch(() => {});
|
|
9568
9668
|
}
|
|
@@ -9658,9 +9758,11 @@ HTML = r"""<!doctype html>
|
|
|
9658
9758
|
const active = new Set((activeValues || []).map(value => String(value || '').trim()).filter(Boolean));
|
|
9659
9759
|
const values = uniqueValues(candidates || [], Array.from(active));
|
|
9660
9760
|
hidden.value = Array.from(active).join(', ');
|
|
9661
|
-
container.innerHTML = values.map(value =>
|
|
9662
|
-
|
|
9663
|
-
|
|
9761
|
+
container.innerHTML = values.map(value => {
|
|
9762
|
+
const label = String(value).replace(/_/g, ' ');
|
|
9763
|
+
return `
|
|
9764
|
+
<button type="button" class="token-pill${active.has(value) ? ' active' : ''}" data-token-value="${escapeHtml(value)}" aria-pressed="${active.has(value) ? 'true' : 'false'}" title="${escapeHtml(value)}">${escapeHtml(label)}</button>
|
|
9765
|
+
`; }).join('');
|
|
9664
9766
|
container.querySelectorAll('[data-token-value]').forEach(button => {
|
|
9665
9767
|
button.addEventListener('click', () => {
|
|
9666
9768
|
button.classList.toggle('active');
|
|
@@ -9964,14 +10066,14 @@ HTML = r"""<!doctype html>
|
|
|
9964
10066
|
if (options.updateHash !== false) {
|
|
9965
10067
|
updateRouteHash('docs', state.activeDoc);
|
|
9966
10068
|
}
|
|
9967
|
-
const rendered = renderMarkdown(payload.content || '');
|
|
10069
|
+
const rendered = renderMarkdown(payload.content || '', payload.path || '');
|
|
9968
10070
|
setHtml('docReader', rendered.html);
|
|
9969
10071
|
renderDocSectionNav(rendered.sections);
|
|
9970
10072
|
setText('docSource', payload.path || '');
|
|
9971
10073
|
applyDocFilters();
|
|
9972
10074
|
}
|
|
9973
10075
|
|
|
9974
|
-
function renderMarkdown(markdown) {
|
|
10076
|
+
function renderMarkdown(markdown, docPath) {
|
|
9975
10077
|
const lines = String(markdown || '').split(/\r?\n/);
|
|
9976
10078
|
const parts = [];
|
|
9977
10079
|
const sections = [];
|
|
@@ -10000,7 +10102,9 @@ HTML = r"""<!doctype html>
|
|
|
10000
10102
|
const line = rawLine.trimEnd();
|
|
10001
10103
|
if (!line.trim()) continue;
|
|
10002
10104
|
const badgeMatches = parseBadgeTokens(line.trim());
|
|
10003
|
-
|
|
10105
|
+
// Badge rows are external status shields; a linked LOCAL image (e.g.
|
|
10106
|
+
// the README's control-panel screenshot) must reach the image handlers.
|
|
10107
|
+
if (badgeMatches.length && badgeMatches.every(token => /^https?:/i.test(token.src))) {
|
|
10004
10108
|
while (index + 1 < lines.length) {
|
|
10005
10109
|
const nextMatches = parseBadgeTokens(lines[index + 1].trim());
|
|
10006
10110
|
if (!nextMatches.length) break;
|
|
@@ -10029,9 +10133,14 @@ HTML = r"""<!doctype html>
|
|
|
10029
10133
|
parts.push(`<h${level} id="${id}" class="doc-block" data-md-type="heading">${inlineMarkdown(heading[2])}</h${level}>`);
|
|
10030
10134
|
continue;
|
|
10031
10135
|
}
|
|
10136
|
+
const linkedImage = line.match(/^\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)$/);
|
|
10137
|
+
if (linkedImage) {
|
|
10138
|
+
parts.push(renderImageBlock(linkedImage[1], linkedImage[2], docPath));
|
|
10139
|
+
continue;
|
|
10140
|
+
}
|
|
10032
10141
|
const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
10033
10142
|
if (image) {
|
|
10034
|
-
parts.push(renderImageBlock(image[1], image[2]));
|
|
10143
|
+
parts.push(renderImageBlock(image[1], image[2], docPath));
|
|
10035
10144
|
continue;
|
|
10036
10145
|
}
|
|
10037
10146
|
const bullet = line.match(/^[-*]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/);
|
|
@@ -10099,8 +10208,27 @@ HTML = r"""<!doctype html>
|
|
|
10099
10208
|
return tokens;
|
|
10100
10209
|
}
|
|
10101
10210
|
|
|
10102
|
-
function
|
|
10103
|
-
|
|
10211
|
+
function resolveDocAsset(src, docPath) {
|
|
10212
|
+
const clean = String(src || '').trim();
|
|
10213
|
+
if (/^(https?:|data:|\/)/i.test(clean)) return clean;
|
|
10214
|
+
const stack = [];
|
|
10215
|
+
const parts = String(docPath || '').split('/').slice(0, -1).concat(clean.split('/'));
|
|
10216
|
+
for (const part of parts) {
|
|
10217
|
+
if (!part || part === '.') continue;
|
|
10218
|
+
if (part === '..') { stack.pop(); continue; }
|
|
10219
|
+
stack.push(part);
|
|
10220
|
+
}
|
|
10221
|
+
const rel = stack.join('/');
|
|
10222
|
+
const marker = 'docs/images/';
|
|
10223
|
+
if (rel.startsWith(marker)) {
|
|
10224
|
+
return '/api/doc_asset/' + rel.slice(marker.length).split('/').map(encodeURIComponent).join('/');
|
|
10225
|
+
}
|
|
10226
|
+
return clean;
|
|
10227
|
+
}
|
|
10228
|
+
|
|
10229
|
+
function renderImageBlock(alt, src, docPath) {
|
|
10230
|
+
const resolved = resolveDocAsset(src, docPath);
|
|
10231
|
+
return `<figure class="doc-image doc-block" data-md-type="image"><img src="${escapeAttribute(resolved)}" alt="${escapeAttribute(alt || src)}" loading="lazy"></figure>`;
|
|
10104
10232
|
}
|
|
10105
10233
|
|
|
10106
10234
|
function renderMarkdownTable(lines) {
|
|
@@ -10280,6 +10408,13 @@ HTML = r"""<!doctype html>
|
|
|
10280
10408
|
closeNavDropdowns();
|
|
10281
10409
|
closeActionMenus();
|
|
10282
10410
|
setPanel(control.dataset.panelTarget, { subpage: control.dataset.subpageTarget });
|
|
10411
|
+
if (control.dataset.reviewView === 'history') {
|
|
10412
|
+
reviewSetView('history');
|
|
10413
|
+
refreshHistoryTimelines().catch(alertError);
|
|
10414
|
+
if (window.location.hash !== '#analysis/review/history') {
|
|
10415
|
+
window.history.replaceState(null, '', '#analysis/review/history');
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10283
10418
|
});
|
|
10284
10419
|
});
|
|
10285
10420
|
document.querySelectorAll('[data-doc]').forEach(control => {
|
|
@@ -10456,7 +10591,7 @@ HTML = r"""<!doctype html>
|
|
|
10456
10591
|
refreshRestartBanner().catch(() => {});
|
|
10457
10592
|
// Poll for restart marker every 30s
|
|
10458
10593
|
if (state.updates.restartTimer) clearInterval(state.updates.restartTimer);
|
|
10459
|
-
state.updates.restartTimer = setInterval(() => refreshRestartBanner().catch(() => {}), 30000);
|
|
10594
|
+
state.updates.restartTimer = setInterval(() => { if (!document.hidden) refreshRestartBanner().catch(() => {}); }, 30000);
|
|
10460
10595
|
|
|
10461
10596
|
// Auto-save preference (server-side preference; reads via /api/setup/defaults)
|
|
10462
10597
|
$('prefAutoSaveAfterArchive')?.addEventListener('change', async e => {
|
|
@@ -13972,9 +14107,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
13972
14107
|
raw = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
|
13973
14108
|
etag = '"' + hashlib.md5(raw).hexdigest() + '"'
|
|
13974
14109
|
if self.headers.get("If-None-Match") == etag:
|
|
13975
|
-
|
|
14110
|
+
tiny = json.dumps({"success": True, "unchanged": True, "etag": etag}).encode("utf-8")
|
|
14111
|
+
self.send_response(200)
|
|
14112
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
14113
|
+
self.send_header("Content-Length", str(len(tiny)))
|
|
13976
14114
|
self.send_header("ETag", etag)
|
|
13977
14115
|
self.end_headers()
|
|
14116
|
+
self.wfile.write(tiny)
|
|
13978
14117
|
return
|
|
13979
14118
|
self.send_response(200)
|
|
13980
14119
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
@@ -14105,6 +14244,21 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
14105
14244
|
payload = _dashboard_doc(doc)
|
|
14106
14245
|
self._json(payload, 200 if payload.get("success") else 404)
|
|
14107
14246
|
return
|
|
14247
|
+
if path.startswith("/api/doc_asset/"):
|
|
14248
|
+
rel = unquote(path[len("/api/doc_asset/"):])
|
|
14249
|
+
base = os.path.realpath(os.path.join(_repo_root(), "docs", "images"))
|
|
14250
|
+
full = os.path.realpath(os.path.join(base, rel))
|
|
14251
|
+
if not (full.startswith(base + os.sep) or full == base):
|
|
14252
|
+
self._json({"success": False, "error": "path escape"}, HTTPStatus.FORBIDDEN)
|
|
14253
|
+
return
|
|
14254
|
+
content_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
14255
|
+
".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp"}
|
|
14256
|
+
ext = os.path.splitext(full)[1].lower()
|
|
14257
|
+
if ext not in content_types or not os.path.isfile(full):
|
|
14258
|
+
self._json({"success": False, "error": "not found"}, HTTPStatus.NOT_FOUND)
|
|
14259
|
+
return
|
|
14260
|
+
self._serve_file(full, content_types[ext])
|
|
14261
|
+
return
|
|
14108
14262
|
if path == "/api/setup/schema":
|
|
14109
14263
|
self._json(_setup_defaults("schema"))
|
|
14110
14264
|
return
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.40.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|