davinci-resolve-mcp 4.8.27 → 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 +45 -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 +1 -1
- package/src/server.py +35 -14
- 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,51 @@
|
|
|
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
|
+
|
|
5
50
|
## What's New in v4.8.27 — Linux exports no longer redirected away from /tmp-named folders
|
|
6
51
|
|
|
7
52
|
### Fixed
|
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()}")
|
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
|
|
@@ -1919,7 +1919,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1919
1919
|
proc = subprocess.run(
|
|
1920
1920
|
["osascript", "-e", 'tell application "DaVinci Resolve" to activate'],
|
|
1921
1921
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1922
|
-
timeout=5,
|
|
1922
|
+
timeout=5,
|
|
1923
|
+
stdin=subprocess.DEVNULL,
|
|
1923
1924
|
)
|
|
1924
1925
|
return {
|
|
1925
1926
|
"activated": proc.returncode == 0,
|
|
@@ -1933,7 +1934,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1933
1934
|
"$s = New-Object -ComObject WScript.Shell; "
|
|
1934
1935
|
"$null = $s.AppActivate('DaVinci Resolve')"],
|
|
1935
1936
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1936
|
-
timeout=5,
|
|
1937
|
+
timeout=5,
|
|
1938
|
+
stdin=subprocess.DEVNULL,
|
|
1937
1939
|
)
|
|
1938
1940
|
return {
|
|
1939
1941
|
"activated": proc.returncode == 0,
|
|
@@ -1946,7 +1948,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1946
1948
|
proc = subprocess.run(
|
|
1947
1949
|
["wmctrl", "-a", "DaVinci Resolve"],
|
|
1948
1950
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1949
|
-
timeout=5,
|
|
1951
|
+
timeout=5,
|
|
1952
|
+
stdin=subprocess.DEVNULL,
|
|
1950
1953
|
)
|
|
1951
1954
|
return {
|
|
1952
1955
|
"activated": proc.returncode == 0,
|
|
@@ -1957,7 +1960,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1957
1960
|
proc = subprocess.run(
|
|
1958
1961
|
["xdotool", "search", "--name", "DaVinci Resolve", "windowactivate"],
|
|
1959
1962
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1960
|
-
timeout=5,
|
|
1963
|
+
timeout=5,
|
|
1964
|
+
stdin=subprocess.DEVNULL,
|
|
1961
1965
|
)
|
|
1962
1966
|
return {
|
|
1963
1967
|
"activated": proc.returncode == 0,
|
|
@@ -1989,7 +1993,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1989
1993
|
proc = subprocess.run(
|
|
1990
1994
|
["osascript", "-e", script],
|
|
1991
1995
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1992
|
-
timeout=5,
|
|
1996
|
+
timeout=5,
|
|
1997
|
+
stdin=subprocess.DEVNULL,
|
|
1993
1998
|
)
|
|
1994
1999
|
return {
|
|
1995
2000
|
"sent": proc.returncode == 0,
|
|
@@ -2005,7 +2010,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
2005
2010
|
"Start-Sleep -Milliseconds 150; "
|
|
2006
2011
|
"[System.Windows.Forms.SendKeys]::SendWait('+i')"],
|
|
2007
2012
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
2008
|
-
timeout=5,
|
|
2013
|
+
timeout=5,
|
|
2014
|
+
stdin=subprocess.DEVNULL,
|
|
2009
2015
|
)
|
|
2010
2016
|
return {
|
|
2011
2017
|
"sent": proc.returncode == 0,
|
|
@@ -2019,7 +2025,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
2019
2025
|
proc = subprocess.run(
|
|
2020
2026
|
["xdotool", "search", "--name", "DaVinci Resolve", "key", "--window", "%@", "shift+i"],
|
|
2021
2027
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
2022
|
-
timeout=5,
|
|
2028
|
+
timeout=5,
|
|
2029
|
+
stdin=subprocess.DEVNULL,
|
|
2023
2030
|
)
|
|
2024
2031
|
return {"sent": proc.returncode == 0, "platform": "linux", "tool": "xdotool", "shortcut": "Shift+I"}
|
|
2025
2032
|
return {"sent": False, "platform": sys.platform, "note": "no key-send tool found"}
|
|
@@ -13887,7 +13894,8 @@ def _ffprobe_media_summary(path: str) -> Optional[Dict[str, Any]]:
|
|
|
13887
13894
|
"format=duration,size:stream=codec_type,codec_name",
|
|
13888
13895
|
"-of", "json", path],
|
|
13889
13896
|
capture_output=True, encoding="utf-8", errors="replace",
|
|
13890
|
-
timeout=20,
|
|
13897
|
+
timeout=20,
|
|
13898
|
+
stdin=subprocess.DEVNULL,
|
|
13891
13899
|
)
|
|
13892
13900
|
except Exception:
|
|
13893
13901
|
return None
|
|
@@ -14848,7 +14856,7 @@ def _ffmpeg_scale_to_bytes(src_path: str, max_width: Optional[int], out_format:
|
|
|
14848
14856
|
# -2 keeps the height even (required by some encoders) and preserves AR.
|
|
14849
14857
|
args += ["-vf", f"scale='min({int(max_width)},iw)':-2:flags=lanczos"]
|
|
14850
14858
|
args += ["-frames:v", "1", tmp_out]
|
|
14851
|
-
proc = subprocess.run(args, capture_output=True, timeout=120)
|
|
14859
|
+
proc = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True, timeout=120)
|
|
14852
14860
|
if proc.returncode != 0:
|
|
14853
14861
|
return None, (proc.stderr.decode("utf-8", "replace").strip() or "ffmpeg failed")[:400]
|
|
14854
14862
|
with open(tmp_out, "rb") as handle:
|
|
@@ -33363,14 +33371,16 @@ def _install_threaded_tool_dispatch(fastmcp) -> int:
|
|
|
33363
33371
|
to a worker thread keeps the event loop servicing the transport. Bodies are
|
|
33364
33372
|
serialized on _bridge_lock so the single-threaded Resolve bridge is never
|
|
33365
33373
|
entered concurrently. A body that outlives a client cancellation runs to
|
|
33366
|
-
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.
|
|
33367
33376
|
|
|
33368
33377
|
Couples to mcp SDK private attrs (ToolManager._tools, Tool.fn /
|
|
33369
|
-
Tool.is_async; verified on mcp 1.27). Best-effort: if that shape
|
|
33370
|
-
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.
|
|
33371
33380
|
"""
|
|
33372
33381
|
import functools
|
|
33373
33382
|
import anyio
|
|
33383
|
+
from anyio.lowlevel import checkpoint_if_cancelled
|
|
33374
33384
|
|
|
33375
33385
|
manager = getattr(fastmcp, "_tool_manager", None)
|
|
33376
33386
|
tools = getattr(manager, "_tools", None)
|
|
@@ -33383,7 +33393,18 @@ def _install_threaded_tool_dispatch(fastmcp) -> int:
|
|
|
33383
33393
|
def call():
|
|
33384
33394
|
with _bridge_lock:
|
|
33385
33395
|
return fn(**kwargs)
|
|
33386
|
-
|
|
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()
|
|
33387
33408
|
return run_off_thread
|
|
33388
33409
|
|
|
33389
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:
|