davinci-resolve-mcp 2.98.0 → 2.98.2
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 +121 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/install.md +8 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +11 -4
- package/src/granular/common.py +1 -1
- package/src/server.py +29 -18
- package/src/utils/app_control.py +2 -0
- package/src/utils/image_qc.py +2 -1
- package/src/utils/media_analysis.py +12 -0
- package/src/utils/resolve_bridge.py +2 -1
- package/src/utils/resolve_runtime.py +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,120 @@
|
|
|
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.98.2
|
|
6
|
+
|
|
7
|
+
**A tool installed next to the server was invisible to it.** Reported in
|
|
8
|
+
[#153](https://github.com/samuelgursky/davinci-resolve-mcp/issues/153) by
|
|
9
|
+
@7daysdedicated as an encoding fault in the whisper probe. The probe turned out
|
|
10
|
+
not to be the cause, and the encoding fault turned out to be real somewhere
|
|
11
|
+
else, so both are fixed here.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **The server's own virtualenv was not searched for command-line tools.**
|
|
16
|
+
`pip install openai-whisper` writes a `whisper` executable into
|
|
17
|
+
`venv/Scripts` on Windows and `venv/bin` elsewhere, and that directory is on
|
|
18
|
+
PATH only while the environment is *activated* — which nothing does, since the
|
|
19
|
+
client launches `venv/python server.py` directly. So `shutil.which("whisper")`
|
|
20
|
+
returned None and `capabilities` reported `whisper_cli.available: false` for a
|
|
21
|
+
tool sitting beside the interpreter looking for it, with nothing in the
|
|
22
|
+
response to say why. The interpreter's script directory now leads the PATH
|
|
23
|
+
augmentation that already covered Homebrew and `/usr/local`. Not
|
|
24
|
+
Windows-specific: the same gap existed on macOS and Linux.
|
|
25
|
+
|
|
26
|
+
- **Text-mode reads of a child process decoded with the locale codec.** Nineteen
|
|
27
|
+
`subprocess.run(..., text=True)` calls across the server, panel, and utils had
|
|
28
|
+
no `encoding=`, so Python used the platform locale — cp1252 on a default
|
|
29
|
+
Windows install, a codec with no mapping for most of what a media tool prints.
|
|
30
|
+
A clip name in Japanese from the advanced-server bridge, a localized WMIC
|
|
31
|
+
banner, or a user script's output was then a `UnicodeDecodeError` raised
|
|
32
|
+
inside a call whose job was to answer a yes/no question. All nineteen now read
|
|
33
|
+
UTF-8 with `errors="replace"`, so the answer can be wrong in the last
|
|
34
|
+
character but never an exception. The WMIC read matters most: it feeds the
|
|
35
|
+
second-instance guard fixed in v2.97.6, and it must fail to "cannot tell".
|
|
36
|
+
|
|
37
|
+
- **Child Python processes are handed `PYTHONIOENCODING=utf-8`.** A script run
|
|
38
|
+
through `resolve_control` writes into a pipe, where Python picks the locale
|
|
39
|
+
codepage rather than the console's, so printing a non-Latin-1 character killed
|
|
40
|
+
the script with `UnicodeEncodeError` — and the failure read as the script's
|
|
41
|
+
fault rather than the pipe's. The transcription path already did this; the
|
|
42
|
+
script-runner did not.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`tests.test_child_process_text_encoding`** — a static walk over `src/` that
|
|
47
|
+
fails on any text-mode child read without an explicit `encoding=`. Static
|
|
48
|
+
because the exception needs a non-UTF-8 locale to reproduce and no machine in
|
|
49
|
+
this suite has one: the missing argument is visible in the source, the
|
|
50
|
+
`UnicodeDecodeError` is only visible in Tokyo. The walk carries a test that it
|
|
51
|
+
can still see an offender, since a guard that quietly matches nothing passes
|
|
52
|
+
forever. Also covers the venv script directory being on PATH, first, and
|
|
53
|
+
idempotently.
|
|
54
|
+
|
|
55
|
+
### Note on the report
|
|
56
|
+
|
|
57
|
+
The diagnosis in #153 named `whisper --help` as the probe. That string is
|
|
58
|
+
display text in the install-guidance table and is never executed — detection is
|
|
59
|
+
`shutil.which("whisper")` — and the transcription path already decoded UTF-8 and
|
|
60
|
+
already set `PYTHONIOENCODING`. The cause was the PATH gap the report mentioned
|
|
61
|
+
last, in passing, as an aside about `pip`. Worth stating plainly, because the
|
|
62
|
+
reporter's `whisper.cmd` workaround set the encoding *and* put a `whisper` on
|
|
63
|
+
PATH, and only the second half was doing the work.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- `docs/install.md` says which environment optional packages have to be
|
|
68
|
+
installed into, which is the part that was undocumented.
|
|
69
|
+
|
|
70
|
+
## What's New in v2.98.1
|
|
71
|
+
|
|
72
|
+
**The Bash guard could be walked past with a newline.** Reported and fixed in
|
|
73
|
+
[#152](https://github.com/samuelgursky/davinci-resolve-mcp/pull/152) by
|
|
74
|
+
@fitfam, who found both holes by executing payloads against the hook rather
|
|
75
|
+
than reading it.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **`split_commands()` did not split on newlines.** It knew `&&`, `||`, `;` and
|
|
80
|
+
`|`, so a multi-line block was a single segment: `argv0` came from the first
|
|
81
|
+
line, and a non-mutating first line returned before anything under it was
|
|
82
|
+
looked at. `mkdir -p footage/_superseded` then a newline then
|
|
83
|
+
`mv footage/clip.mp4 footage/_superseded/` passed, while the same two commands
|
|
84
|
+
joined by `;` denied. Either answer is defensible on its own; giving two
|
|
85
|
+
different answers to what a shell treats as the same command is what made the
|
|
86
|
+
guard trainable around.
|
|
87
|
+
|
|
88
|
+
- **The ffmpeg branch exempted an output that equalled an input.** The output
|
|
89
|
+
operand was dropped when it matched a `-i` value — which is exactly
|
|
90
|
+
`ffmpeg -i master.mp4 master.mp4`, the in-place overwrite the hook's own deny
|
|
91
|
+
message calls unrecoverable. Independent of the first hole: the single-line
|
|
92
|
+
form was allowed too.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- The trailing operand is no longer read as an output when it is the value of a
|
|
97
|
+
`-i` immediately before it. `ffmpeg -i master.mov` prints stream info and
|
|
98
|
+
writes nothing, and denying a read is how a guard teaches an agent to route
|
|
99
|
+
around it. `ffmpeg -i x.mp4 x.mp4` still denies — the token there is not
|
|
100
|
+
preceded by `-i`.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- **`tests.test_source_media_guard`** — the hook had no tests, which is why two
|
|
105
|
+
holes in it needed a payload matrix to find. It runs the hook the way the
|
|
106
|
+
harness does, a PreToolUse payload on stdin and a decision on stdout, and
|
|
107
|
+
fails on exactly the four rows this release fixes. The newline/`;` pair is
|
|
108
|
+
asserted as a pair: not that either answer is right, but that the guard cannot
|
|
109
|
+
give two.
|
|
110
|
+
|
|
111
|
+
### Note
|
|
112
|
+
|
|
113
|
+
- Splitting on newlines means multi-line text carried *inside* a command — a
|
|
114
|
+
heredoc body, a commit message — is now read as commands too, so text that
|
|
115
|
+
merely quotes `rm camera.mov` is denied. That is the right way round for a
|
|
116
|
+
tripwire; write the text to a file first. #152 was itself blocked this way on
|
|
117
|
+
its first attempt, by the fix it was proposing.
|
|
118
|
+
|
|
5
119
|
## What's New in v2.98.0
|
|
6
120
|
|
|
7
121
|
**The control panel could be driven by any web page you visited.** Reported
|
|
@@ -117,6 +231,13 @@ state file and panel pidfile move out of shared locations at the same time.
|
|
|
117
231
|
response, so an unrelated document sitting in the export folder could reach an
|
|
118
232
|
assistant's context. Only staged files are read now.
|
|
119
233
|
|
|
234
|
+
Live-verified after release on Studio 19.1.3.7 (macOS, Color page, Gallery
|
|
235
|
+
panel open): Resolve's `ExportStills` writes into the staging subdirectory — a
|
|
236
|
+
257 KB JPEG and its 28 KB `.drx` came back inlined — a bystander file in the
|
|
237
|
+
same folder survived untouched, the folder itself was kept, and
|
|
238
|
+
`cleanup: false` moved both files up into it. That subdirectory write was the
|
|
239
|
+
one claim the offline fixtures could not make.
|
|
240
|
+
|
|
120
241
|
### Added
|
|
121
242
|
|
|
122
243
|
- `tests.test_gallery_still_export_cleanup` runs the action against a real temp
|
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
|
-
> 本翻译对应 v2.98.
|
|
15
|
+
> 本翻译对应 v2.98.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/install.md
CHANGED
|
@@ -35,6 +35,14 @@ rather than guessing** if it is absent:
|
|
|
35
35
|
`python scripts/doctor.py` reports which are present. `media_analysis`
|
|
36
36
|
`capabilities` covers the analysis stack in more detail.
|
|
37
37
|
|
|
38
|
+
**Install them into the environment the server runs from.** A managed install
|
|
39
|
+
uses its own virtualenv, and `pip install` from an unrelated shell puts the
|
|
40
|
+
package somewhere that interpreter never looks. Use the venv's own pip —
|
|
41
|
+
`<install>/venv/bin/pip` (`venv\Scripts\pip.exe` on Windows) — or activate it
|
|
42
|
+
first. Command-line tools that come with a package (`openai-whisper` installs a
|
|
43
|
+
`whisper` executable) are found in that venv's script directory automatically
|
|
44
|
+
since v2.98.2; before that they had to be on PATH by hand.
|
|
45
|
+
|
|
38
46
|
Model weights carry their own licences, separate from the code that loads them.
|
|
39
47
|
|
|
40
48
|
> **Python 3.13 / 3.14:** these are **allowed** — setup will use them and warn.
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.98.
|
|
40
|
+
VERSION = "2.98.2"
|
|
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
|
@@ -14569,7 +14569,8 @@ def _launch_claude_code_terminal() -> Dict[str, Any]:
|
|
|
14569
14569
|
try:
|
|
14570
14570
|
check = subprocess.run(
|
|
14571
14571
|
["osascript", "-e", 'application "iTerm" is running'],
|
|
14572
|
-
capture_output=True, text=True,
|
|
14572
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14573
|
+
timeout=8,
|
|
14573
14574
|
)
|
|
14574
14575
|
iterm_running = (check.stdout or "").strip().lower() == "true"
|
|
14575
14576
|
except Exception:
|
|
@@ -14592,7 +14593,8 @@ def _launch_claude_code_terminal() -> Dict[str, Any]:
|
|
|
14592
14593
|
try:
|
|
14593
14594
|
proc = subprocess.run(
|
|
14594
14595
|
["osascript", "-e", script],
|
|
14595
|
-
capture_output=True, text=True,
|
|
14596
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14597
|
+
timeout=15,
|
|
14596
14598
|
)
|
|
14597
14599
|
if proc.returncode != 0:
|
|
14598
14600
|
return {"success": False, "error": (proc.stderr or "").strip() or "osascript failed"}
|
|
@@ -14623,7 +14625,8 @@ def _native_directory_picker(initial: Optional[str] = None) -> Dict[str, Any]:
|
|
|
14623
14625
|
import subprocess
|
|
14624
14626
|
proc = subprocess.run(
|
|
14625
14627
|
["osascript", "-e", script],
|
|
14626
|
-
capture_output=True, text=True,
|
|
14628
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
14629
|
+
timeout=120,
|
|
14627
14630
|
)
|
|
14628
14631
|
if proc.returncode != 0:
|
|
14629
14632
|
stderr = (proc.stderr or "").strip()
|
|
@@ -15194,7 +15197,11 @@ def _run_advanced_bridge(surface: str, op: str, args: Optional[Dict[str, Any]] =
|
|
|
15194
15197
|
# stdin=DEVNULL: never let a child race-read a protocol/stdin stream (api_truth).
|
|
15195
15198
|
proc = subprocess.run(
|
|
15196
15199
|
[node, bridge, str(surface), str(op), json.dumps(args or {})],
|
|
15197
|
-
|
|
15200
|
+
# The bridge answers in JSON that carries clip and project names;
|
|
15201
|
+
# decoding those with the locale codepage is how a non-ASCII name
|
|
15202
|
+
# turns a working panel call into a UnicodeDecodeError (#153).
|
|
15203
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
15204
|
+
timeout=timeout,
|
|
15198
15205
|
stdin=subprocess.DEVNULL, cwd=_advanced_root(),
|
|
15199
15206
|
)
|
|
15200
15207
|
except subprocess.TimeoutExpired:
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.98.
|
|
90
|
+
VERSION = "2.98.2"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.98.
|
|
14
|
+
VERSION = "2.98.2"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -1724,8 +1724,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1724
1724
|
import subprocess
|
|
1725
1725
|
proc = subprocess.run(
|
|
1726
1726
|
["osascript", "-e", 'tell application "DaVinci Resolve" to activate'],
|
|
1727
|
-
capture_output=True, text=True,
|
|
1728
|
-
stdin=subprocess.DEVNULL,
|
|
1727
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1728
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1729
1729
|
)
|
|
1730
1730
|
return {
|
|
1731
1731
|
"activated": proc.returncode == 0,
|
|
@@ -1738,8 +1738,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1738
1738
|
["powershell", "-NoProfile", "-Command",
|
|
1739
1739
|
"$s = New-Object -ComObject WScript.Shell; "
|
|
1740
1740
|
"$null = $s.AppActivate('DaVinci Resolve')"],
|
|
1741
|
-
capture_output=True, text=True,
|
|
1742
|
-
stdin=subprocess.DEVNULL,
|
|
1741
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1742
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1743
1743
|
)
|
|
1744
1744
|
return {
|
|
1745
1745
|
"activated": proc.returncode == 0,
|
|
@@ -1751,8 +1751,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1751
1751
|
if shutil.which("wmctrl"):
|
|
1752
1752
|
proc = subprocess.run(
|
|
1753
1753
|
["wmctrl", "-a", "DaVinci Resolve"],
|
|
1754
|
-
capture_output=True, text=True,
|
|
1755
|
-
stdin=subprocess.DEVNULL,
|
|
1754
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1755
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1756
1756
|
)
|
|
1757
1757
|
return {
|
|
1758
1758
|
"activated": proc.returncode == 0,
|
|
@@ -1762,8 +1762,8 @@ def _activate_resolve_window() -> Dict[str, Any]:
|
|
|
1762
1762
|
if shutil.which("xdotool"):
|
|
1763
1763
|
proc = subprocess.run(
|
|
1764
1764
|
["xdotool", "search", "--name", "DaVinci Resolve", "windowactivate"],
|
|
1765
|
-
capture_output=True, text=True,
|
|
1766
|
-
stdin=subprocess.DEVNULL,
|
|
1765
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1766
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1767
1767
|
)
|
|
1768
1768
|
return {
|
|
1769
1769
|
"activated": proc.returncode == 0,
|
|
@@ -1794,8 +1794,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1794
1794
|
)
|
|
1795
1795
|
proc = subprocess.run(
|
|
1796
1796
|
["osascript", "-e", script],
|
|
1797
|
-
capture_output=True, text=True,
|
|
1798
|
-
stdin=subprocess.DEVNULL,
|
|
1797
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1798
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1799
1799
|
)
|
|
1800
1800
|
return {
|
|
1801
1801
|
"sent": proc.returncode == 0,
|
|
@@ -1810,8 +1810,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1810
1810
|
"Add-Type -AssemblyName System.Windows.Forms; "
|
|
1811
1811
|
"Start-Sleep -Milliseconds 150; "
|
|
1812
1812
|
"[System.Windows.Forms.SendKeys]::SendWait('+i')"],
|
|
1813
|
-
capture_output=True, text=True,
|
|
1814
|
-
stdin=subprocess.DEVNULL,
|
|
1813
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1814
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1815
1815
|
)
|
|
1816
1816
|
return {
|
|
1817
1817
|
"sent": proc.returncode == 0,
|
|
@@ -1824,8 +1824,8 @@ def _send_resolve_keystroke_go_to_mark_in() -> Dict[str, Any]:
|
|
|
1824
1824
|
if shutil.which("xdotool"):
|
|
1825
1825
|
proc = subprocess.run(
|
|
1826
1826
|
["xdotool", "search", "--name", "DaVinci Resolve", "key", "--window", "%@", "shift+i"],
|
|
1827
|
-
capture_output=True, text=True,
|
|
1828
|
-
stdin=subprocess.DEVNULL,
|
|
1827
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
1828
|
+
timeout=5, stdin=subprocess.DEVNULL,
|
|
1829
1829
|
)
|
|
1830
1830
|
return {"sent": proc.returncode == 0, "platform": "linux", "tool": "xdotool", "shortcut": "Shift+I"}
|
|
1831
1831
|
return {"sent": False, "platform": sys.platform, "note": "no key-send tool found"}
|
|
@@ -15286,7 +15286,8 @@ def _port_owner_pid(host: str, port: int) -> Optional[int]:
|
|
|
15286
15286
|
try:
|
|
15287
15287
|
result = subprocess.run(
|
|
15288
15288
|
["lsof", "-nP", "-iTCP:" + str(port), "-sTCP:LISTEN", "-t"],
|
|
15289
|
-
capture_output=True, timeout=3, text=True,
|
|
15289
|
+
capture_output=True, timeout=3, text=True, encoding="utf-8",
|
|
15290
|
+
errors="replace", check=False,
|
|
15290
15291
|
stdin=subprocess.DEVNULL,
|
|
15291
15292
|
)
|
|
15292
15293
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -16561,6 +16562,8 @@ def _make_spec_hook_runner(timeout: float = 120.0):
|
|
|
16561
16562
|
stdin=subprocess.DEVNULL,
|
|
16562
16563
|
capture_output=True,
|
|
16563
16564
|
text=True,
|
|
16565
|
+
encoding="utf-8",
|
|
16566
|
+
errors="replace",
|
|
16564
16567
|
)
|
|
16565
16568
|
return proc.returncode == 0
|
|
16566
16569
|
except Exception as exc:
|
|
@@ -27141,7 +27144,8 @@ def _validate_lua_syntax(source: str) -> Dict[str, Any]:
|
|
|
27141
27144
|
f.write(source)
|
|
27142
27145
|
tmp = f.name
|
|
27143
27146
|
try:
|
|
27144
|
-
result = subprocess.run([luac, "-p", tmp], capture_output=True, text=True,
|
|
27147
|
+
result = subprocess.run([luac, "-p", tmp], capture_output=True, text=True,
|
|
27148
|
+
encoding="utf-8", errors="replace", timeout=10,
|
|
27145
27149
|
stdin=subprocess.DEVNULL)
|
|
27146
27150
|
if result.returncode == 0:
|
|
27147
27151
|
return {"valid": True, "errors": None, "checker": luac}
|
|
@@ -27694,6 +27698,12 @@ def _python_env_for_resolve() -> Dict[str, str]:
|
|
|
27694
27698
|
env = os.environ.copy()
|
|
27695
27699
|
env["RESOLVE_SCRIPT_API"] = RESOLVE_API_PATH
|
|
27696
27700
|
env["RESOLVE_SCRIPT_LIB"] = RESOLVE_LIB_PATH
|
|
27701
|
+
# The child writes its stdout into a pipe, so Python picks the locale
|
|
27702
|
+
# codepage rather than the console's — cp1252 on a default Windows install.
|
|
27703
|
+
# A script that prints a non-Latin-1 character then dies with
|
|
27704
|
+
# UnicodeEncodeError instead of returning its output, and the failure is
|
|
27705
|
+
# attributed to the script rather than to the pipe it was handed (#153).
|
|
27706
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
27697
27707
|
pp = env.get("PYTHONPATH", "")
|
|
27698
27708
|
if RESOLVE_MODULES_PATH not in pp:
|
|
27699
27709
|
env["PYTHONPATH"] = (RESOLVE_MODULES_PATH +
|
|
@@ -27741,7 +27751,8 @@ def _execute_python_script(path: str, args: List[str],
|
|
|
27741
27751
|
cmd = [sys.executable, "-c", _PY_SCRIPT_EXIT_GUARD, path] + [str(a) for a in args]
|
|
27742
27752
|
try:
|
|
27743
27753
|
result = safe_run(cmd, env=_python_env_for_resolve(),
|
|
27744
|
-
capture_output=True, text=True,
|
|
27754
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
27755
|
+
errors="replace", timeout=timeout)
|
|
27745
27756
|
except subprocess.TimeoutExpired as e:
|
|
27746
27757
|
return _err(f"Script timed out after {timeout}s. "
|
|
27747
27758
|
f"Partial stdout: {(e.stdout or '')[:1000]}")
|
package/src/utils/app_control.py
CHANGED
package/src/utils/image_qc.py
CHANGED
|
@@ -189,7 +189,8 @@ 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,
|
|
192
|
+
proc = subprocess.run(args, capture_output=True, text=True, encoding="utf-8",
|
|
193
|
+
errors="replace", timeout=30, check=False)
|
|
193
194
|
except (subprocess.TimeoutExpired, OSError):
|
|
194
195
|
return None
|
|
195
196
|
value = (proc.stdout or "").strip().lower()
|
|
@@ -335,8 +335,20 @@ def _ensure_path_includes_standard_tool_dirs() -> None:
|
|
|
335
335
|
/opt/homebrew/bin/ffprobe. Subprocess calls (subprocess.run(["ffprobe"...]))
|
|
336
336
|
then also fail to find the binary. Prepending the standard tool dirs here
|
|
337
337
|
fixes both detection and execution for every importer of this module.
|
|
338
|
+
|
|
339
|
+
The interpreter's own script directory is one of them, and it was missing.
|
|
340
|
+
A console script installed into the server's virtualenv — `pip install
|
|
341
|
+
openai-whisper` puts `whisper` in `venv/Scripts` on Windows, `venv/bin`
|
|
342
|
+
elsewhere — is only on PATH when that environment has been *activated*, and
|
|
343
|
+
nothing activates it: the client launches `venv/python server.py` directly.
|
|
344
|
+
So the tool was installed, working, and invisible, and `capabilities`
|
|
345
|
+
reported `whisper_cli.available: false` with no hint as to why (#153, where
|
|
346
|
+
the workaround that appeared to fix it was a shim placed on PATH by hand).
|
|
338
347
|
"""
|
|
339
348
|
candidates = [
|
|
349
|
+
# The venv this server is running from, first: a tool installed
|
|
350
|
+
# deliberately alongside it should win over an older copy elsewhere.
|
|
351
|
+
os.path.dirname(os.path.abspath(sys.executable)),
|
|
340
352
|
"/opt/homebrew/bin",
|
|
341
353
|
"/opt/homebrew/sbin",
|
|
342
354
|
"/usr/local/bin",
|
|
@@ -216,7 +216,8 @@ def _process_name(pid: int) -> str:
|
|
|
216
216
|
|
|
217
217
|
out = subprocess.run(
|
|
218
218
|
["ps", "-p", str(pid), "-o", "comm="],
|
|
219
|
-
capture_output=True, text=True,
|
|
219
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
220
|
+
timeout=5, check=False,
|
|
220
221
|
)
|
|
221
222
|
return (out.stdout or "").strip()
|
|
222
223
|
except Exception: # pragma: no cover - defensive
|
|
@@ -68,13 +68,23 @@ def _process_lines() -> Optional[List[str]]:
|
|
|
68
68
|
if platform.system().lower() == "windows":
|
|
69
69
|
# `tasklist` prints no command line, so the flag is invisible there.
|
|
70
70
|
# WMIC does print it and is what makes headless detection possible.
|
|
71
|
+
#
|
|
72
|
+
# Decoded explicitly: `text=True` alone decodes with the locale
|
|
73
|
+
# codepage, which raises UnicodeDecodeError on a byte cp1252 has no
|
|
74
|
+
# mapping for — and this read is the input to the second-instance
|
|
75
|
+
# guard, so it must fail to "cannot tell", never to an exception.
|
|
76
|
+
# ASCII is byte-identical under both codecs, so the matching this
|
|
77
|
+
# feeds is unchanged; what WMIC emits for a non-ASCII install path
|
|
78
|
+
# on a non-English Windows is not something we can verify here.
|
|
71
79
|
out = subprocess.run(
|
|
72
80
|
["wmic", "process", "where", "name='Resolve.exe'", "get", "CommandLine"],
|
|
73
|
-
capture_output=True, text=True,
|
|
81
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
82
|
+
timeout=10, check=False,
|
|
74
83
|
)
|
|
75
84
|
else:
|
|
76
85
|
out = subprocess.run(
|
|
77
|
-
["ps", "-Ao", "command="], capture_output=True, text=True,
|
|
86
|
+
["ps", "-Ao", "command="], capture_output=True, text=True,
|
|
87
|
+
encoding="utf-8", errors="replace", timeout=10, check=False,
|
|
78
88
|
)
|
|
79
89
|
if out.returncode != 0 and not out.stdout:
|
|
80
90
|
return None
|