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.
@@ -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
@@ -0,0 +1,3 @@
1
+ """clipwright-overlay: MCP tool for adding image overlays to an OTIO timeline."""
2
+
3
+ __version__ = "0.1.0"