clipwright 0.1.1__tar.gz
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.
- clipwright-0.1.1/PKG-INFO +194 -0
- clipwright-0.1.1/README.md +181 -0
- clipwright-0.1.1/pyproject.toml +80 -0
- clipwright-0.1.1/src/clipwright/__init__.py +3 -0
- clipwright-0.1.1/src/clipwright/cli_io.py +25 -0
- clipwright-0.1.1/src/clipwright/envelope.py +64 -0
- clipwright-0.1.1/src/clipwright/errors.py +62 -0
- clipwright-0.1.1/src/clipwright/media.py +223 -0
- clipwright-0.1.1/src/clipwright/operations.py +165 -0
- clipwright-0.1.1/src/clipwright/otio_utils.py +324 -0
- clipwright-0.1.1/src/clipwright/process.py +165 -0
- clipwright-0.1.1/src/clipwright/project.py +209 -0
- clipwright-0.1.1/src/clipwright/py.typed +0 -0
- clipwright-0.1.1/src/clipwright/schemas.py +179 -0
- clipwright-0.1.1/src/clipwright/server.py +471 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: clipwright
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MCP server group wrapping FFmpeg/OTIO. Provides primitives to manipulate video editing workflows from AI agents.
|
|
5
|
+
Author: satoh-y-0323
|
|
6
|
+
Author-email: satoh-y-0323 <shoma.papa.0323@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: mcp[cli]>=1.27.2
|
|
9
|
+
Requires-Dist: opentimelineio>=0.18
|
|
10
|
+
Requires-Dist: pydantic>=2
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Clipwright
|
|
15
|
+
|
|
16
|
+
> For Japanese, see [README.ja.md](README.ja.md).
|
|
17
|
+
|
|
18
|
+
MCP server group wrapping FFmpeg/OTIO. Provides primitives to manipulate video editing workflows from AI agents.
|
|
19
|
+
|
|
20
|
+
## Prerequisite: FFmpeg
|
|
21
|
+
|
|
22
|
+
Clipwright requires ffprobe (runtime) and ffmpeg (test fixture generation) on PATH. Binaries are not included.
|
|
23
|
+
|
|
24
|
+
### Installation (Windows / WinGet)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
winget install Gyan.FFmpeg
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**PATH takes effect after shell restart.** When using with Claude Code, restart the app for PATH to become active.
|
|
31
|
+
|
|
32
|
+
If you cannot wait for a restart, specify environment variables directly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# runtime: ffprobe only
|
|
36
|
+
export CLIPWRIGHT_FFPROBE="C:/Users/<user>/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.1.1-full_build/bin/ffprobe.exe"
|
|
37
|
+
|
|
38
|
+
# test: both ffmpeg + ffprobe (for test fixture generation)
|
|
39
|
+
export CLIPWRIGHT_FFMPEG="C:/Users/<user>/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.1.1-full_build/bin/ffmpeg.exe"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Environment Variable Usage
|
|
43
|
+
|
|
44
|
+
| Variable | Purpose |
|
|
45
|
+
|----------|---------|
|
|
46
|
+
| `CLIPWRIGHT_FFPROBE` | **Runtime only**. Used by the `clipwright_inspect_media` tool |
|
|
47
|
+
| `CLIPWRIGHT_FFMPEG` | **Test only**. Used by the `sample_media` fixture in `conftest.py` |
|
|
48
|
+
|
|
49
|
+
> Runtime depends only on ffprobe. ffmpeg is used only for test fixture generation (design: [DC-AM-008]).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Development Environment Setup
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Install dependencies
|
|
57
|
+
uv sync --dev
|
|
58
|
+
|
|
59
|
+
# Run tests (with coverage)
|
|
60
|
+
uv run pytest --cov=clipwright --cov-report=term-missing
|
|
61
|
+
|
|
62
|
+
# lint / format
|
|
63
|
+
uv run ruff check src tests
|
|
64
|
+
uv run ruff format src tests
|
|
65
|
+
|
|
66
|
+
# Type checking
|
|
67
|
+
uv run mypy src
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Integration Test Prerequisites
|
|
71
|
+
|
|
72
|
+
To run integration tests (tests that actually invoke ffprobe/ffmpeg), ffmpeg / ffprobe must exist on PATH or the following environment variables must be set:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Specify path to ffprobe (used by runtime and integration tests)
|
|
76
|
+
export CLIPWRIGHT_FFPROBE="/path/to/ffprobe"
|
|
77
|
+
|
|
78
|
+
# Specify path to ffmpeg (used for test fixture generation)
|
|
79
|
+
export CLIPWRIGHT_FFMPEG="/path/to/ffmpeg"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If ffmpeg / ffprobe are already registered in PATH, setting environment variables is not required. If neither is found, integration tests are automatically skipped.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Development Notes: MCP Package
|
|
87
|
+
|
|
88
|
+
### Adopted Package
|
|
89
|
+
|
|
90
|
+
**Official MCP Python SDK** (`mcp[cli]`) is adopted (ADR-5 confirmed).
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
mcp[cli]>=1.27.2
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Importable via `from mcp.server.fastmcp import FastMCP`. Verified to work on Python 3.11 / Windows.
|
|
97
|
+
|
|
98
|
+
### Annotation Syntax (Adopted Version)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from mcp.server.fastmcp import FastMCP
|
|
102
|
+
from mcp.types import ToolAnnotations
|
|
103
|
+
|
|
104
|
+
mcp = FastMCP("clipwright")
|
|
105
|
+
|
|
106
|
+
@mcp.tool(
|
|
107
|
+
annotations=ToolAnnotations(
|
|
108
|
+
readOnlyHint=True,
|
|
109
|
+
destructiveHint=False,
|
|
110
|
+
idempotentHint=True,
|
|
111
|
+
openWorldHint=False,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
def clipwright_inspect_media(path: str) -> dict:
|
|
115
|
+
"""Probe a media file and return its information."""
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`ToolAnnotations` fields: `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`
|
|
120
|
+
|
|
121
|
+
### outputSchema / structured_output
|
|
122
|
+
|
|
123
|
+
When `mcp.tool(structured_output=True)` is specified, Pydantic model return values are reflected in outputSchema as JSON Schema.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pydantic import BaseModel
|
|
127
|
+
|
|
128
|
+
class MediaResult(BaseModel):
|
|
129
|
+
ok: bool
|
|
130
|
+
summary: str
|
|
131
|
+
|
|
132
|
+
@mcp.tool(structured_output=True)
|
|
133
|
+
def clipwright_inspect_media(path: str) -> MediaResult:
|
|
134
|
+
...
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## MCP Inspector Communication Procedure
|
|
140
|
+
|
|
141
|
+
How to manually verify the server using MCP Inspector (`@modelcontextprotocol/inspector`).
|
|
142
|
+
|
|
143
|
+
### Setup (Node.js Required)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Verify Node.js is installed
|
|
147
|
+
node --version
|
|
148
|
+
npx --version
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Starting the Server and Connecting
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Start MCP Inspector and connect the server via stdio
|
|
155
|
+
npx @modelcontextprotocol/inspector uv run python -m clipwright.server
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Browser opens automatically at `http://localhost:5173` (or access manually).
|
|
159
|
+
|
|
160
|
+
The tool list (`clipwright_init_project` / `clipwright_inspect_media` / `clipwright_read_timeline` / `clipwright_write_timeline`) appears in Inspector, and you can manually execute each tool.
|
|
161
|
+
|
|
162
|
+
### Expected Behavior
|
|
163
|
+
|
|
164
|
+
- 4 tools appear in the tool list
|
|
165
|
+
- Passing a non-existent path to `clipwright_inspect_media` returns an error envelope with `ok=false`
|
|
166
|
+
- If ffprobe is not set in PATH / environment variables, a `DEPENDENCY_MISSING` error is returned
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Architecture Overview
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
src/clipwright/
|
|
174
|
+
__init__.py # Version definition
|
|
175
|
+
schemas.py # Shared Pydantic types (contract surface)
|
|
176
|
+
envelope.py # Return value envelope + error formatting
|
|
177
|
+
errors.py # Error codes + ClipwrightError exception
|
|
178
|
+
process.py # Subprocess runner (shell=False / timeout required)
|
|
179
|
+
media.py # ffprobe wrapper
|
|
180
|
+
otio_utils.py # OTIO helpers
|
|
181
|
+
operations.py # Declarative edit operation types + application logic
|
|
182
|
+
project.py # Project directory management
|
|
183
|
+
server.py # FastMCP server (4 tools exposed)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Dependency direction: `schemas / envelope / errors` (contract surface) → `process / media / otio_utils / project` → `operations` → `server`
|
|
187
|
+
|
|
188
|
+
For details, see [docs/clipwright-spec.md](docs/clipwright-spec.md).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT — See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Clipwright
|
|
2
|
+
|
|
3
|
+
> For Japanese, see [README.ja.md](README.ja.md).
|
|
4
|
+
|
|
5
|
+
MCP server group wrapping FFmpeg/OTIO. Provides primitives to manipulate video editing workflows from AI agents.
|
|
6
|
+
|
|
7
|
+
## Prerequisite: FFmpeg
|
|
8
|
+
|
|
9
|
+
Clipwright requires ffprobe (runtime) and ffmpeg (test fixture generation) on PATH. Binaries are not included.
|
|
10
|
+
|
|
11
|
+
### Installation (Windows / WinGet)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
winget install Gyan.FFmpeg
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**PATH takes effect after shell restart.** When using with Claude Code, restart the app for PATH to become active.
|
|
18
|
+
|
|
19
|
+
If you cannot wait for a restart, specify environment variables directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# runtime: ffprobe only
|
|
23
|
+
export CLIPWRIGHT_FFPROBE="C:/Users/<user>/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.1.1-full_build/bin/ffprobe.exe"
|
|
24
|
+
|
|
25
|
+
# test: both ffmpeg + ffprobe (for test fixture generation)
|
|
26
|
+
export CLIPWRIGHT_FFMPEG="C:/Users/<user>/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.1.1-full_build/bin/ffmpeg.exe"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Environment Variable Usage
|
|
30
|
+
|
|
31
|
+
| Variable | Purpose |
|
|
32
|
+
|----------|---------|
|
|
33
|
+
| `CLIPWRIGHT_FFPROBE` | **Runtime only**. Used by the `clipwright_inspect_media` tool |
|
|
34
|
+
| `CLIPWRIGHT_FFMPEG` | **Test only**. Used by the `sample_media` fixture in `conftest.py` |
|
|
35
|
+
|
|
36
|
+
> Runtime depends only on ffprobe. ffmpeg is used only for test fixture generation (design: [DC-AM-008]).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Development Environment Setup
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Install dependencies
|
|
44
|
+
uv sync --dev
|
|
45
|
+
|
|
46
|
+
# Run tests (with coverage)
|
|
47
|
+
uv run pytest --cov=clipwright --cov-report=term-missing
|
|
48
|
+
|
|
49
|
+
# lint / format
|
|
50
|
+
uv run ruff check src tests
|
|
51
|
+
uv run ruff format src tests
|
|
52
|
+
|
|
53
|
+
# Type checking
|
|
54
|
+
uv run mypy src
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Integration Test Prerequisites
|
|
58
|
+
|
|
59
|
+
To run integration tests (tests that actually invoke ffprobe/ffmpeg), ffmpeg / ffprobe must exist on PATH or the following environment variables must be set:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Specify path to ffprobe (used by runtime and integration tests)
|
|
63
|
+
export CLIPWRIGHT_FFPROBE="/path/to/ffprobe"
|
|
64
|
+
|
|
65
|
+
# Specify path to ffmpeg (used for test fixture generation)
|
|
66
|
+
export CLIPWRIGHT_FFMPEG="/path/to/ffmpeg"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If ffmpeg / ffprobe are already registered in PATH, setting environment variables is not required. If neither is found, integration tests are automatically skipped.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Development Notes: MCP Package
|
|
74
|
+
|
|
75
|
+
### Adopted Package
|
|
76
|
+
|
|
77
|
+
**Official MCP Python SDK** (`mcp[cli]`) is adopted (ADR-5 confirmed).
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
mcp[cli]>=1.27.2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Importable via `from mcp.server.fastmcp import FastMCP`. Verified to work on Python 3.11 / Windows.
|
|
84
|
+
|
|
85
|
+
### Annotation Syntax (Adopted Version)
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from mcp.server.fastmcp import FastMCP
|
|
89
|
+
from mcp.types import ToolAnnotations
|
|
90
|
+
|
|
91
|
+
mcp = FastMCP("clipwright")
|
|
92
|
+
|
|
93
|
+
@mcp.tool(
|
|
94
|
+
annotations=ToolAnnotations(
|
|
95
|
+
readOnlyHint=True,
|
|
96
|
+
destructiveHint=False,
|
|
97
|
+
idempotentHint=True,
|
|
98
|
+
openWorldHint=False,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
def clipwright_inspect_media(path: str) -> dict:
|
|
102
|
+
"""Probe a media file and return its information."""
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`ToolAnnotations` fields: `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`
|
|
107
|
+
|
|
108
|
+
### outputSchema / structured_output
|
|
109
|
+
|
|
110
|
+
When `mcp.tool(structured_output=True)` is specified, Pydantic model return values are reflected in outputSchema as JSON Schema.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from pydantic import BaseModel
|
|
114
|
+
|
|
115
|
+
class MediaResult(BaseModel):
|
|
116
|
+
ok: bool
|
|
117
|
+
summary: str
|
|
118
|
+
|
|
119
|
+
@mcp.tool(structured_output=True)
|
|
120
|
+
def clipwright_inspect_media(path: str) -> MediaResult:
|
|
121
|
+
...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## MCP Inspector Communication Procedure
|
|
127
|
+
|
|
128
|
+
How to manually verify the server using MCP Inspector (`@modelcontextprotocol/inspector`).
|
|
129
|
+
|
|
130
|
+
### Setup (Node.js Required)
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Verify Node.js is installed
|
|
134
|
+
node --version
|
|
135
|
+
npx --version
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Starting the Server and Connecting
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Start MCP Inspector and connect the server via stdio
|
|
142
|
+
npx @modelcontextprotocol/inspector uv run python -m clipwright.server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Browser opens automatically at `http://localhost:5173` (or access manually).
|
|
146
|
+
|
|
147
|
+
The tool list (`clipwright_init_project` / `clipwright_inspect_media` / `clipwright_read_timeline` / `clipwright_write_timeline`) appears in Inspector, and you can manually execute each tool.
|
|
148
|
+
|
|
149
|
+
### Expected Behavior
|
|
150
|
+
|
|
151
|
+
- 4 tools appear in the tool list
|
|
152
|
+
- Passing a non-existent path to `clipwright_inspect_media` returns an error envelope with `ok=false`
|
|
153
|
+
- If ffprobe is not set in PATH / environment variables, a `DEPENDENCY_MISSING` error is returned
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Architecture Overview
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
src/clipwright/
|
|
161
|
+
__init__.py # Version definition
|
|
162
|
+
schemas.py # Shared Pydantic types (contract surface)
|
|
163
|
+
envelope.py # Return value envelope + error formatting
|
|
164
|
+
errors.py # Error codes + ClipwrightError exception
|
|
165
|
+
process.py # Subprocess runner (shell=False / timeout required)
|
|
166
|
+
media.py # ffprobe wrapper
|
|
167
|
+
otio_utils.py # OTIO helpers
|
|
168
|
+
operations.py # Declarative edit operation types + application logic
|
|
169
|
+
project.py # Project directory management
|
|
170
|
+
server.py # FastMCP server (4 tools exposed)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Dependency direction: `schemas / envelope / errors` (contract surface) → `process / media / otio_utils / project` → `operations` → `server`
|
|
174
|
+
|
|
175
|
+
For details, see [docs/clipwright-spec.md](docs/clipwright-spec.md).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT — See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clipwright"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "MCP server group wrapping FFmpeg/OTIO. Provides primitives to manipulate video editing workflows from AI agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "satoh-y-0323", email = "shoma.papa.0323@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp[cli]>=1.27.2",
|
|
13
|
+
"opentimelineio>=0.18",
|
|
14
|
+
"pydantic>=2",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.11.19,<0.12.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"mypy>=2.1.0",
|
|
24
|
+
"pytest>=9.0.3",
|
|
25
|
+
"pytest-cov>=7.1.0",
|
|
26
|
+
"pytest-mock>=3.15.1",
|
|
27
|
+
"ruff>=0.15.16",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# --- Ruff ---
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
target-version = "py311"
|
|
33
|
+
line-length = 88
|
|
34
|
+
# templates/ is a template containing placeholders (__TOOL__ etc). Excluded from lint/format.
|
|
35
|
+
extend-exclude = ["templates"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
|
|
39
|
+
ignore = []
|
|
40
|
+
|
|
41
|
+
[tool.ruff.format]
|
|
42
|
+
# Default ruff formatter is OK
|
|
43
|
+
|
|
44
|
+
# --- mypy ---
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
python_version = "3.11"
|
|
47
|
+
strict = true
|
|
48
|
+
warn_return_any = true
|
|
49
|
+
warn_unused_configs = true
|
|
50
|
+
disallow_untyped_defs = true
|
|
51
|
+
disallow_any_generics = true
|
|
52
|
+
# templates/ is a template containing placeholders. Excluded from type checking.
|
|
53
|
+
exclude = ["^templates/"]
|
|
54
|
+
|
|
55
|
+
# opentimelineio has no stubs, ignored with mypy strict
|
|
56
|
+
[[tool.mypy.overrides]]
|
|
57
|
+
module = "opentimelineio.*"
|
|
58
|
+
ignore_missing_imports = true
|
|
59
|
+
|
|
60
|
+
# --- pytest ---
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
addopts = "--strict-markers -q"
|
|
64
|
+
markers = [
|
|
65
|
+
"integration: integration test requiring actual ffmpeg/ffprobe binaries",
|
|
66
|
+
"slow: test with long execution time",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# --- coverage ---
|
|
70
|
+
[tool.coverage.run]
|
|
71
|
+
source = ["clipwright"]
|
|
72
|
+
omit = ["tests/*"]
|
|
73
|
+
|
|
74
|
+
[tool.coverage.report]
|
|
75
|
+
show_missing = true
|
|
76
|
+
skip_covered = false
|
|
77
|
+
|
|
78
|
+
# --- uv workspace ---
|
|
79
|
+
[tool.uv.workspace]
|
|
80
|
+
members = ["clipwright-bgm", "clipwright-loudness", "clipwright-noise", "clipwright-render", "clipwright-silence", "clipwright-transcribe", "clipwright-wrap"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""cli_io.py — Shared UTF-8 I/O helper for separate-process CLIs.
|
|
2
|
+
|
|
3
|
+
DRY home for the per-CLI UTF-8 pinning helper (CR L-2 / SR I-1). wrap_cli and
|
|
4
|
+
vad_cli import force_utf8_io() from here instead of carrying a local copy, so the
|
|
5
|
+
behaviour is defined once for every separate-process CLI that depends on core.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def force_utf8_io() -> None:
|
|
14
|
+
"""Pin stdin/stdout to UTF-8 so this CLI is correct regardless of host
|
|
15
|
+
locale or inherited PYTHONIOENCODING (cp932 on JP Windows otherwise).
|
|
16
|
+
|
|
17
|
+
MUST be called before the first read from stdin: TextIOWrapper.reconfigure
|
|
18
|
+
raises once buffered reading has begun. Calling it at the very top of main()
|
|
19
|
+
(before sys.stdin.read()) satisfies this. Safe to call even on a CLI that
|
|
20
|
+
never reads stdin (reconfigure before any read is a no-op there).
|
|
21
|
+
"""
|
|
22
|
+
for stream in (sys.stdin, sys.stdout):
|
|
23
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
24
|
+
if reconfigure is not None:
|
|
25
|
+
reconfigure(encoding="utf-8")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""envelope.py — Return value envelope construction helpers.
|
|
2
|
+
|
|
3
|
+
Thin helpers that normalise all tool return values to the standard format (§6.3 / §6.4).
|
|
4
|
+
Returns dict representations of ToolResult / ToolErrorResult, which are directly
|
|
5
|
+
compatible with FastMCP JSON serialisation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ok_result(
|
|
14
|
+
summary: str,
|
|
15
|
+
*,
|
|
16
|
+
data: dict[str, Any] | None = None,
|
|
17
|
+
artifacts: list[Any] | None = None,
|
|
18
|
+
warnings: list[str] | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Build a success envelope dict (§6.3 ToolResult form).
|
|
21
|
+
|
|
22
|
+
summary must include key points an AI needs to decide the next action
|
|
23
|
+
(counts, durations, maxima, etc.). Do not make it minimal.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
summary: Key points of the result (required).
|
|
27
|
+
data: Supplementary information (optional).
|
|
28
|
+
artifacts: List of references to output files (optional).
|
|
29
|
+
warnings: List of warning messages (optional).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A dict of the form { ok: True, summary, data, artifacts, warnings }.
|
|
33
|
+
"""
|
|
34
|
+
return {
|
|
35
|
+
"ok": True,
|
|
36
|
+
"summary": summary,
|
|
37
|
+
"data": data if data is not None else {},
|
|
38
|
+
"artifacts": artifacts if artifacts is not None else [],
|
|
39
|
+
"warnings": warnings if warnings is not None else [],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def error_result(code: str, message: str, hint: str) -> dict[str, Any]:
|
|
44
|
+
"""Build a failure envelope dict (§6.4 ToolErrorResult form).
|
|
45
|
+
|
|
46
|
+
message describes what happened; hint describes the concrete next step.
|
|
47
|
+
An empty hint violates the error contract (§6).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
code: String representation of an ErrorCode value.
|
|
51
|
+
message: What happened.
|
|
52
|
+
hint: Concrete, actionable next step.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A dict of the form { ok: False, error: { code, message, hint } }.
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"ok": False,
|
|
59
|
+
"error": {
|
|
60
|
+
"code": code,
|
|
61
|
+
"message": message,
|
|
62
|
+
"hint": hint,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""errors.py — Error code taxonomy and ClipwrightError exception.
|
|
2
|
+
|
|
3
|
+
The library layer raises ClipwrightError on failure;
|
|
4
|
+
server.py converts it to error_result at the MCP boundary.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ErrorCode(StrEnum):
|
|
13
|
+
"""Error codes shared across all Clipwright tools (§4 + §13.1 DC-AM-002/DC-AS-003).
|
|
14
|
+
|
|
15
|
+
Inherits str so values are JSON-serializable at API boundaries.
|
|
16
|
+
Each value equals its name string.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
DEPENDENCY_MISSING = "DEPENDENCY_MISSING"
|
|
20
|
+
"""External tool (ffmpeg/ffprobe, etc.) not found."""
|
|
21
|
+
INVALID_INPUT = "INVALID_INPUT"
|
|
22
|
+
"""Argument validation failed."""
|
|
23
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
24
|
+
"""No file exists at the given input path."""
|
|
25
|
+
PATH_NOT_ALLOWED = "PATH_NOT_ALLOWED"
|
|
26
|
+
"""Path validation failed (e.g., path traversal attempt)."""
|
|
27
|
+
SUBPROCESS_FAILED = "SUBPROCESS_FAILED"
|
|
28
|
+
"""External process exited with a non-zero return code."""
|
|
29
|
+
SUBPROCESS_TIMEOUT = "SUBPROCESS_TIMEOUT"
|
|
30
|
+
"""External process timed out."""
|
|
31
|
+
PROBE_FAILED = "PROBE_FAILED"
|
|
32
|
+
"""Failed to parse ffprobe output."""
|
|
33
|
+
OTIO_ERROR = "OTIO_ERROR"
|
|
34
|
+
"""Failed to read, write, or parse an OTIO file."""
|
|
35
|
+
PROJECT_NOT_FOUND = "PROJECT_NOT_FOUND"
|
|
36
|
+
"""clipwright.json not found."""
|
|
37
|
+
PROJECT_EXISTS = "PROJECT_EXISTS"
|
|
38
|
+
"""An existing project already exists at the target init location."""
|
|
39
|
+
UNSUPPORTED_OPERATION = "UNSUPPORTED_OPERATION"
|
|
40
|
+
"""Unknown or unsupported operation type."""
|
|
41
|
+
INTERNAL = "INTERNAL"
|
|
42
|
+
"""Unexpected internal error (§13.1 DC-AM-002).
|
|
43
|
+
|
|
44
|
+
Use a generic message; expose stack traces only in hints/logs.
|
|
45
|
+
The hint must include a prompt to report with reproduction steps.
|
|
46
|
+
"""
|
|
47
|
+
TRACK_NOT_FOUND = "TRACK_NOT_FOUND"
|
|
48
|
+
"""The track index in operations exceeds the total track count (§13.1 DC-AS-003)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ClipwrightError(Exception):
|
|
52
|
+
"""Exception raised by the Clipwright library layer.
|
|
53
|
+
|
|
54
|
+
Always carries the three-part set: code / message / hint (§6.4 error contract).
|
|
55
|
+
hint must describe the concrete next action for the user or AI agent.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, code: ErrorCode, message: str, hint: str) -> None:
|
|
59
|
+
super().__init__(message)
|
|
60
|
+
self.code = code
|
|
61
|
+
self.message = message
|
|
62
|
+
self.hint = hint
|