davinci-resolve-mcp 4.8.26 → 4.8.28
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 +71 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +3 -0
- package/src/granular/common.py +5 -2
- package/src/server.py +42 -15
- package/src/utils/app_control.py +4 -3
- package/src/utils/audio_fairlight_live_probe.py +1 -1
- package/src/utils/color_grade_live_probe.py +1 -1
- package/src/utils/embeddings.py +1 -1
- package/src/utils/fusion_composition_live_probe.py +1 -1
- package/src/utils/image_qc.py +2 -2
- package/src/utils/media_analysis.py +7 -1
- package/src/utils/media_pool_ingest_live_probe.py +1 -1
- package/src/utils/prebalance.py +1 -1
- package/src/utils/private_state.py +1 -0
- package/src/utils/render_deliver_live_probe.py +1 -1
- package/src/utils/resolve_bridge.py +1 -0
- package/src/utils/resolve_runtime.py +2 -2
- package/src/utils/review_annotation_live_probe.py +1 -1
- package/src/utils/sound_density.py +1 -1
- package/src/utils/sync_detection.py +2 -2
- package/src/utils/timeline_conform_live_probe.py +1 -1
- package/src/utils/timeline_kernel_live_probe.py +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,77 @@
|
|
|
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 v4.8.28 — ffmpeg can no longer hang on the protocol stream; a cancelled call no longer kills the server
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **ffmpeg could hang indefinitely while reading the server's stdin.** Every
|
|
10
|
+
ffmpeg/ffprobe analysis pass (`edit_engine plan_silence_ripple`, media
|
|
11
|
+
analysis, sync detection, sound density and the rest) ran through a
|
|
12
|
+
runner that let the child inherit the server's stdin. Over stdio that is
|
|
13
|
+
the JSON-RPC stream. ffmpeg polls stdin for keyboard commands; the `c` in
|
|
14
|
+
`"jsonrpc"` opens its interactive command prompt, which then waits for a
|
|
15
|
+
newline while consuming the protocol bytes it reads. Measured with ffmpeg
|
|
16
|
+
9.0.2: a partial frame arriving mid-pass hung a silencedetect run until it
|
|
17
|
+
was killed, and the same run with `stdin=subprocess.DEVNULL` finished
|
|
18
|
+
normally. The runner, and all 29 other call sites under `src/` that spawned
|
|
19
|
+
a child without `stdin=`, now pass `stdin=subprocess.DEVNULL`. Reported on
|
|
20
|
+
Windows with the free 21.0.2 bridge (#272); the mechanism was reproduced on
|
|
21
|
+
macOS, and the Windows build was not available to test.
|
|
22
|
+
- **A tool call cancelled by the client could take the whole session down.**
|
|
23
|
+
Tool bodies run in a worker thread, and the wait for that thread is
|
|
24
|
+
shielded from cancellation. When the client cancelled (for example after
|
|
25
|
+
its own timeout) and the body finished later, its result came back as if
|
|
26
|
+
nothing had happened. The SDK then tried to answer a request it had already
|
|
27
|
+
answered "cancelled", and its `Request already responded to` assertion
|
|
28
|
+
closed the connection. The pending cancellation is now raised once the
|
|
29
|
+
body returns, which the SDK treats as the cancellation it already
|
|
30
|
+
acknowledged. The body still runs to completion, so Resolve is never left
|
|
31
|
+
half-mutated; only its result is discarded.
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
|
|
35
|
+
- `tests/test_subprocess_stdin_discipline.py` (new) fails the suite on any
|
|
36
|
+
`subprocess.run/Popen/call/check_call/check_output` under `src/` that does
|
|
37
|
+
not pass `stdin=` or `input=`, in any import spelling. With the fix
|
|
38
|
+
reverted it names all 30 sites (the runner plus the 29 others).
|
|
39
|
+
- `tests/test_threaded_tool_dispatch.py` gains an end-to-end case over the
|
|
40
|
+
real SDK's in-memory transport: it cancels a call mid-body, lets the body
|
|
41
|
+
finish, and requires the next call to succeed. With the fix reverted, it
|
|
42
|
+
fails with the reported `AssertionError`.
|
|
43
|
+
|
|
44
|
+
### Validation
|
|
45
|
+
|
|
46
|
+
- No Resolve scripting call changed. Both mechanisms were reproduced before
|
|
47
|
+
the fix and confirmed fixed after it, on macOS with ffmpeg 9.0.2 and
|
|
48
|
+
mcp 1.30.0. Not tested on Windows.
|
|
49
|
+
|
|
50
|
+
## What's New in v4.8.27 — Linux exports no longer redirected away from /tmp-named folders
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **On Linux, an output folder whose name merely began with `/tmp` or
|
|
55
|
+
`/var/tmp` was silently replaced.** `_resolve_safe_dir` tested
|
|
56
|
+
`path.startswith("/tmp")`, which is also true of `/tmpdata`, `/tmpfiles`,
|
|
57
|
+
`/tmp-scratch` and `/var/tmpdata`. Each was treated as a temp directory and
|
|
58
|
+
swapped for `~/Documents/resolve-stills`, and
|
|
59
|
+
`gallery_stills(grab_and_export)` then reported that folder as if it were the
|
|
60
|
+
one asked for. The same helper picks the output folder for `encrypt_dctl` and
|
|
61
|
+
the granular `save_project` export fallback. The Linux branch now compares by
|
|
62
|
+
path segment, as the macOS branch already did; `/tmp`, `/tmp/…`, `/var/tmp`
|
|
63
|
+
and `/var/tmp/…` still redirect. Both copies of the helper (`src/server.py`
|
|
64
|
+
and `src/granular/common.py`) are fixed. Thanks to @Dev-next-gen (#271).
|
|
65
|
+
|
|
66
|
+
### Tests
|
|
67
|
+
|
|
68
|
+
- `tests/test_granular_safe_dir.py` gains a Linux class covering both copies:
|
|
69
|
+
real temp paths still redirect, sibling names are left alone.
|
|
70
|
+
|
|
71
|
+
### Validation
|
|
72
|
+
|
|
73
|
+
- Path classification only, decided before any Resolve call; no Resolve
|
|
74
|
+
behavior changed. Live test not required.
|
|
75
|
+
|
|
5
76
|
## What's New in v4.8.26 — two dead layout-preset helpers removed
|
|
6
77
|
|
|
7
78
|
### Removed
|
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
|
-
> 本翻译对应 v4.8.
|
|
15
|
+
> 本翻译对应 v4.8.28 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.8.
|
|
40
|
+
VERSION = "4.8.28"
|
|
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
|
@@ -14738,6 +14738,7 @@ def _launch_claude_code_terminal() -> Dict[str, Any]:
|
|
|
14738
14738
|
try:
|
|
14739
14739
|
check = subprocess.run(
|
|
14740
14740
|
["osascript", "-e", 'application "iTerm" is running'],
|
|
14741
|
+
stdin=subprocess.DEVNULL,
|
|
14741
14742
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14742
14743
|
timeout=8,
|
|
14743
14744
|
)
|
|
@@ -14762,6 +14763,7 @@ def _launch_claude_code_terminal() -> Dict[str, Any]:
|
|
|
14762
14763
|
try:
|
|
14763
14764
|
proc = subprocess.run(
|
|
14764
14765
|
["osascript", "-e", script],
|
|
14766
|
+
stdin=subprocess.DEVNULL,
|
|
14765
14767
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14766
14768
|
timeout=15,
|
|
14767
14769
|
)
|
|
@@ -14794,6 +14796,7 @@ def _native_directory_picker(initial: Optional[str] = None) -> Dict[str, Any]:
|
|
|
14794
14796
|
import subprocess
|
|
14795
14797
|
proc = subprocess.run(
|
|
14796
14798
|
["osascript", "-e", script],
|
|
14799
|
+
stdin=subprocess.DEVNULL,
|
|
14797
14800
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14798
14801
|
timeout=120,
|
|
14799
14802
|
)
|
package/src/granular/common.py
CHANGED
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.8.
|
|
94
|
+
VERSION = "4.8.28"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -377,7 +377,10 @@ def _resolve_safe_dir(path):
|
|
|
377
377
|
# silently into both, same as /var/folders (matches src/server.py).
|
|
378
378
|
_is_sandbox = path.startswith(("/var/", "/private/var/", "/tmp/", "/private/tmp/")) or path in ("/tmp", "/private/tmp")
|
|
379
379
|
elif platform.system() == "Linux":
|
|
380
|
-
|
|
380
|
+
# By segment, not by character prefix (matches src/server.py): a sibling
|
|
381
|
+
# of /tmp whose name merely begins with it — /tmpfiles, /tmp-scratch,
|
|
382
|
+
# /var/tmpdata — cleared `startswith("/tmp")` and was redirected.
|
|
383
|
+
_is_sandbox = path.startswith(("/tmp/", "/var/tmp/")) or path in ("/tmp", "/var/tmp")
|
|
381
384
|
elif platform.system() == "Windows":
|
|
382
385
|
try:
|
|
383
386
|
_is_sandbox = os.path.commonpath([os.path.abspath(path), os.path.abspath(system_temp)]) == os.path.abspath(system_temp)
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.8.
|
|
14
|
+
VERSION = "4.8.28"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -1421,7 +1421,13 @@ def _resolve_safe_dir(path):
|
|
|
1421
1421
|
# silently fails into both (live-verified 2026-07-03), same as /var/folders.
|
|
1422
1422
|
_is_sandbox = path.startswith(("/var/", "/private/var/", "/tmp/", "/private/tmp/")) or path in ("/tmp", "/private/tmp")
|
|
1423
1423
|
elif platform.system() == "Linux":
|
|
1424
|
-
|
|
1424
|
+
# By segment, not by character prefix: `startswith("/tmp")` also matched a
|
|
1425
|
+
# sibling of /tmp whose name merely begins with it — /tmpfiles,
|
|
1426
|
+
# /tmp-scratch, /var/tmpdata — so an export the caller aimed at one of
|
|
1427
|
+
# those was redirected away from it. The Darwin branch already compares
|
|
1428
|
+
# this way, and `/tmpfiles/out` is one of the paths
|
|
1429
|
+
# tests/test_granular_safe_dir.py asserts is left alone.
|
|
1430
|
+
_is_sandbox = path.startswith(("/tmp/", "/var/tmp/")) or path in ("/tmp", "/var/tmp")
|
|
1425
1431
|
elif platform.system() == "Windows":
|
|
1426
1432
|
# Check if path is under the system temp directory (e.g. AppData\Local\Temp)
|
|
1427
1433
|
try:
|
|
@@ -1913,7 +1919,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1913
1919
|
proc = subprocess.run(
|
|
1914
1920
|
["osascript", "-e", 'tell application "DaVinci Resolve" to activate'],
|
|
1915
1921
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1916
|
-
timeout=5,
|
|
1922
|
+
timeout=5,
|
|
1923
|
+
stdin=subprocess.DEVNULL,
|
|
1917
1924
|
)
|
|
1918
1925
|
return {
|
|
1919
1926
|
"activated": proc.returncode == 0,
|
|
@@ -1927,7 +1934,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1927
1934
|
"$s = New-Object -ComObject WScript.Shell; "
|
|
1928
1935
|
"$null = $s.AppActivate('DaVinci Resolve')"],
|
|
1929
1936
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1930
|
-
timeout=5,
|
|
1937
|
+
timeout=5,
|
|
1938
|
+
stdin=subprocess.DEVNULL,
|
|
1931
1939
|
)
|
|
1932
1940
|
return {
|
|
1933
1941
|
"activated": proc.returncode == 0,
|
|
@@ -1940,7 +1948,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1940
1948
|
proc = subprocess.run(
|
|
1941
1949
|
["wmctrl", "-a", "DaVinci Resolve"],
|
|
1942
1950
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1943
|
-
timeout=5,
|
|
1951
|
+
timeout=5,
|
|
1952
|
+
stdin=subprocess.DEVNULL,
|
|
1944
1953
|
)
|
|
1945
1954
|
return {
|
|
1946
1955
|
"activated": proc.returncode == 0,
|
|
@@ -1951,7 +1960,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1951
1960
|
proc = subprocess.run(
|
|
1952
1961
|
["xdotool", "search", "--name", "DaVinci Resolve", "windowactivate"],
|
|
1953
1962
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1954
|
-
timeout=5,
|
|
1963
|
+
timeout=5,
|
|
1964
|
+
stdin=subprocess.DEVNULL,
|
|
1955
1965
|
)
|
|
1956
1966
|
return {
|
|
1957
1967
|
"activated": proc.returncode == 0,
|
|
@@ -1983,7 +1993,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1983
1993
|
proc = subprocess.run(
|
|
1984
1994
|
["osascript", "-e", script],
|
|
1985
1995
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1986
|
-
timeout=5,
|
|
1996
|
+
timeout=5,
|
|
1997
|
+
stdin=subprocess.DEVNULL,
|
|
1987
1998
|
)
|
|
1988
1999
|
return {
|
|
1989
2000
|
"sent": proc.returncode == 0,
|
|
@@ -1999,7 +2010,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1999
2010
|
"Start-Sleep -Milliseconds 150; "
|
|
2000
2011
|
"[System.Windows.Forms.SendKeys]::SendWait('+i')"],
|
|
2001
2012
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
2002
|
-
timeout=5,
|
|
2013
|
+
timeout=5,
|
|
2014
|
+
stdin=subprocess.DEVNULL,
|
|
2003
2015
|
)
|
|
2004
2016
|
return {
|
|
2005
2017
|
"sent": proc.returncode == 0,
|
|
@@ -2013,7 +2025,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
2013
2025
|
proc = subprocess.run(
|
|
2014
2026
|
["xdotool", "search", "--name", "DaVinci Resolve", "key", "--window", "%@", "shift+i"],
|
|
2015
2027
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
2016
|
-
timeout=5,
|
|
2028
|
+
timeout=5,
|
|
2029
|
+
stdin=subprocess.DEVNULL,
|
|
2017
2030
|
)
|
|
2018
2031
|
return {"sent": proc.returncode == 0, "platform": "linux", "tool": "xdotool", "shortcut": "Shift+I"}
|
|
2019
2032
|
return {"sent": False, "platform": sys.platform, "note": "no key-send tool found"}
|
|
@@ -13881,7 +13894,8 @@ def _ffprobe_media_summary(path: str) -> Optional[Dict[str, Any]]:
|
|
|
13881
13894
|
"format=duration,size:stream=codec_type,codec_name",
|
|
13882
13895
|
"-of", "json", path],
|
|
13883
13896
|
capture_output=True, encoding="utf-8", errors="replace",
|
|
13884
|
-
timeout=20,
|
|
13897
|
+
timeout=20,
|
|
13898
|
+
stdin=subprocess.DEVNULL,
|
|
13885
13899
|
)
|
|
13886
13900
|
except Exception:
|
|
13887
13901
|
return None
|
|
@@ -14842,7 +14856,7 @@ def _ffmpeg_scale_to_bytes(src_path: str, max_width: Optional[int], out_format:
|
|
|
14842
14856
|
# -2 keeps the height even (required by some encoders) and preserves AR.
|
|
14843
14857
|
args += ["-vf", f"scale='min({int(max_width)},iw)':-2:flags=lanczos"]
|
|
14844
14858
|
args += ["-frames:v", "1", tmp_out]
|
|
14845
|
-
proc = subprocess.run(args, capture_output=True, timeout=120)
|
|
14859
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True, timeout=120)
|
|
14846
14860
|
if proc.returncode != 0:
|
|
14847
14861
|
return None, (proc.stderr.decode("utf-8", "replace").strip() or "ffmpeg failed")[:400]
|
|
14848
14862
|
with open(tmp_out, "rb") as handle:
|
|
@@ -33357,14 +33371,16 @@ def _install_threaded_tool_dispatch(fastmcp) -> int:
|
|
|
33357
33371
|
to a worker thread keeps the event loop servicing the transport. Bodies are
|
|
33358
33372
|
serialized on _bridge_lock so the single-threaded Resolve bridge is never
|
|
33359
33373
|
entered concurrently. A body that outlives a client cancellation runs to
|
|
33360
|
-
completion (the bridge is never left half-mutated) still holding the lock
|
|
33374
|
+
completion (the bridge is never left half-mutated) still holding the lock,
|
|
33375
|
+
and its result is then discarded rather than sent.
|
|
33361
33376
|
|
|
33362
33377
|
Couples to mcp SDK private attrs (ToolManager._tools, Tool.fn /
|
|
33363
|
-
Tool.is_async; verified on mcp 1.27). Best-effort: if that shape
|
|
33364
|
-
leave the tools as-is, falling back to the current inline behavior.
|
|
33378
|
+
Tool.is_async; verified on mcp 1.27 and 1.30). Best-effort: if that shape
|
|
33379
|
+
changes, leave the tools as-is, falling back to the current inline behavior.
|
|
33365
33380
|
"""
|
|
33366
33381
|
import functools
|
|
33367
33382
|
import anyio
|
|
33383
|
+
from anyio.lowlevel import checkpoint_if_cancelled
|
|
33368
33384
|
|
|
33369
33385
|
manager = getattr(fastmcp, "_tool_manager", None)
|
|
33370
33386
|
tools = getattr(manager, "_tools", None)
|
|
@@ -33377,7 +33393,18 @@ def _install_threaded_tool_dispatch(fastmcp) -> int:
|
|
|
33377
33393
|
def call():
|
|
33378
33394
|
with _bridge_lock:
|
|
33379
33395
|
return fn(**kwargs)
|
|
33380
|
-
|
|
33396
|
+
try:
|
|
33397
|
+
return await anyio.to_thread.run_sync(call)
|
|
33398
|
+
finally:
|
|
33399
|
+
# run_sync shields its wait, so a client cancellation that lands
|
|
33400
|
+
# while the body runs is not raised there: the result comes back
|
|
33401
|
+
# as if nothing happened, the SDK tries to answer a request it
|
|
33402
|
+
# already answered "cancelled", and its "Request already
|
|
33403
|
+
# responded to" assert takes the whole session down (#272).
|
|
33404
|
+
# Raise the pending cancellation instead — the SDK expects it
|
|
33405
|
+
# and suppresses the duplicate response. Also covers a body
|
|
33406
|
+
# that raised after the cancel.
|
|
33407
|
+
await checkpoint_if_cancelled()
|
|
33381
33408
|
return run_off_thread
|
|
33382
33409
|
|
|
33383
33410
|
wrapped = 0
|
package/src/utils/app_control.py
CHANGED
|
@@ -32,6 +32,7 @@ def _run_app_command(
|
|
|
32
32
|
try:
|
|
33
33
|
result = subprocess.run(
|
|
34
34
|
cmd,
|
|
35
|
+
stdin=subprocess.DEVNULL,
|
|
35
36
|
check=False,
|
|
36
37
|
capture_output=True,
|
|
37
38
|
text=True,
|
|
@@ -258,11 +259,11 @@ def restart_resolve_app(resolve_obj, wait_seconds: int = 5) -> bool:
|
|
|
258
259
|
logger.info("Attempting to start Resolve")
|
|
259
260
|
|
|
260
261
|
if platform.system().lower() == 'darwin':
|
|
261
|
-
subprocess.Popen(['open', resolve_path])
|
|
262
|
+
subprocess.Popen(['open', resolve_path], stdin=subprocess.DEVNULL)
|
|
262
263
|
elif platform.system().lower() == 'windows':
|
|
263
|
-
subprocess.Popen([resolve_path])
|
|
264
|
+
subprocess.Popen([resolve_path], stdin=subprocess.DEVNULL)
|
|
264
265
|
elif platform.system().lower() == 'linux':
|
|
265
|
-
subprocess.Popen([resolve_path])
|
|
266
|
+
subprocess.Popen([resolve_path], stdin=subprocess.DEVNULL)
|
|
266
267
|
|
|
267
268
|
return True
|
|
268
269
|
except Exception as e:
|
|
@@ -48,7 +48,7 @@ def _record_tool_result(
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
51
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True, timeout=120)
|
|
51
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True, timeout=120)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _make_synthetic_media(work_dir: Path) -> Dict[str, Path]:
|
|
@@ -91,7 +91,7 @@ def _call_confirmed(tool, action: str, params: Dict[str, Any]) -> Dict[str, Any]
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
94
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
94
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True)
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def _make_synthetic_video(work_dir: Path) -> Path:
|
package/src/utils/embeddings.py
CHANGED
|
@@ -358,7 +358,7 @@ def _extract_audio_window(file_path: str, start_seconds: float, duration_seconds
|
|
|
358
358
|
"-f", "f32le", "-",
|
|
359
359
|
]
|
|
360
360
|
try:
|
|
361
|
-
proc = subprocess.run(command, capture_output=True, timeout=120, check=False)
|
|
361
|
+
proc = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True, timeout=120, check=False)
|
|
362
362
|
except (OSError, subprocess.TimeoutExpired):
|
|
363
363
|
return None
|
|
364
364
|
if proc.returncode != 0 or not proc.stdout:
|
|
@@ -60,7 +60,7 @@ def _record_tool_result(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
63
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
63
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def _make_synthetic_video(work_dir: Path) -> Path:
|
package/src/utils/image_qc.py
CHANGED
|
@@ -170,7 +170,7 @@ def _decode_frame(
|
|
|
170
170
|
"-f", "rawvideo", "-",
|
|
171
171
|
]
|
|
172
172
|
try:
|
|
173
|
-
proc = subprocess.run(args, capture_output=True, timeout=120, check=False)
|
|
173
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True, timeout=120, check=False)
|
|
174
174
|
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
175
175
|
logger.debug("frame decode failed for %s: %s", path, exc)
|
|
176
176
|
return None
|
|
@@ -189,7 +189,7 @@ def _probe_color_transfer(path: str) -> Optional[str]:
|
|
|
189
189
|
"-show_entries", "stream=color_transfer", "-of", "default=nw=1:nk=1", path,
|
|
190
190
|
]
|
|
191
191
|
try:
|
|
192
|
-
proc = subprocess.run(args, capture_output=True, text=True, encoding="utf-8",
|
|
192
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True, text=True, encoding="utf-8",
|
|
193
193
|
errors="replace", timeout=30, check=False)
|
|
194
194
|
except (subprocess.TimeoutExpired, OSError):
|
|
195
195
|
return None
|
|
@@ -2489,6 +2489,7 @@ def _kill_process_tree(pid: int) -> None:
|
|
|
2489
2489
|
if os.name == "nt":
|
|
2490
2490
|
subprocess.run(
|
|
2491
2491
|
["taskkill", "/F", "/T", "/PID", str(pid)],
|
|
2492
|
+
stdin=subprocess.DEVNULL,
|
|
2492
2493
|
capture_output=True,
|
|
2493
2494
|
check=False,
|
|
2494
2495
|
)
|
|
@@ -2535,6 +2536,11 @@ def _run_command(
|
|
|
2535
2536
|
try:
|
|
2536
2537
|
proc = subprocess.Popen(
|
|
2537
2538
|
args,
|
|
2539
|
+
# Never the server's stdin: under stdio that is the JSON-RPC stream,
|
|
2540
|
+
# and ffmpeg reads it as keyboard commands. The "c" in "jsonrpc"
|
|
2541
|
+
# opens ffmpeg's command prompt, which then blocks waiting for a
|
|
2542
|
+
# newline while eating protocol bytes (#272).
|
|
2543
|
+
stdin=subprocess.DEVNULL,
|
|
2538
2544
|
stdout=subprocess.PIPE,
|
|
2539
2545
|
stderr=subprocess.PIPE,
|
|
2540
2546
|
env=env,
|
|
@@ -3473,7 +3479,7 @@ def _raw_frame(path: str, time_seconds: float, width: int = 96, height: int = 54
|
|
|
3473
3479
|
"-",
|
|
3474
3480
|
]
|
|
3475
3481
|
try:
|
|
3476
|
-
proc = subprocess.run(args, capture_output=True, timeout=180, check=False)
|
|
3482
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True, timeout=180, check=False)
|
|
3477
3483
|
except (subprocess.TimeoutExpired, OSError):
|
|
3478
3484
|
return None
|
|
3479
3485
|
expected = width * height * 3
|
|
@@ -78,7 +78,7 @@ def _record_tool_result(
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
81
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
81
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True)
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _make_synthetic_assets(work_dir: Path) -> Dict[str, Path]:
|
package/src/utils/prebalance.py
CHANGED
|
@@ -208,7 +208,7 @@ def measure_frame_levels(media_path: str, at_seconds: float = 0.0) -> Dict[str,
|
|
|
208
208
|
"-vf", f"scale={width}:{height}", "-f", "rawvideo", "-pix_fmt", "rgb24", "-",
|
|
209
209
|
]
|
|
210
210
|
try:
|
|
211
|
-
proc = subprocess.run(cmd, capture_output=True, timeout=30)
|
|
211
|
+
proc = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, timeout=30)
|
|
212
212
|
except (subprocess.SubprocessError, OSError) as exc:
|
|
213
213
|
return {"success": False, "error": f"frame extraction failed: {exc}"}
|
|
214
214
|
expected = width * height * 3
|
|
@@ -36,6 +36,7 @@ def _restrict_windows_acl(path: str) -> None:
|
|
|
36
36
|
try:
|
|
37
37
|
subprocess.run(
|
|
38
38
|
["icacls", path, "/inheritance:r", "/grant:r", f"{user}:F"],
|
|
39
|
+
stdin=subprocess.DEVNULL,
|
|
39
40
|
capture_output=True, timeout=5, check=False,
|
|
40
41
|
)
|
|
41
42
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -60,7 +60,7 @@ def _record_tool_result(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
63
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True, timeout=120)
|
|
63
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True, timeout=120)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def _make_synthetic_video(work_dir: Path) -> Path:
|
|
@@ -72,7 +72,7 @@ def _run_ps(columns: str) -> Optional[List[str]]:
|
|
|
72
72
|
"""
|
|
73
73
|
try:
|
|
74
74
|
out = subprocess.run(
|
|
75
|
-
["ps", "-Awwo", columns], capture_output=True, text=True,
|
|
75
|
+
["ps", "-Awwo", columns], stdin=subprocess.DEVNULL, capture_output=True, text=True,
|
|
76
76
|
encoding="utf-8", errors="replace", timeout=10, check=False,
|
|
77
77
|
)
|
|
78
78
|
except Exception: # pragma: no cover - defensive; an unknown answer is None
|
|
@@ -211,7 +211,7 @@ def _process_table() -> Optional[List[Dict[str, Optional[str]]]]:
|
|
|
211
211
|
for reader, parse in WINDOWS_PROCESS_READERS:
|
|
212
212
|
try:
|
|
213
213
|
out = subprocess.run(
|
|
214
|
-
reader, capture_output=True, text=True, encoding="utf-8",
|
|
214
|
+
reader, stdin=subprocess.DEVNULL, capture_output=True, text=True, encoding="utf-8",
|
|
215
215
|
errors="replace", timeout=10, check=False,
|
|
216
216
|
)
|
|
217
217
|
except Exception:
|
|
@@ -60,7 +60,7 @@ def _record_tool_result(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
63
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True, timeout=120)
|
|
63
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True, timeout=120)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def _make_synthetic_video(work_dir: Path) -> Path:
|
|
@@ -221,7 +221,7 @@ def measure_track_levels(
|
|
|
221
221
|
if duration_seconds:
|
|
222
222
|
cmd[4:4] = ["-t", str(duration_seconds)]
|
|
223
223
|
try:
|
|
224
|
-
proc = subprocess.run(cmd, capture_output=True, timeout=120)
|
|
224
|
+
proc = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, timeout=120)
|
|
225
225
|
except (subprocess.SubprocessError, OSError) as exc:
|
|
226
226
|
return {"success": False, "error": f"decode failed for {name}: {exc}"}
|
|
227
227
|
if proc.returncode != 0 or not proc.stdout:
|
|
@@ -160,7 +160,7 @@ def sync_event_install_guidance(capabilities: Optional[Dict[str, Any]] = None) -
|
|
|
160
160
|
|
|
161
161
|
def _run_json(args: List[str], timeout: int) -> Tuple[int, Dict[str, Any], str]:
|
|
162
162
|
try:
|
|
163
|
-
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
|
163
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
|
164
164
|
except subprocess.TimeoutExpired:
|
|
165
165
|
return 124, {}, f"Command timed out after {timeout}s"
|
|
166
166
|
except OSError as exc:
|
|
@@ -397,7 +397,7 @@ def _decode_audio_window(
|
|
|
397
397
|
"-",
|
|
398
398
|
])
|
|
399
399
|
try:
|
|
400
|
-
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
|
400
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
|
401
401
|
except subprocess.TimeoutExpired:
|
|
402
402
|
return None, f"ffmpeg audio decode timed out after {timeout}s"
|
|
403
403
|
except OSError as exc:
|
|
@@ -54,7 +54,7 @@ def _record_tool_result(
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _run_ffmpeg(args: list[str]) -> None:
|
|
57
|
-
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
57
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], stdin=subprocess.DEVNULL, check=True)
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _make_synthetic_video(work_dir: Path, name: str, source: str, frequency: int) -> Path:
|