clipwright-overlay 0.1.0__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_overlay-0.1.0/PKG-INFO +300 -0
- clipwright_overlay-0.1.0/README.md +286 -0
- clipwright_overlay-0.1.0/pyproject.toml +82 -0
- clipwright_overlay-0.1.0/src/clipwright_overlay/__init__.py +3 -0
- clipwright_overlay-0.1.0/src/clipwright_overlay/overlay.py +781 -0
- clipwright_overlay-0.1.0/src/clipwright_overlay/py.typed +0 -0
- clipwright_overlay-0.1.0/src/clipwright_overlay/schemas.py +85 -0
- clipwright_overlay-0.1.0/src/clipwright_overlay/server.py +87 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: clipwright-overlay
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP tool for adding image overlays to an OTIO timeline.
|
|
5
|
+
Author: satoh-y-0323
|
|
6
|
+
Author-email: satoh-y-0323 <shoma.papa.0323@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: clipwright>=0.3.0
|
|
9
|
+
Requires-Dist: mcp[cli]>=1.27.2
|
|
10
|
+
Requires-Dist: opentimelineio>=0.18
|
|
11
|
+
Requires-Dist: pydantic>=2
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# clipwright-overlay
|
|
16
|
+
|
|
17
|
+
MCP tool that annotates an OTIO timeline with a static image overlay (logo,
|
|
18
|
+
watermark, lower-third graphic, end card) for materialisation by `clipwright-render`.
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
`clipwright-overlay` writes an `image_overlay` marker to the first video track (V1)
|
|
23
|
+
of an OTIO timeline. The marker stores the image path, position, scale, opacity, and
|
|
24
|
+
timing. `clipwright-render` reads the marker and inserts the image as an extra `-i`
|
|
25
|
+
input, building an FFmpeg filter chain that composites the image onto the video during
|
|
26
|
+
the single render pass.
|
|
27
|
+
|
|
28
|
+
**Design principle** (separation of annotation and realisation):
|
|
29
|
+
`clipwright-overlay` writes only the OTIO; it does not invoke FFmpeg or touch media
|
|
30
|
+
files. All image compositing is deferred to `clipwright-render`.
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
- Python 3.11 or later
|
|
35
|
+
- `clipwright` core package (shared types, envelope, OTIO utils)
|
|
36
|
+
- No FFmpeg is needed at annotation time. FFmpeg is only invoked by `clipwright-render`
|
|
37
|
+
at realisation time.
|
|
38
|
+
|
|
39
|
+
> **Note — clipwright-render >= 0.10.0 required for materialisation.**
|
|
40
|
+
> `clipwright-overlay` only *annotates* the OTIO timeline; it never invokes FFmpeg.
|
|
41
|
+
> The actual image compositing is performed by `clipwright-render`. Image overlay support
|
|
42
|
+
> was added in `clipwright-render` **0.10.0** — earlier versions will ignore the
|
|
43
|
+
> `image_overlay` markers and produce output without the overlay.
|
|
44
|
+
> Install or upgrade before calling `clipwright_render` on an annotated timeline:
|
|
45
|
+
>
|
|
46
|
+
> ```bash
|
|
47
|
+
> pip install "clipwright-render>=0.10.0"
|
|
48
|
+
> ```
|
|
49
|
+
|
|
50
|
+
## MCP Tool: `clipwright_add_overlay`
|
|
51
|
+
|
|
52
|
+
### Parameters
|
|
53
|
+
|
|
54
|
+
| Name | Type | Default | Description |
|
|
55
|
+
|------|------|---------|-------------|
|
|
56
|
+
| `timeline` | `string` | required | Input OTIO timeline file path (`.otio`). The parent directory is the co-location root for the image file. |
|
|
57
|
+
| `output` | `string` | required | Output OTIO timeline file path (`.otio`). Must differ from `timeline`. |
|
|
58
|
+
| `image_path` | `string` | required | Path to the overlay image. Must be co-located under the output timeline's parent directory (see Co-location Constraint). Supported formats: `.png`, `.jpg`, `.jpeg`, `.webp`. |
|
|
59
|
+
| `start_sec` | `float` | required | Start time in seconds (≥ 0) when the overlay becomes visible. |
|
|
60
|
+
| `duration_sec` | `float` | required | Duration in seconds (> 0) that the overlay remains visible. |
|
|
61
|
+
| `x` | `string` | `"(W-w)/2"` | Horizontal position expression (FFmpeg overlay filter syntax). `W` = base video width, `w` = overlay width. Default centres the overlay horizontally. |
|
|
62
|
+
| `y` | `string` | `"(H-h)/2"` | Vertical position expression. `H` = base video height, `h` = overlay height. Default centres the overlay vertically. |
|
|
63
|
+
| `scale` | `float` | `1.0` | Overlay scale factor relative to the original image size. Range: `(0, 8]`. `1.0` = original size. |
|
|
64
|
+
| `opacity` | `float` | `1.0` | Opacity of the overlay. Range: `[0.0, 1.0]`. `1.0` = fully opaque. |
|
|
65
|
+
| `fade_in_sec` | `float` | `0.3` | Fade-in duration in seconds (≥ 0). `0.0` = no fade-in (overlay appears immediately). |
|
|
66
|
+
| `fade_out_sec` | `float` | `0.3` | Fade-out duration in seconds (≥ 0). `0.0` = no fade-out. `fade_in_sec + fade_out_sec` must not exceed `duration_sec`. |
|
|
67
|
+
|
|
68
|
+
### Return value
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"ok": true,
|
|
73
|
+
"summary": "Added image overlay 'logo.png' at 5.0s for 10.0s. Timeline now has 1 image overlay(s). Output: output.otio.",
|
|
74
|
+
"data": {
|
|
75
|
+
"applied": 1,
|
|
76
|
+
"overlay_count": 1,
|
|
77
|
+
"start_sec": 5.0,
|
|
78
|
+
"duration_sec": 10.0
|
|
79
|
+
},
|
|
80
|
+
"artifacts": [{"role": "timeline", "path": "/absolute/path/to/output.otio", "format": "otio"}],
|
|
81
|
+
"warnings": []
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When an identical overlay is submitted again (same parameters), `applied` returns `0`
|
|
86
|
+
and a warning is added; no duplicate marker is written (idempotency).
|
|
87
|
+
|
|
88
|
+
### Error codes (annotation time)
|
|
89
|
+
|
|
90
|
+
| Code | Cause |
|
|
91
|
+
|------|-------|
|
|
92
|
+
| `INVALID_INPUT` | `start_sec < 0`, `duration_sec ≤ 0`, `scale ≤ 0` or `scale > 8`, `opacity` outside `[0, 1]`, `fade_in_sec < 0`, `fade_out_sec < 0`, or `fade_in_sec + fade_out_sec > duration_sec` |
|
|
93
|
+
| `INVALID_INPUT` | `image_path` extension is not `.png`, `.jpg`, `.jpeg`, or `.webp` |
|
|
94
|
+
| `INVALID_INPUT` | `image_path` or `x` / `y` expression contains a control character or a prohibited character (`: ; [ ] , '`) |
|
|
95
|
+
| `INVALID_INPUT` | `output` path is identical to `timeline` path |
|
|
96
|
+
| `INVALID_INPUT` | Timeline already has 64 `image_overlay` markers (per-timeline limit) |
|
|
97
|
+
| `FILE_NOT_FOUND` | `image_path` does not exist |
|
|
98
|
+
| `PATH_NOT_ALLOWED` | `image_path` is outside the output timeline's parent directory tree |
|
|
99
|
+
| `UNSUPPORTED_OPERATION` | The input timeline has no V1 video track |
|
|
100
|
+
|
|
101
|
+
### Error codes (render time, raised by `clipwright-render`)
|
|
102
|
+
|
|
103
|
+
| Code | Cause |
|
|
104
|
+
|------|-------|
|
|
105
|
+
| `SUBPROCESS_FAILED` | The overlay image could not be decoded by FFmpeg (corrupt file or unsupported encoding). Message shows the image basename only (CWE-209). Hint: `"The overlay image may be corrupt or an unsupported format; provide a valid .png/.jpg/.jpeg/.webp."` |
|
|
106
|
+
|
|
107
|
+
## Co-location Constraint
|
|
108
|
+
|
|
109
|
+
The `image_path` **must be located under the same directory as the output `.otio` file**
|
|
110
|
+
(or in a recursive subdirectory of it).
|
|
111
|
+
|
|
112
|
+
**Why this rule exists:**
|
|
113
|
+
`clipwright-render` enforces the same co-location boundary for all resources referenced
|
|
114
|
+
in an OTIO timeline (sources, subtitles, image overlays). If the image were outside that
|
|
115
|
+
boundary, the annotation would succeed but render would immediately fail with
|
|
116
|
+
`PATH_NOT_ALLOWED`.
|
|
117
|
+
|
|
118
|
+
By enforcing co-location at annotation time, `clipwright-overlay` guarantees that any
|
|
119
|
+
`.otio` it produces will pass through `clipwright-render` without a `PATH_NOT_ALLOWED`
|
|
120
|
+
error.
|
|
121
|
+
|
|
122
|
+
**Relative-path storage (V2-3 round-trip portability):**
|
|
123
|
+
The image path is stored in the OTIO marker as a POSIX relative path from the output
|
|
124
|
+
timeline's parent directory (e.g. `images/logo.png`). When `clipwright-render` reads the
|
|
125
|
+
marker, it reconstructs the absolute path using the timeline file's parent directory as the
|
|
126
|
+
base. This means projects remain portable when the entire directory tree is moved or copied
|
|
127
|
+
to another location, as long as the relative positions of the timeline and image files are
|
|
128
|
+
preserved.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
project/
|
|
132
|
+
logo.png ← allowed (same directory, stored as "logo.png")
|
|
133
|
+
assets/
|
|
134
|
+
watermark.png ← allowed (subdirectory, stored as "assets/watermark.png")
|
|
135
|
+
output.otio ← output timeline
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
/other/path/logo.png ← PATH_NOT_ALLOWED
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Position Expressions
|
|
143
|
+
|
|
144
|
+
`x` and `y` accept FFmpeg overlay filter expressions. The following variables are
|
|
145
|
+
available at render time:
|
|
146
|
+
|
|
147
|
+
| Variable | Meaning |
|
|
148
|
+
|----------|---------|
|
|
149
|
+
| `W` | Base video width in pixels |
|
|
150
|
+
| `H` | Base video height in pixels |
|
|
151
|
+
| `w` | Overlay image width (after scaling) |
|
|
152
|
+
| `h` | Overlay image height (after scaling) |
|
|
153
|
+
| `main_w` | Alias for `W` |
|
|
154
|
+
| `main_h` | Alias for `H` |
|
|
155
|
+
| `overlay_w` | Alias for `w` |
|
|
156
|
+
| `overlay_h` | Alias for `h` |
|
|
157
|
+
|
|
158
|
+
### Common position examples
|
|
159
|
+
|
|
160
|
+
| Position | `x` | `y` |
|
|
161
|
+
|----------|-----|-----|
|
|
162
|
+
| Centre | `(W-w)/2` (default) | `(H-h)/2` (default) |
|
|
163
|
+
| Top-left (10 px margin) | `10` | `10` |
|
|
164
|
+
| Top-right (10 px margin) | `W-w-10` | `10` |
|
|
165
|
+
| Bottom-left (10 px margin) | `10` | `H-h-10` |
|
|
166
|
+
| Bottom-right (10 px margin) | `W-w-10` | `H-h-10` |
|
|
167
|
+
| Bottom-centre | `(W-w)/2` | `H-h-10` |
|
|
168
|
+
|
|
169
|
+
**Allowed characters in `x` / `y`:** letters, digits, `_`, `(`, `)`, `+`, `-`, `*`,
|
|
170
|
+
`/`, `.`, and space. Characters `: ; [ ] , '` and control characters are prohibited to
|
|
171
|
+
prevent FFmpeg filtergraph injection.
|
|
172
|
+
|
|
173
|
+
## `readOnlyHint` Rationale
|
|
174
|
+
|
|
175
|
+
`clipwright_add_overlay` carries `readOnlyHint=true` in its MCP annotations.
|
|
176
|
+
|
|
177
|
+
`clipwright-overlay` writes only a new `.otio` file; the input media, the input
|
|
178
|
+
timeline, and the image file are never modified. The new-file write is outside the
|
|
179
|
+
readOnly scope (consistent with `clipwright-sequence`, `clipwright-trim`,
|
|
180
|
+
`clipwright-silence`, and other annotation tools). This signals to AI orchestrators
|
|
181
|
+
that the tool is safe for speculative execution and automatic retry without side effects.
|
|
182
|
+
|
|
183
|
+
## Fade Chain
|
|
184
|
+
|
|
185
|
+
The opacity and fade effect are implemented in a single filter chain per overlay:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
[{N}:v]scale=iw*{scale}:-2,format=rgba,colorchannelmixer=aa={opacity},
|
|
189
|
+
fade=t=in:st={start}:d={fade_in}:alpha=1,fade=t=out:st={end-fade_out}:d={fade_out}:alpha=1[ov{i}];
|
|
190
|
+
{base}[ov{i}]overlay=x='{x}':y='{y}':enable='between(t,{start},{end})'[outvimg{i}]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `scale=iw*{scale}:-2` — scale the image width by `scale`; height is computed
|
|
194
|
+
automatically with even rounding (`-2`) for yuv420p compatibility.
|
|
195
|
+
- `format=rgba` — add an alpha channel to the image.
|
|
196
|
+
- `colorchannelmixer=aa={opacity}` — set constant opacity (`aa` accepts a constant
|
|
197
|
+
double only; time-varying expressions are not supported by FFmpeg).
|
|
198
|
+
- `fade=t=in:...:alpha=1` — ramp the alpha from 0 to 1 over `fade_in_sec`. When
|
|
199
|
+
`fade_in_sec == 0` this segment is omitted entirely (no degenerate `d=0` filter).
|
|
200
|
+
- `fade=t=out:...:alpha=1` — ramp the alpha from 1 to 0 over `fade_out_sec`. Omitted
|
|
201
|
+
when `fade_out_sec == 0`.
|
|
202
|
+
- The `fade:alpha=1` flag multiplies the existing alpha channel, so the effective
|
|
203
|
+
alpha ramps from `0 → opacity → 0` across the fade windows.
|
|
204
|
+
- `overlay=x='{x}':y='{y}':enable='between(t,{start},{end})'` — composite the
|
|
205
|
+
prepared image onto the base video within the time window. `x` / `y` are
|
|
206
|
+
single-quoted (consistent with `enable` and `drawtext`).
|
|
207
|
+
|
|
208
|
+
This chain is inserted after the `drawtext` filter, so image overlays appear on top
|
|
209
|
+
of text overlays.
|
|
210
|
+
|
|
211
|
+
## Two-Phase Workflow
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
clipwright_add_overlay(timeline, output, image_path, ...) # Phase 1 — annotate OTIO
|
|
215
|
+
│
|
|
216
|
+
▼ OTIO timeline with image_overlay marker
|
|
217
|
+
clipwright_render(timeline, output_media) # Phase 2 — composite and encode
|
|
218
|
+
│
|
|
219
|
+
▼ video with image/logo composited
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Stacking multiple overlays
|
|
223
|
+
|
|
224
|
+
Multiple calls accumulate overlays on the same timeline:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Add a channel logo (top-right, always visible)
|
|
228
|
+
r1 = await session.call_tool("clipwright_add_overlay", {
|
|
229
|
+
"timeline": "/project/edit.otio",
|
|
230
|
+
"output": "/project/with_logo.otio",
|
|
231
|
+
"image_path": "/project/assets/logo.png",
|
|
232
|
+
"start_sec": 0.0,
|
|
233
|
+
"duration_sec": 120.0,
|
|
234
|
+
"x": "W-w-20",
|
|
235
|
+
"y": "20",
|
|
236
|
+
"scale": 0.15,
|
|
237
|
+
"opacity": 0.8,
|
|
238
|
+
"fade_in_sec": 0.0,
|
|
239
|
+
"fade_out_sec": 0.0
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# Add a lower-third graphic at a specific moment
|
|
243
|
+
r2 = await session.call_tool("clipwright_add_overlay", {
|
|
244
|
+
"timeline": "/project/with_logo.otio",
|
|
245
|
+
"output": "/project/with_logo_lowerthird.otio",
|
|
246
|
+
"image_path": "/project/assets/lower_third.png",
|
|
247
|
+
"start_sec": 15.0,
|
|
248
|
+
"duration_sec": 5.0,
|
|
249
|
+
"x": "(W-w)/2",
|
|
250
|
+
"y": "H-h-80",
|
|
251
|
+
"scale": 0.6,
|
|
252
|
+
"opacity": 1.0,
|
|
253
|
+
"fade_in_sec": 0.3,
|
|
254
|
+
"fade_out_sec": 0.3
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
# Render
|
|
258
|
+
render_result = await session.call_tool("clipwright_render", {
|
|
259
|
+
"timeline": "/project/with_logo_lowerthird.otio",
|
|
260
|
+
"output": "/project/final.mp4"
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## MCP Client Registration
|
|
265
|
+
|
|
266
|
+
`clipwright-overlay` does not require FFmpeg at annotation time, so no environment
|
|
267
|
+
variables are needed in the MCP server entry. Register it in your MCP client
|
|
268
|
+
configuration (`.mcp.json` / `claude_desktop_config.json`):
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"mcpServers": {
|
|
273
|
+
"clipwright-overlay": {
|
|
274
|
+
"command": "clipwright-overlay"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`clipwright-render` (which materialises the OTIO into video) still requires
|
|
281
|
+
`CLIPWRIGHT_FFMPEG`.
|
|
282
|
+
|
|
283
|
+
## Installation
|
|
284
|
+
|
|
285
|
+
Within a uv workspace:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
uv run --package clipwright-overlay clipwright-overlay
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Or install from PyPI:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
pip install clipwright-overlay
|
|
295
|
+
clipwright-overlay
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT — See [LICENSE](../LICENSE) for details.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# clipwright-overlay
|
|
2
|
+
|
|
3
|
+
MCP tool that annotates an OTIO timeline with a static image overlay (logo,
|
|
4
|
+
watermark, lower-third graphic, end card) for materialisation by `clipwright-render`.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`clipwright-overlay` writes an `image_overlay` marker to the first video track (V1)
|
|
9
|
+
of an OTIO timeline. The marker stores the image path, position, scale, opacity, and
|
|
10
|
+
timing. `clipwright-render` reads the marker and inserts the image as an extra `-i`
|
|
11
|
+
input, building an FFmpeg filter chain that composites the image onto the video during
|
|
12
|
+
the single render pass.
|
|
13
|
+
|
|
14
|
+
**Design principle** (separation of annotation and realisation):
|
|
15
|
+
`clipwright-overlay` writes only the OTIO; it does not invoke FFmpeg or touch media
|
|
16
|
+
files. All image compositing is deferred to `clipwright-render`.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Python 3.11 or later
|
|
21
|
+
- `clipwright` core package (shared types, envelope, OTIO utils)
|
|
22
|
+
- No FFmpeg is needed at annotation time. FFmpeg is only invoked by `clipwright-render`
|
|
23
|
+
at realisation time.
|
|
24
|
+
|
|
25
|
+
> **Note — clipwright-render >= 0.10.0 required for materialisation.**
|
|
26
|
+
> `clipwright-overlay` only *annotates* the OTIO timeline; it never invokes FFmpeg.
|
|
27
|
+
> The actual image compositing is performed by `clipwright-render`. Image overlay support
|
|
28
|
+
> was added in `clipwright-render` **0.10.0** — earlier versions will ignore the
|
|
29
|
+
> `image_overlay` markers and produce output without the overlay.
|
|
30
|
+
> Install or upgrade before calling `clipwright_render` on an annotated timeline:
|
|
31
|
+
>
|
|
32
|
+
> ```bash
|
|
33
|
+
> pip install "clipwright-render>=0.10.0"
|
|
34
|
+
> ```
|
|
35
|
+
|
|
36
|
+
## MCP Tool: `clipwright_add_overlay`
|
|
37
|
+
|
|
38
|
+
### Parameters
|
|
39
|
+
|
|
40
|
+
| Name | Type | Default | Description |
|
|
41
|
+
|------|------|---------|-------------|
|
|
42
|
+
| `timeline` | `string` | required | Input OTIO timeline file path (`.otio`). The parent directory is the co-location root for the image file. |
|
|
43
|
+
| `output` | `string` | required | Output OTIO timeline file path (`.otio`). Must differ from `timeline`. |
|
|
44
|
+
| `image_path` | `string` | required | Path to the overlay image. Must be co-located under the output timeline's parent directory (see Co-location Constraint). Supported formats: `.png`, `.jpg`, `.jpeg`, `.webp`. |
|
|
45
|
+
| `start_sec` | `float` | required | Start time in seconds (≥ 0) when the overlay becomes visible. |
|
|
46
|
+
| `duration_sec` | `float` | required | Duration in seconds (> 0) that the overlay remains visible. |
|
|
47
|
+
| `x` | `string` | `"(W-w)/2"` | Horizontal position expression (FFmpeg overlay filter syntax). `W` = base video width, `w` = overlay width. Default centres the overlay horizontally. |
|
|
48
|
+
| `y` | `string` | `"(H-h)/2"` | Vertical position expression. `H` = base video height, `h` = overlay height. Default centres the overlay vertically. |
|
|
49
|
+
| `scale` | `float` | `1.0` | Overlay scale factor relative to the original image size. Range: `(0, 8]`. `1.0` = original size. |
|
|
50
|
+
| `opacity` | `float` | `1.0` | Opacity of the overlay. Range: `[0.0, 1.0]`. `1.0` = fully opaque. |
|
|
51
|
+
| `fade_in_sec` | `float` | `0.3` | Fade-in duration in seconds (≥ 0). `0.0` = no fade-in (overlay appears immediately). |
|
|
52
|
+
| `fade_out_sec` | `float` | `0.3` | Fade-out duration in seconds (≥ 0). `0.0` = no fade-out. `fade_in_sec + fade_out_sec` must not exceed `duration_sec`. |
|
|
53
|
+
|
|
54
|
+
### Return value
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"ok": true,
|
|
59
|
+
"summary": "Added image overlay 'logo.png' at 5.0s for 10.0s. Timeline now has 1 image overlay(s). Output: output.otio.",
|
|
60
|
+
"data": {
|
|
61
|
+
"applied": 1,
|
|
62
|
+
"overlay_count": 1,
|
|
63
|
+
"start_sec": 5.0,
|
|
64
|
+
"duration_sec": 10.0
|
|
65
|
+
},
|
|
66
|
+
"artifacts": [{"role": "timeline", "path": "/absolute/path/to/output.otio", "format": "otio"}],
|
|
67
|
+
"warnings": []
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When an identical overlay is submitted again (same parameters), `applied` returns `0`
|
|
72
|
+
and a warning is added; no duplicate marker is written (idempotency).
|
|
73
|
+
|
|
74
|
+
### Error codes (annotation time)
|
|
75
|
+
|
|
76
|
+
| Code | Cause |
|
|
77
|
+
|------|-------|
|
|
78
|
+
| `INVALID_INPUT` | `start_sec < 0`, `duration_sec ≤ 0`, `scale ≤ 0` or `scale > 8`, `opacity` outside `[0, 1]`, `fade_in_sec < 0`, `fade_out_sec < 0`, or `fade_in_sec + fade_out_sec > duration_sec` |
|
|
79
|
+
| `INVALID_INPUT` | `image_path` extension is not `.png`, `.jpg`, `.jpeg`, or `.webp` |
|
|
80
|
+
| `INVALID_INPUT` | `image_path` or `x` / `y` expression contains a control character or a prohibited character (`: ; [ ] , '`) |
|
|
81
|
+
| `INVALID_INPUT` | `output` path is identical to `timeline` path |
|
|
82
|
+
| `INVALID_INPUT` | Timeline already has 64 `image_overlay` markers (per-timeline limit) |
|
|
83
|
+
| `FILE_NOT_FOUND` | `image_path` does not exist |
|
|
84
|
+
| `PATH_NOT_ALLOWED` | `image_path` is outside the output timeline's parent directory tree |
|
|
85
|
+
| `UNSUPPORTED_OPERATION` | The input timeline has no V1 video track |
|
|
86
|
+
|
|
87
|
+
### Error codes (render time, raised by `clipwright-render`)
|
|
88
|
+
|
|
89
|
+
| Code | Cause |
|
|
90
|
+
|------|-------|
|
|
91
|
+
| `SUBPROCESS_FAILED` | The overlay image could not be decoded by FFmpeg (corrupt file or unsupported encoding). Message shows the image basename only (CWE-209). Hint: `"The overlay image may be corrupt or an unsupported format; provide a valid .png/.jpg/.jpeg/.webp."` |
|
|
92
|
+
|
|
93
|
+
## Co-location Constraint
|
|
94
|
+
|
|
95
|
+
The `image_path` **must be located under the same directory as the output `.otio` file**
|
|
96
|
+
(or in a recursive subdirectory of it).
|
|
97
|
+
|
|
98
|
+
**Why this rule exists:**
|
|
99
|
+
`clipwright-render` enforces the same co-location boundary for all resources referenced
|
|
100
|
+
in an OTIO timeline (sources, subtitles, image overlays). If the image were outside that
|
|
101
|
+
boundary, the annotation would succeed but render would immediately fail with
|
|
102
|
+
`PATH_NOT_ALLOWED`.
|
|
103
|
+
|
|
104
|
+
By enforcing co-location at annotation time, `clipwright-overlay` guarantees that any
|
|
105
|
+
`.otio` it produces will pass through `clipwright-render` without a `PATH_NOT_ALLOWED`
|
|
106
|
+
error.
|
|
107
|
+
|
|
108
|
+
**Relative-path storage (V2-3 round-trip portability):**
|
|
109
|
+
The image path is stored in the OTIO marker as a POSIX relative path from the output
|
|
110
|
+
timeline's parent directory (e.g. `images/logo.png`). When `clipwright-render` reads the
|
|
111
|
+
marker, it reconstructs the absolute path using the timeline file's parent directory as the
|
|
112
|
+
base. This means projects remain portable when the entire directory tree is moved or copied
|
|
113
|
+
to another location, as long as the relative positions of the timeline and image files are
|
|
114
|
+
preserved.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
project/
|
|
118
|
+
logo.png ← allowed (same directory, stored as "logo.png")
|
|
119
|
+
assets/
|
|
120
|
+
watermark.png ← allowed (subdirectory, stored as "assets/watermark.png")
|
|
121
|
+
output.otio ← output timeline
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
/other/path/logo.png ← PATH_NOT_ALLOWED
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Position Expressions
|
|
129
|
+
|
|
130
|
+
`x` and `y` accept FFmpeg overlay filter expressions. The following variables are
|
|
131
|
+
available at render time:
|
|
132
|
+
|
|
133
|
+
| Variable | Meaning |
|
|
134
|
+
|----------|---------|
|
|
135
|
+
| `W` | Base video width in pixels |
|
|
136
|
+
| `H` | Base video height in pixels |
|
|
137
|
+
| `w` | Overlay image width (after scaling) |
|
|
138
|
+
| `h` | Overlay image height (after scaling) |
|
|
139
|
+
| `main_w` | Alias for `W` |
|
|
140
|
+
| `main_h` | Alias for `H` |
|
|
141
|
+
| `overlay_w` | Alias for `w` |
|
|
142
|
+
| `overlay_h` | Alias for `h` |
|
|
143
|
+
|
|
144
|
+
### Common position examples
|
|
145
|
+
|
|
146
|
+
| Position | `x` | `y` |
|
|
147
|
+
|----------|-----|-----|
|
|
148
|
+
| Centre | `(W-w)/2` (default) | `(H-h)/2` (default) |
|
|
149
|
+
| Top-left (10 px margin) | `10` | `10` |
|
|
150
|
+
| Top-right (10 px margin) | `W-w-10` | `10` |
|
|
151
|
+
| Bottom-left (10 px margin) | `10` | `H-h-10` |
|
|
152
|
+
| Bottom-right (10 px margin) | `W-w-10` | `H-h-10` |
|
|
153
|
+
| Bottom-centre | `(W-w)/2` | `H-h-10` |
|
|
154
|
+
|
|
155
|
+
**Allowed characters in `x` / `y`:** letters, digits, `_`, `(`, `)`, `+`, `-`, `*`,
|
|
156
|
+
`/`, `.`, and space. Characters `: ; [ ] , '` and control characters are prohibited to
|
|
157
|
+
prevent FFmpeg filtergraph injection.
|
|
158
|
+
|
|
159
|
+
## `readOnlyHint` Rationale
|
|
160
|
+
|
|
161
|
+
`clipwright_add_overlay` carries `readOnlyHint=true` in its MCP annotations.
|
|
162
|
+
|
|
163
|
+
`clipwright-overlay` writes only a new `.otio` file; the input media, the input
|
|
164
|
+
timeline, and the image file are never modified. The new-file write is outside the
|
|
165
|
+
readOnly scope (consistent with `clipwright-sequence`, `clipwright-trim`,
|
|
166
|
+
`clipwright-silence`, and other annotation tools). This signals to AI orchestrators
|
|
167
|
+
that the tool is safe for speculative execution and automatic retry without side effects.
|
|
168
|
+
|
|
169
|
+
## Fade Chain
|
|
170
|
+
|
|
171
|
+
The opacity and fade effect are implemented in a single filter chain per overlay:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
[{N}:v]scale=iw*{scale}:-2,format=rgba,colorchannelmixer=aa={opacity},
|
|
175
|
+
fade=t=in:st={start}:d={fade_in}:alpha=1,fade=t=out:st={end-fade_out}:d={fade_out}:alpha=1[ov{i}];
|
|
176
|
+
{base}[ov{i}]overlay=x='{x}':y='{y}':enable='between(t,{start},{end})'[outvimg{i}]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `scale=iw*{scale}:-2` — scale the image width by `scale`; height is computed
|
|
180
|
+
automatically with even rounding (`-2`) for yuv420p compatibility.
|
|
181
|
+
- `format=rgba` — add an alpha channel to the image.
|
|
182
|
+
- `colorchannelmixer=aa={opacity}` — set constant opacity (`aa` accepts a constant
|
|
183
|
+
double only; time-varying expressions are not supported by FFmpeg).
|
|
184
|
+
- `fade=t=in:...:alpha=1` — ramp the alpha from 0 to 1 over `fade_in_sec`. When
|
|
185
|
+
`fade_in_sec == 0` this segment is omitted entirely (no degenerate `d=0` filter).
|
|
186
|
+
- `fade=t=out:...:alpha=1` — ramp the alpha from 1 to 0 over `fade_out_sec`. Omitted
|
|
187
|
+
when `fade_out_sec == 0`.
|
|
188
|
+
- The `fade:alpha=1` flag multiplies the existing alpha channel, so the effective
|
|
189
|
+
alpha ramps from `0 → opacity → 0` across the fade windows.
|
|
190
|
+
- `overlay=x='{x}':y='{y}':enable='between(t,{start},{end})'` — composite the
|
|
191
|
+
prepared image onto the base video within the time window. `x` / `y` are
|
|
192
|
+
single-quoted (consistent with `enable` and `drawtext`).
|
|
193
|
+
|
|
194
|
+
This chain is inserted after the `drawtext` filter, so image overlays appear on top
|
|
195
|
+
of text overlays.
|
|
196
|
+
|
|
197
|
+
## Two-Phase Workflow
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
clipwright_add_overlay(timeline, output, image_path, ...) # Phase 1 — annotate OTIO
|
|
201
|
+
│
|
|
202
|
+
▼ OTIO timeline with image_overlay marker
|
|
203
|
+
clipwright_render(timeline, output_media) # Phase 2 — composite and encode
|
|
204
|
+
│
|
|
205
|
+
▼ video with image/logo composited
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Stacking multiple overlays
|
|
209
|
+
|
|
210
|
+
Multiple calls accumulate overlays on the same timeline:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# Add a channel logo (top-right, always visible)
|
|
214
|
+
r1 = await session.call_tool("clipwright_add_overlay", {
|
|
215
|
+
"timeline": "/project/edit.otio",
|
|
216
|
+
"output": "/project/with_logo.otio",
|
|
217
|
+
"image_path": "/project/assets/logo.png",
|
|
218
|
+
"start_sec": 0.0,
|
|
219
|
+
"duration_sec": 120.0,
|
|
220
|
+
"x": "W-w-20",
|
|
221
|
+
"y": "20",
|
|
222
|
+
"scale": 0.15,
|
|
223
|
+
"opacity": 0.8,
|
|
224
|
+
"fade_in_sec": 0.0,
|
|
225
|
+
"fade_out_sec": 0.0
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
# Add a lower-third graphic at a specific moment
|
|
229
|
+
r2 = await session.call_tool("clipwright_add_overlay", {
|
|
230
|
+
"timeline": "/project/with_logo.otio",
|
|
231
|
+
"output": "/project/with_logo_lowerthird.otio",
|
|
232
|
+
"image_path": "/project/assets/lower_third.png",
|
|
233
|
+
"start_sec": 15.0,
|
|
234
|
+
"duration_sec": 5.0,
|
|
235
|
+
"x": "(W-w)/2",
|
|
236
|
+
"y": "H-h-80",
|
|
237
|
+
"scale": 0.6,
|
|
238
|
+
"opacity": 1.0,
|
|
239
|
+
"fade_in_sec": 0.3,
|
|
240
|
+
"fade_out_sec": 0.3
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# Render
|
|
244
|
+
render_result = await session.call_tool("clipwright_render", {
|
|
245
|
+
"timeline": "/project/with_logo_lowerthird.otio",
|
|
246
|
+
"output": "/project/final.mp4"
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## MCP Client Registration
|
|
251
|
+
|
|
252
|
+
`clipwright-overlay` does not require FFmpeg at annotation time, so no environment
|
|
253
|
+
variables are needed in the MCP server entry. Register it in your MCP client
|
|
254
|
+
configuration (`.mcp.json` / `claude_desktop_config.json`):
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"mcpServers": {
|
|
259
|
+
"clipwright-overlay": {
|
|
260
|
+
"command": "clipwright-overlay"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`clipwright-render` (which materialises the OTIO into video) still requires
|
|
267
|
+
`CLIPWRIGHT_FFMPEG`.
|
|
268
|
+
|
|
269
|
+
## Installation
|
|
270
|
+
|
|
271
|
+
Within a uv workspace:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
uv run --package clipwright-overlay clipwright-overlay
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Or install from PyPI:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
pip install clipwright-overlay
|
|
281
|
+
clipwright-overlay
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT — See [LICENSE](../LICENSE) for details.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clipwright-overlay"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP tool for adding image overlays to an OTIO timeline."
|
|
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
|
+
"clipwright>=0.3.0",
|
|
13
|
+
"mcp[cli]>=1.27.2",
|
|
14
|
+
"opentimelineio>=0.18",
|
|
15
|
+
"pydantic>=2",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
clipwright-overlay = "clipwright_overlay.server:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.11.19,<0.12.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"mypy>=2.1.0",
|
|
28
|
+
"pytest>=9.0.3",
|
|
29
|
+
"pytest-cov>=7.1.0",
|
|
30
|
+
"pytest-mock>=3.15.1",
|
|
31
|
+
"ruff>=0.15.16",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.uv.sources]
|
|
35
|
+
clipwright = { workspace = true }
|
|
36
|
+
|
|
37
|
+
# --- Ruff ---
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py311"
|
|
40
|
+
line-length = 88
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
|
|
44
|
+
ignore = []
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint.per-file-ignores]
|
|
47
|
+
# Allow E501 for English docstrings/comments in test files.
|
|
48
|
+
"tests/*.py" = ["E501"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.format]
|
|
51
|
+
|
|
52
|
+
# --- mypy ---
|
|
53
|
+
[tool.mypy]
|
|
54
|
+
python_version = "3.11"
|
|
55
|
+
strict = true
|
|
56
|
+
warn_return_any = true
|
|
57
|
+
warn_unused_configs = true
|
|
58
|
+
disallow_untyped_defs = true
|
|
59
|
+
disallow_any_generics = true
|
|
60
|
+
|
|
61
|
+
[[tool.mypy.overrides]]
|
|
62
|
+
module = "opentimelineio.*"
|
|
63
|
+
ignore_missing_imports = true
|
|
64
|
+
|
|
65
|
+
# --- pytest ---
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
addopts = "--strict-markers -q"
|
|
69
|
+
asyncio_mode = "strict"
|
|
70
|
+
markers = [
|
|
71
|
+
"integration: integration test requiring actual ffmpeg/ffprobe binaries",
|
|
72
|
+
"slow: test with long execution time",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# --- coverage ---
|
|
76
|
+
[tool.coverage.run]
|
|
77
|
+
source = ["clipwright_overlay"]
|
|
78
|
+
omit = ["tests/*"]
|
|
79
|
+
|
|
80
|
+
[tool.coverage.report]
|
|
81
|
+
show_missing = true
|
|
82
|
+
skip_covered = false
|