davinci-resolve-mcp 2.26.0 → 2.27.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/AGENTS.md +3 -1
- package/CHANGELOG.md +51 -0
- package/README.md +3 -3
- package/bin/davinci-resolve-mcp.mjs +64 -7
- package/docs/SKILL.md +17 -3
- package/docs/guides/media-analysis-guide.md +27 -0
- package/docs/install.md +10 -1
- package/install.py +73 -8
- package/package.json +1 -1
- package/src/analysis_dashboard.py +82 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +287 -4
- package/src/utils/analysis_caps.py +28 -19
- package/src/utils/media_analysis.py +229 -30
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 329-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.27.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -498,9 +498,10 @@ def prep_color_handoff(output_dir: str = "") -> str:
|
|
|
498
498
|
_py_ver = sys.version_info[:2]
|
|
499
499
|
if _py_ver >= (3, 13):
|
|
500
500
|
logger.warning(
|
|
501
|
-
f"Python {_py_ver[0]}.{_py_ver[1]} detected.
|
|
502
|
-
f"
|
|
503
|
-
f"recreate the venv with
|
|
501
|
+
f"Python {_py_ver[0]}.{_py_ver[1]} detected. This is verified working on recent "
|
|
502
|
+
f"Resolve builds (Studio 20.3.2), but older builds may not load the scripting "
|
|
503
|
+
f"bridge on 3.13+. If scriptapp('Resolve') returns None, recreate the venv with "
|
|
504
|
+
f"Python 3.10-3.12."
|
|
504
505
|
)
|
|
505
506
|
|
|
506
507
|
# ─── Resolve Connection (lazy) ───────────────────────────────────────────────
|
|
@@ -6311,6 +6312,18 @@ _MEDIA_ANALYSIS_DEFAULT_PREFS = {
|
|
|
6311
6312
|
"source_trust": "auto",
|
|
6312
6313
|
"default_depth": "standard",
|
|
6313
6314
|
"default_sample_frames": 8,
|
|
6315
|
+
# Frame-sampling mode — how many frames a clip gets for visual analysis.
|
|
6316
|
+
# "ask" prompts the user to choose a standing default the first time they
|
|
6317
|
+
# analyze; the choice is then saved here. Canonical modes (see
|
|
6318
|
+
# media_analysis.SAMPLING_MODES): fixed (Economy), per_minute (Balanced),
|
|
6319
|
+
# adaptive_capped (Thorough, recommended), adaptive (Thorough uncapped).
|
|
6320
|
+
"sampling_mode_default": "ask",
|
|
6321
|
+
# Tunables shared by Balanced + Thorough modes. frames_per_minute drives the
|
|
6322
|
+
# Balanced target; frame_floor/frame_ceiling bound every duration/content
|
|
6323
|
+
# scaled mode (the ceiling is also the Thorough per-clip cap).
|
|
6324
|
+
"sampling_frames_per_minute": 4.0,
|
|
6325
|
+
"sampling_frame_floor": 3,
|
|
6326
|
+
"sampling_frame_ceiling": 80,
|
|
6314
6327
|
"preferred_analysis_root": None,
|
|
6315
6328
|
"preferred_generated_media_folder": None,
|
|
6316
6329
|
"default_post_operation_page": "stay_put",
|
|
@@ -6579,6 +6592,29 @@ def _media_analysis_effective_preferences() -> Dict[str, Any]:
|
|
|
6579
6592
|
except (TypeError, ValueError):
|
|
6580
6593
|
sample_frames_int = 8
|
|
6581
6594
|
effective["default_sample_frames"] = max(0, min(48, sample_frames_int))
|
|
6595
|
+
# sampling_mode_default normalizes to a canonical mode, or None when unset /
|
|
6596
|
+
# "ask" (None means "not yet chosen" → first-run prompt fires).
|
|
6597
|
+
effective["sampling_mode_default"] = _normalize_sampling_mode_default(effective.get("sampling_mode_default"))
|
|
6598
|
+
|
|
6599
|
+
def _pos_number(value: Any, fallback: float) -> float:
|
|
6600
|
+
try:
|
|
6601
|
+
f = float(value)
|
|
6602
|
+
except (TypeError, ValueError):
|
|
6603
|
+
return fallback
|
|
6604
|
+
return f if f > 0 else fallback
|
|
6605
|
+
|
|
6606
|
+
effective["sampling_frames_per_minute"] = _pos_number(
|
|
6607
|
+
effective.get("sampling_frames_per_minute"), _media_analysis_module.DEFAULT_FRAMES_PER_MINUTE
|
|
6608
|
+
)
|
|
6609
|
+
effective["sampling_frame_floor"] = int(_pos_number(
|
|
6610
|
+
effective.get("sampling_frame_floor"), _media_analysis_module.DEFAULT_FRAME_FLOOR
|
|
6611
|
+
))
|
|
6612
|
+
ceiling = int(_pos_number(
|
|
6613
|
+
effective.get("sampling_frame_ceiling"), _media_analysis_module.DEFAULT_FRAME_CEILING
|
|
6614
|
+
))
|
|
6615
|
+
if ceiling < effective["sampling_frame_floor"]:
|
|
6616
|
+
ceiling = effective["sampling_frame_floor"]
|
|
6617
|
+
effective["sampling_frame_ceiling"] = ceiling
|
|
6582
6618
|
effective["default_post_operation_page"] = _normalize_setup_choice(
|
|
6583
6619
|
effective.get("default_post_operation_page"),
|
|
6584
6620
|
["stay_put", "media", "cut", "edit", "fusion", "color", "fairlight", "deliver"],
|
|
@@ -6742,6 +6778,148 @@ def _media_analysis_timed_marker_decision(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
6742
6778
|
}
|
|
6743
6779
|
|
|
6744
6780
|
|
|
6781
|
+
def _normalize_sampling_mode_default(value: Any) -> Optional[str]:
|
|
6782
|
+
"""Resolve a stored sampling_mode_default to a canonical mode, or None.
|
|
6783
|
+
|
|
6784
|
+
None means "not chosen yet" — covers an unset value and the "ask" sentinel,
|
|
6785
|
+
both of which should trigger the first-run prompt.
|
|
6786
|
+
"""
|
|
6787
|
+
if value is None:
|
|
6788
|
+
return None
|
|
6789
|
+
raw = str(value).strip().lower()
|
|
6790
|
+
if raw in {"", "ask", "prompt", "ask_me", "ask_user", "none", "unset", "default"}:
|
|
6791
|
+
return None
|
|
6792
|
+
return _media_analysis_module.normalize_sampling_mode(value, default=None)
|
|
6793
|
+
|
|
6794
|
+
|
|
6795
|
+
def _sampling_mode_choice_from_params(p: Dict[str, Any]) -> Optional[str]:
|
|
6796
|
+
"""Read an explicit sampling-mode choice from analysis params.
|
|
6797
|
+
|
|
6798
|
+
Returns a canonical mode, the "ask" sentinel, or None when unspecified.
|
|
6799
|
+
"""
|
|
6800
|
+
raw = _first_param(
|
|
6801
|
+
p,
|
|
6802
|
+
"sampling_mode",
|
|
6803
|
+
"samplingMode",
|
|
6804
|
+
"frame_sampling_mode",
|
|
6805
|
+
"frameSamplingMode",
|
|
6806
|
+
"analysis_mode",
|
|
6807
|
+
"analysisMode",
|
|
6808
|
+
)
|
|
6809
|
+
if raw is None and isinstance(p.get("sampling"), dict):
|
|
6810
|
+
raw = _first_param(p["sampling"], "mode", "sampling_mode", "samplingMode")
|
|
6811
|
+
if raw is None:
|
|
6812
|
+
return None
|
|
6813
|
+
if str(raw).strip().lower() in {"ask", "prompt", "ask_me", "ask_user"}:
|
|
6814
|
+
return "ask"
|
|
6815
|
+
return _media_analysis_module.normalize_sampling_mode(raw, default=None)
|
|
6816
|
+
|
|
6817
|
+
|
|
6818
|
+
def _media_analysis_sampling_mode_prompt() -> Dict[str, Any]:
|
|
6819
|
+
"""First-run prompt offering the four sampling modes (Thorough recommended).
|
|
6820
|
+
|
|
6821
|
+
Each option saves the chosen mode as the standing default (save_sampling_default)
|
|
6822
|
+
so the user is only asked once. Pass `sampling_mode` alone, without the save
|
|
6823
|
+
flag, for a one-off run that doesn't change the default.
|
|
6824
|
+
"""
|
|
6825
|
+
return {
|
|
6826
|
+
"question": (
|
|
6827
|
+
"How should frames be sampled for visual analysis? This sets your "
|
|
6828
|
+
"default for future runs (pass sampling_mode per-call for a one-off)."
|
|
6829
|
+
),
|
|
6830
|
+
"default_behavior": "Recommended: Thorough — content-aware coverage with a bounded, predictable cost.",
|
|
6831
|
+
"options": [
|
|
6832
|
+
{
|
|
6833
|
+
"id": "fixed",
|
|
6834
|
+
"label": "Economy",
|
|
6835
|
+
"description": (
|
|
6836
|
+
"Flat ~8 frames per clip regardless of length. Cheapest and most "
|
|
6837
|
+
"predictable; good for proxies, triage, or known-short clips."
|
|
6838
|
+
),
|
|
6839
|
+
"params": {"sampling_mode": "fixed", "save_sampling_default": True},
|
|
6840
|
+
},
|
|
6841
|
+
{
|
|
6842
|
+
"id": "per_minute",
|
|
6843
|
+
"label": "Balanced",
|
|
6844
|
+
"description": (
|
|
6845
|
+
"Frames scale with duration (~4/min, bounded 3–80). Cost is linear "
|
|
6846
|
+
"in footage length and easy to predict; content-blind."
|
|
6847
|
+
),
|
|
6848
|
+
"params": {"sampling_mode": "per_minute", "save_sampling_default": True},
|
|
6849
|
+
},
|
|
6850
|
+
{
|
|
6851
|
+
"id": "adaptive_capped",
|
|
6852
|
+
"label": "Thorough (recommended)",
|
|
6853
|
+
"description": (
|
|
6854
|
+
"Content-aware: samples shot boundaries, representatives, and flash "
|
|
6855
|
+
"frames, bounded 3–80 per clip. Best coverage with a bounded cost."
|
|
6856
|
+
),
|
|
6857
|
+
"params": {"sampling_mode": "adaptive_capped", "save_sampling_default": True},
|
|
6858
|
+
},
|
|
6859
|
+
{
|
|
6860
|
+
"id": "adaptive",
|
|
6861
|
+
"label": "Thorough (uncapped)",
|
|
6862
|
+
"description": (
|
|
6863
|
+
"Content-aware with no per-clip ceiling (up to 512 frames). Use only "
|
|
6864
|
+
"when clips are known to be short or few — cost can grow fast."
|
|
6865
|
+
),
|
|
6866
|
+
"params": {"sampling_mode": "adaptive", "save_sampling_default": True},
|
|
6867
|
+
},
|
|
6868
|
+
],
|
|
6869
|
+
"recommended": _media_analysis_module.RECOMMENDED_SAMPLING_MODE,
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6872
|
+
|
|
6873
|
+
def _media_analysis_sampling_mode_decision(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
6874
|
+
"""Resolve the frame-sampling mode for an analysis run.
|
|
6875
|
+
|
|
6876
|
+
Resolution order:
|
|
6877
|
+
1. Explicit `sampling_mode` param → one-off (persisted only if save flag set).
|
|
6878
|
+
2. Saved `sampling_mode_default` preference → used silently.
|
|
6879
|
+
3. Otherwise → prompt_required (first run); falls back to the recommended
|
|
6880
|
+
mode so previews/automation still work, but the entry point surfaces the
|
|
6881
|
+
prompt and blocks real execution.
|
|
6882
|
+
"""
|
|
6883
|
+
choice = _sampling_mode_choice_from_params(p)
|
|
6884
|
+
save_default = _media_analysis_bool(
|
|
6885
|
+
_first_param(p, "save_sampling_default", "saveSamplingDefault", "set_sampling_default", "setSamplingDefault"),
|
|
6886
|
+
False,
|
|
6887
|
+
)
|
|
6888
|
+
preferences = _read_media_analysis_preferences()
|
|
6889
|
+
explicit_saved = "sampling_mode_default" in preferences
|
|
6890
|
+
saved_default = _media_analysis_effective_preferences().get("sampling_mode_default")
|
|
6891
|
+
|
|
6892
|
+
recommended = _media_analysis_module.RECOMMENDED_SAMPLING_MODE
|
|
6893
|
+
saved = None
|
|
6894
|
+
|
|
6895
|
+
if choice and choice != "ask":
|
|
6896
|
+
mode = choice
|
|
6897
|
+
source = "explicit"
|
|
6898
|
+
if save_default:
|
|
6899
|
+
saved = mode
|
|
6900
|
+
preferences["sampling_mode_default"] = mode
|
|
6901
|
+
preferences["sampling_mode_default_updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
6902
|
+
_write_media_analysis_preferences(preferences)
|
|
6903
|
+
source = "saved_default"
|
|
6904
|
+
elif choice == "ask":
|
|
6905
|
+
mode = recommended
|
|
6906
|
+
source = "prompt_required"
|
|
6907
|
+
elif saved_default:
|
|
6908
|
+
mode = saved_default
|
|
6909
|
+
source = "saved_default" if explicit_saved else "default"
|
|
6910
|
+
else:
|
|
6911
|
+
mode = recommended
|
|
6912
|
+
source = "prompt_required"
|
|
6913
|
+
|
|
6914
|
+
return {
|
|
6915
|
+
"mode": mode,
|
|
6916
|
+
"source": source,
|
|
6917
|
+
"prompt_required": source == "prompt_required",
|
|
6918
|
+
"saved_default": saved or saved_default,
|
|
6919
|
+
"preferences_path": _media_analysis_preferences_path(),
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
|
|
6745
6923
|
def _media_analysis_sync_marker_suggestions(detection: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
6746
6924
|
suggestions = []
|
|
6747
6925
|
for file_result in detection.get("files") or []:
|
|
@@ -8085,6 +8263,23 @@ def _media_analysis_apply_setup_defaults(action: str, p: Dict[str, Any]) -> Dict
|
|
|
8085
8263
|
)
|
|
8086
8264
|
applied["transcription_default"] = transcription_default
|
|
8087
8265
|
|
|
8266
|
+
# Frame-sampling mode: resolve from explicit param > saved default > first-run
|
|
8267
|
+
# prompt (recommended fallback). Inject mode + tunables so the analysis engine
|
|
8268
|
+
# picks them up via _resolve_sampling_config; stash the decision so the entry
|
|
8269
|
+
# point can surface the first-run prompt.
|
|
8270
|
+
if action in {"plan", "analyze_file", "analyze_clip", "analyze_bin", "analyze_project", "analyze_timeline", "analyze_sequence", "start_batch_job"}:
|
|
8271
|
+
sampling_decision = _media_analysis_sampling_mode_decision(out)
|
|
8272
|
+
if not _has_any_param(out, "sampling_mode", "samplingMode", "frame_sampling_mode", "frameSamplingMode"):
|
|
8273
|
+
out["sampling_mode"] = sampling_decision["mode"]
|
|
8274
|
+
applied["sampling_mode"] = sampling_decision["mode"]
|
|
8275
|
+
if not _has_any_param(out, "frames_per_minute", "framesPerMinute"):
|
|
8276
|
+
out["frames_per_minute"] = prefs.get("sampling_frames_per_minute")
|
|
8277
|
+
if not _has_any_param(out, "frame_floor", "frameFloor"):
|
|
8278
|
+
out["frame_floor"] = prefs.get("sampling_frame_floor")
|
|
8279
|
+
if not _has_any_param(out, "frame_ceiling", "frameCeiling"):
|
|
8280
|
+
out["frame_ceiling"] = prefs.get("sampling_frame_ceiling")
|
|
8281
|
+
out["_sampling_mode_decision"] = sampling_decision
|
|
8282
|
+
|
|
8088
8283
|
if action in {"plan", "analyze_file", "analyze_clip", "analyze_bin", "analyze_project", "analyze_timeline", "analyze_sequence", "start_batch_job", "publish_clip_metadata"}:
|
|
8089
8284
|
if not _has_any_param(
|
|
8090
8285
|
out,
|
|
@@ -9390,7 +9585,9 @@ def _setup_media_analysis_defaults() -> Dict[str, Any]:
|
|
|
9390
9585
|
"source_trust": ["auto", "filename", "low", "medium", "high"],
|
|
9391
9586
|
"default_depth": ["quick", "standard", "deep"],
|
|
9392
9587
|
"default_post_operation_page": ["stay_put", "media", "cut", "edit", "fusion", "color", "fairlight", "deliver"],
|
|
9588
|
+
"sampling_mode_default": ["ask", "fixed", "per_minute", "adaptive_capped", "adaptive"],
|
|
9393
9589
|
},
|
|
9590
|
+
"sampling_mode_labels": dict(_media_analysis_module.SAMPLING_MODE_LABELS),
|
|
9394
9591
|
}
|
|
9395
9592
|
|
|
9396
9593
|
|
|
@@ -9511,6 +9708,24 @@ def _setup_set_media_analysis_defaults(media_defaults: Dict[str, Any], dry_run:
|
|
|
9511
9708
|
"defaultsampleframes": "default_sample_frames",
|
|
9512
9709
|
"sample_frames": "default_sample_frames",
|
|
9513
9710
|
"sampleframes": "default_sample_frames",
|
|
9711
|
+
"sampling_mode_default": "sampling_mode_default",
|
|
9712
|
+
"samplingmodedefault": "sampling_mode_default",
|
|
9713
|
+
"sampling_mode": "sampling_mode_default",
|
|
9714
|
+
"samplingmode": "sampling_mode_default",
|
|
9715
|
+
"analysis_mode": "sampling_mode_default",
|
|
9716
|
+
"analysismode": "sampling_mode_default",
|
|
9717
|
+
"sampling_frames_per_minute": "sampling_frames_per_minute",
|
|
9718
|
+
"samplingframesperminute": "sampling_frames_per_minute",
|
|
9719
|
+
"frames_per_minute": "sampling_frames_per_minute",
|
|
9720
|
+
"framesperminute": "sampling_frames_per_minute",
|
|
9721
|
+
"sampling_frame_floor": "sampling_frame_floor",
|
|
9722
|
+
"samplingframefloor": "sampling_frame_floor",
|
|
9723
|
+
"frame_floor": "sampling_frame_floor",
|
|
9724
|
+
"framefloor": "sampling_frame_floor",
|
|
9725
|
+
"sampling_frame_ceiling": "sampling_frame_ceiling",
|
|
9726
|
+
"samplingframeceiling": "sampling_frame_ceiling",
|
|
9727
|
+
"frame_ceiling": "sampling_frame_ceiling",
|
|
9728
|
+
"frameceiling": "sampling_frame_ceiling",
|
|
9514
9729
|
}
|
|
9515
9730
|
|
|
9516
9731
|
requested: Dict[str, Any] = {}
|
|
@@ -9639,6 +9854,37 @@ def _setup_set_media_analysis_defaults(media_defaults: Dict[str, Any], dry_run:
|
|
|
9639
9854
|
except (TypeError, ValueError):
|
|
9640
9855
|
return _err("default_sample_frames must be an integer between 0 and 48.")
|
|
9641
9856
|
set_or_clear(key, raw_value, max(0, min(48, frames_int)))
|
|
9857
|
+
elif key == "sampling_mode_default":
|
|
9858
|
+
# "ask" clears the saved default so the first-run prompt fires again;
|
|
9859
|
+
# otherwise normalize a canonical key or friendly label.
|
|
9860
|
+
if _setup_text_key(raw_value) in {"ask", "prompt", "askme", "askuser"}:
|
|
9861
|
+
next_preferences.pop("sampling_mode_default", None)
|
|
9862
|
+
next_preferences.pop("sampling_mode_default_updated_at", None)
|
|
9863
|
+
updates[key] = {"before": before.get(key), "after": None, "cleared": True}
|
|
9864
|
+
else:
|
|
9865
|
+
normalized = _media_analysis_module.normalize_sampling_mode(raw_value, default=None)
|
|
9866
|
+
if normalized is None:
|
|
9867
|
+
return _err(
|
|
9868
|
+
"Unsupported sampling_mode_default. Use ask, fixed/economy, "
|
|
9869
|
+
"per_minute/balanced, adaptive_capped/thorough, or adaptive."
|
|
9870
|
+
)
|
|
9871
|
+
set_or_clear(key, raw_value, normalized)
|
|
9872
|
+
elif key == "sampling_frames_per_minute":
|
|
9873
|
+
try:
|
|
9874
|
+
rate = float(raw_value)
|
|
9875
|
+
except (TypeError, ValueError):
|
|
9876
|
+
return _err("sampling_frames_per_minute must be a positive number.")
|
|
9877
|
+
if rate <= 0:
|
|
9878
|
+
return _err("sampling_frames_per_minute must be greater than 0.")
|
|
9879
|
+
set_or_clear(key, raw_value, rate)
|
|
9880
|
+
elif key in {"sampling_frame_floor", "sampling_frame_ceiling"}:
|
|
9881
|
+
try:
|
|
9882
|
+
n = int(raw_value) if not isinstance(raw_value, bool) else 0
|
|
9883
|
+
except (TypeError, ValueError):
|
|
9884
|
+
return _err(f"{key} must be a positive integer.")
|
|
9885
|
+
if n <= 0:
|
|
9886
|
+
return _err(f"{key} must be a positive integer.")
|
|
9887
|
+
set_or_clear(key, raw_value, n)
|
|
9642
9888
|
elif key == "default_post_operation_page":
|
|
9643
9889
|
normalized = _normalize_setup_choice(
|
|
9644
9890
|
raw_value,
|
|
@@ -9796,6 +10042,10 @@ def _setup_clear_defaults(keys: Any, dry_run: bool) -> Dict[str, Any]:
|
|
|
9796
10042
|
"metadata_writeback_default": "media_analysis.metadata_writeback_default",
|
|
9797
10043
|
"ask_before_metadata_publish": "media_analysis.ask_before_metadata_publish",
|
|
9798
10044
|
"dry_run_first_default": "media_analysis.dry_run_first_default",
|
|
10045
|
+
"sampling_mode_default": "media_analysis.sampling_mode_default",
|
|
10046
|
+
"sampling_frames_per_minute": "media_analysis.sampling_frames_per_minute",
|
|
10047
|
+
"sampling_frame_floor": "media_analysis.sampling_frame_floor",
|
|
10048
|
+
"sampling_frame_ceiling": "media_analysis.sampling_frame_ceiling",
|
|
9799
10049
|
}
|
|
9800
10050
|
media_payload: Dict[str, Any] = {}
|
|
9801
10051
|
if clear_all or "media_analysis" in normalized_keys:
|
|
@@ -9898,6 +10148,14 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
9898
10148
|
"media_analysis.metadata_writeback_default": {"values": [True, False], "storage": _media_analysis_preferences_path()},
|
|
9899
10149
|
"media_analysis.ask_before_metadata_publish": {"values": [True, False], "storage": _media_analysis_preferences_path()},
|
|
9900
10150
|
"media_analysis.dry_run_first_default": {"values": [True, False], "storage": _media_analysis_preferences_path()},
|
|
10151
|
+
"media_analysis.sampling_mode_default": {
|
|
10152
|
+
"description": "Frame-sampling mode for visual analysis. 'ask' prompts on first analysis to set a standing default. fixed=Economy (flat frames), per_minute=Balanced (duration-scaled), adaptive_capped=Thorough (content-aware, bounded — recommended), adaptive=Thorough uncapped.",
|
|
10153
|
+
"values": ["ask", "fixed", "per_minute", "adaptive_capped", "adaptive"],
|
|
10154
|
+
"storage": _media_analysis_preferences_path(),
|
|
10155
|
+
},
|
|
10156
|
+
"media_analysis.sampling_frames_per_minute": {"description": "Frames per minute for Balanced mode (also seeds Thorough on short clips).", "values": "number > 0 (default 4)", "storage": _media_analysis_preferences_path()},
|
|
10157
|
+
"media_analysis.sampling_frame_floor": {"description": "Minimum frames per clip for duration/content-scaled modes.", "values": "integer > 0 (default 3)", "storage": _media_analysis_preferences_path()},
|
|
10158
|
+
"media_analysis.sampling_frame_ceiling": {"description": "Maximum frames per clip for Balanced + Thorough modes (the Thorough per-clip cap).", "values": "integer > 0 (default 80)", "storage": _media_analysis_preferences_path()},
|
|
9901
10159
|
"updates.mode": {
|
|
9902
10160
|
"description": "Local MCP update policy.",
|
|
9903
10161
|
"values": sorted(_SETUP_UPDATE_MODES),
|
|
@@ -13190,6 +13448,31 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
13190
13448
|
"""
|
|
13191
13449
|
p = _media_analysis_apply_setup_defaults(action, dict(params or {}))
|
|
13192
13450
|
|
|
13451
|
+
# First-run frame-sampling prompt: if the user has never chosen a sampling
|
|
13452
|
+
# mode (and didn't pass one this call), ask before spending any vision
|
|
13453
|
+
# tokens. Re-running with sampling_mode=<choice> saves it as the default;
|
|
13454
|
+
# passing sampling_mode explicitly any time is a one-off that skips this.
|
|
13455
|
+
_sampling_decision = p.get("_sampling_mode_decision") if isinstance(p, dict) else None
|
|
13456
|
+
if (
|
|
13457
|
+
isinstance(_sampling_decision, dict)
|
|
13458
|
+
and _sampling_decision.get("prompt_required")
|
|
13459
|
+
and action in {"analyze_clip", "analyze_file", "analyze_bin", "analyze_project", "analyze_sequence", "analyze_timeline", "start_batch_job"}
|
|
13460
|
+
):
|
|
13461
|
+
return {
|
|
13462
|
+
"success": True,
|
|
13463
|
+
"status": "confirmation_required",
|
|
13464
|
+
"confirmation_required": True,
|
|
13465
|
+
"sampling_mode_prompt": _media_analysis_sampling_mode_prompt(),
|
|
13466
|
+
"recommended_sampling_mode": _media_analysis_module.RECOMMENDED_SAMPLING_MODE,
|
|
13467
|
+
"message": (
|
|
13468
|
+
"Choose a frame-sampling mode for visual analysis. Re-run with "
|
|
13469
|
+
"sampling_mode set to one of fixed/per_minute/adaptive_capped/adaptive "
|
|
13470
|
+
"(the chosen value is saved as your default), or set it in the control "
|
|
13471
|
+
"panel under Analysis Modes. Pass sampling_mode per-call any time for a one-off."
|
|
13472
|
+
),
|
|
13473
|
+
"preferences_path": _media_analysis_preferences_path(),
|
|
13474
|
+
}
|
|
13475
|
+
|
|
13193
13476
|
# E2 — capture original action + scope before the dispatch may rewrite
|
|
13194
13477
|
# `action` (e.g. analyze_clip → plan). Used by _e2_wrap below to attach
|
|
13195
13478
|
# an `escalation` block when a (scope, action) pair fails repeatedly.
|
|
@@ -18,17 +18,26 @@ deferred a "$ cost cap" axis. Token budgets are the unit of currency here.
|
|
|
18
18
|
|
|
19
19
|
| Dimension | minimal | standard | generous | unlimited |
|
|
20
20
|
|-----------|--------:|---------:|---------:|----------:|
|
|
21
|
-
| response_chars | 5,000 | 25,000
|
|
22
|
-
| vision_tokens_per_clip |
|
|
23
|
-
| frames_per_clip |
|
|
24
|
-
| vision_tokens_per_job |
|
|
25
|
-
| vision_tokens_per_day |
|
|
26
|
-
| wall_clock_seconds_per_call | 30 | 90
|
|
27
|
-
| max_frame_dim_pixels | 512 | 768
|
|
21
|
+
| response_chars | 5,000 | 25,000 | 100,000 | None |
|
|
22
|
+
| vision_tokens_per_clip | 16,000 | 100,000 | 250,000 | None |
|
|
23
|
+
| frames_per_clip | 12 | 80 | 200 | None |
|
|
24
|
+
| vision_tokens_per_job | 60,000 | 1,000,000 | 3,000,000 | None |
|
|
25
|
+
| vision_tokens_per_day | 150,000 | 2,000,000 | 6,000,000 | None |
|
|
26
|
+
| wall_clock_seconds_per_call | 30 | 90 | 300 | None |
|
|
27
|
+
| max_frame_dim_pixels | 512 | 768 | 1280 | None |
|
|
28
28
|
|
|
29
29
|
`minimal` = preview/triage mode. `standard` = realistic per-project default.
|
|
30
30
|
`generous` = high-fidelity analysis on a few specific clips. `unlimited` = all
|
|
31
31
|
guards off; use only when you're certain about the input size.
|
|
32
|
+
|
|
33
|
+
NOTE on `frames_per_clip`: this is a *safety ceiling*, not the primary frame
|
|
34
|
+
dial. How many frames a clip actually gets is chosen by the `sampling_mode`
|
|
35
|
+
(Economy/Balanced/Thorough — see media_analysis.SAMPLING_MODES), which is
|
|
36
|
+
duration- and content-aware. `frames_per_clip` only clips the result if the mode
|
|
37
|
+
would exceed it. The standard ceiling (80) matches the default Thorough ceiling
|
|
38
|
+
so the mode is never silently truncated; lower it to hard-cap cost, raise it for
|
|
39
|
+
unusually long/cutty clips. (Before sampling modes existed this defaulted to 8
|
|
40
|
+
and *was* the frame dial — that flat cap is what made long clips under-covered.)
|
|
32
41
|
"""
|
|
33
42
|
|
|
34
43
|
from __future__ import annotations
|
|
@@ -74,30 +83,30 @@ CAP_PRESETS: Dict[str, Caps] = {
|
|
|
74
83
|
PRESET_MINIMAL: Caps(
|
|
75
84
|
preset=PRESET_MINIMAL,
|
|
76
85
|
response_chars=5_000,
|
|
77
|
-
vision_tokens_per_clip=
|
|
78
|
-
frames_per_clip=
|
|
79
|
-
vision_tokens_per_job=
|
|
80
|
-
vision_tokens_per_day=
|
|
86
|
+
vision_tokens_per_clip=16_000,
|
|
87
|
+
frames_per_clip=12,
|
|
88
|
+
vision_tokens_per_job=60_000,
|
|
89
|
+
vision_tokens_per_day=150_000,
|
|
81
90
|
wall_clock_seconds_per_call=30,
|
|
82
91
|
max_frame_dim_pixels=512,
|
|
83
92
|
),
|
|
84
93
|
PRESET_STANDARD: Caps(
|
|
85
94
|
preset=PRESET_STANDARD,
|
|
86
95
|
response_chars=25_000,
|
|
87
|
-
vision_tokens_per_clip=
|
|
88
|
-
frames_per_clip=
|
|
89
|
-
vision_tokens_per_job=
|
|
90
|
-
vision_tokens_per_day=
|
|
96
|
+
vision_tokens_per_clip=100_000,
|
|
97
|
+
frames_per_clip=80,
|
|
98
|
+
vision_tokens_per_job=1_000_000,
|
|
99
|
+
vision_tokens_per_day=2_000_000,
|
|
91
100
|
wall_clock_seconds_per_call=90,
|
|
92
101
|
max_frame_dim_pixels=768,
|
|
93
102
|
),
|
|
94
103
|
PRESET_GENEROUS: Caps(
|
|
95
104
|
preset=PRESET_GENEROUS,
|
|
96
105
|
response_chars=100_000,
|
|
97
|
-
vision_tokens_per_clip=
|
|
98
|
-
frames_per_clip=
|
|
99
|
-
vision_tokens_per_job=
|
|
100
|
-
vision_tokens_per_day=
|
|
106
|
+
vision_tokens_per_clip=250_000,
|
|
107
|
+
frames_per_clip=200,
|
|
108
|
+
vision_tokens_per_job=3_000_000,
|
|
109
|
+
vision_tokens_per_day=6_000_000,
|
|
101
110
|
wall_clock_seconds_per_call=300,
|
|
102
111
|
max_frame_dim_pixels=1280,
|
|
103
112
|
),
|