agentcut 0.1.0__py3-none-any.whl
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.
- agentcut/__init__.py +7 -0
- agentcut/__main__.py +300 -0
- agentcut/client.py +272 -0
- agentcut/engine.py +1421 -0
- agentcut/errors.py +171 -0
- agentcut/models.py +198 -0
- agentcut/server.py +629 -0
- agentcut/templates.py +190 -0
- agentcut-0.1.0.dist-info/METADATA +683 -0
- agentcut-0.1.0.dist-info/RECORD +13 -0
- agentcut-0.1.0.dist-info/WHEEL +4 -0
- agentcut-0.1.0.dist-info/entry_points.txt +2 -0
- agentcut-0.1.0.dist-info/licenses/LICENSE +190 -0
agentcut/__init__.py
ADDED
agentcut/__main__.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""AgentCut CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="agentcut",
|
|
13
|
+
description="AgentCut — Video editing for AI agents",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--mcp",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Run as MCP server (default mode)",
|
|
19
|
+
)
|
|
20
|
+
subparsers = parser.add_subparsers(dest="command", help="CLI commands")
|
|
21
|
+
|
|
22
|
+
# info
|
|
23
|
+
info_p = subparsers.add_parser("info", help="Get video metadata")
|
|
24
|
+
info_p.add_argument("input", help="Input video file")
|
|
25
|
+
|
|
26
|
+
# trim
|
|
27
|
+
trim_p = subparsers.add_parser("trim", help="Trim a video")
|
|
28
|
+
trim_p.add_argument("input", help="Input video file")
|
|
29
|
+
trim_p.add_argument("-s", "--start", default="0", help="Start time")
|
|
30
|
+
trim_p.add_argument("-d", "--duration", help="Duration")
|
|
31
|
+
trim_p.add_argument("-e", "--end", help="End time")
|
|
32
|
+
trim_p.add_argument("-o", "--output", help="Output file path")
|
|
33
|
+
|
|
34
|
+
# merge
|
|
35
|
+
merge_p = subparsers.add_parser("merge", help="Merge multiple clips")
|
|
36
|
+
merge_p.add_argument("inputs", nargs="+", help="Input video files")
|
|
37
|
+
merge_p.add_argument("-t", "--transition", default=None, choices=["fade", "dissolve", "wipe-left", "wipe-right", "wipe-up", "wipe-down"])
|
|
38
|
+
merge_p.add_argument("--transitions", nargs="+", choices=["fade", "dissolve", "wipe-left", "wipe-right", "wipe-up", "wipe-down"], help="Per-pair transition types (overrides --transition)")
|
|
39
|
+
merge_p.add_argument("-td", "--transition-duration", type=float, default=1.0, help="Transition duration in seconds")
|
|
40
|
+
merge_p.add_argument("-o", "--output", help="Output file path")
|
|
41
|
+
|
|
42
|
+
# add_text
|
|
43
|
+
text_p = subparsers.add_parser("add-text", help="Overlay text on a video")
|
|
44
|
+
text_p.add_argument("input", help="Input video file")
|
|
45
|
+
text_p.add_argument("text", help="Text to overlay")
|
|
46
|
+
text_p.add_argument("-p", "--position", default="top-center", choices=["top-left", "top-center", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom-center", "bottom-right"])
|
|
47
|
+
text_p.add_argument("--font", help="Path to font file")
|
|
48
|
+
text_p.add_argument("--size", type=int, default=48, help="Font size in pixels")
|
|
49
|
+
text_p.add_argument("--color", default="white", help="Text color")
|
|
50
|
+
text_p.add_argument("--no-shadow", action="store_true", help="Disable text shadow")
|
|
51
|
+
text_p.add_argument("--start-time", type=float, help="When text appears (seconds)")
|
|
52
|
+
text_p.add_argument("--duration", type=float, help="How long text is visible (seconds)")
|
|
53
|
+
text_p.add_argument("-o", "--output", help="Output file path")
|
|
54
|
+
|
|
55
|
+
# add_audio
|
|
56
|
+
audio_p = subparsers.add_parser("add-audio", help="Add or replace audio track")
|
|
57
|
+
audio_p.add_argument("video", help="Input video file")
|
|
58
|
+
audio_p.add_argument("audio", help="Audio file (MP3, WAV, etc.)")
|
|
59
|
+
audio_p.add_argument("-v", "--volume", type=float, default=1.0, help="Audio volume (0.0-2.0)")
|
|
60
|
+
audio_p.add_argument("--fade-in", type=float, default=0.0, help="Fade in duration")
|
|
61
|
+
audio_p.add_argument("--fade-out", type=float, default=0.0, help="Fade out duration")
|
|
62
|
+
audio_p.add_argument("--mix", action="store_true", help="Mix with existing audio instead of replacing")
|
|
63
|
+
audio_p.add_argument("--start-time", type=float, help="When audio starts (seconds)")
|
|
64
|
+
audio_p.add_argument("-o", "--output", help="Output file path")
|
|
65
|
+
|
|
66
|
+
# resize
|
|
67
|
+
resize_p = subparsers.add_parser("resize", help="Resize a video")
|
|
68
|
+
resize_p.add_argument("input", help="Input video file")
|
|
69
|
+
resize_p.add_argument("-w", "--width", type=int, help="Target width")
|
|
70
|
+
resize_p.add_argument("--height", type=int, help="Target height")
|
|
71
|
+
resize_p.add_argument("-a", "--aspect-ratio", choices=["16:9", "9:16", "1:1", "4:3", "4:5", "21:9"], help="Preset aspect ratio")
|
|
72
|
+
resize_p.add_argument("-q", "--quality", default="high", choices=["low", "medium", "high", "ultra"])
|
|
73
|
+
resize_p.add_argument("-o", "--output", help="Output file path")
|
|
74
|
+
|
|
75
|
+
# speed
|
|
76
|
+
speed_p = subparsers.add_parser("speed", help="Change playback speed")
|
|
77
|
+
speed_p.add_argument("input", help="Input video file")
|
|
78
|
+
speed_p.add_argument("-f", "--factor", type=float, default=1.0, help="Speed multiplier (0.5=slow, 2.0=fast)")
|
|
79
|
+
speed_p.add_argument("-o", "--output", help="Output file path")
|
|
80
|
+
|
|
81
|
+
# convert
|
|
82
|
+
convert_p = subparsers.add_parser("convert", help="Convert video format")
|
|
83
|
+
convert_p.add_argument("input", help="Input video file")
|
|
84
|
+
convert_p.add_argument("-f", "--format", default="mp4", choices=["mp4", "webm", "gif", "mov"])
|
|
85
|
+
convert_p.add_argument("-q", "--quality", default="high", choices=["low", "medium", "high", "ultra"])
|
|
86
|
+
convert_p.add_argument("-o", "--output", help="Output file path")
|
|
87
|
+
|
|
88
|
+
# thumbnail
|
|
89
|
+
thumb_p = subparsers.add_parser("thumbnail", help="Extract a single frame")
|
|
90
|
+
thumb_p.add_argument("input", help="Input video file")
|
|
91
|
+
thumb_p.add_argument("-t", "--timestamp", type=float, help="Time in seconds (default: 10%% of duration)")
|
|
92
|
+
thumb_p.add_argument("-o", "--output", help="Output image path")
|
|
93
|
+
|
|
94
|
+
# preview
|
|
95
|
+
preview_p = subparsers.add_parser("preview", help="Generate a fast low-res preview")
|
|
96
|
+
preview_p.add_argument("input", help="Input video file")
|
|
97
|
+
preview_p.add_argument("-o", "--output", help="Output file path")
|
|
98
|
+
preview_p.add_argument("-s", "--scale", type=int, default=4, help="Downscale factor (default: 4)")
|
|
99
|
+
|
|
100
|
+
# storyboard
|
|
101
|
+
storyboard_p = subparsers.add_parser("storyboard", help="Extract key frames as storyboard")
|
|
102
|
+
storyboard_p.add_argument("input", help="Input video file")
|
|
103
|
+
storyboard_p.add_argument("-o", "--output-dir", help="Output directory")
|
|
104
|
+
storyboard_p.add_argument("-n", "--frames", type=int, default=8, help="Number of frames (default: 8)")
|
|
105
|
+
|
|
106
|
+
# subtitles
|
|
107
|
+
subs_p = subparsers.add_parser("subtitles", help="Burn subtitles into video")
|
|
108
|
+
subs_p.add_argument("input", help="Input video file")
|
|
109
|
+
subs_p.add_argument("subtitle", help="Subtitle file (.srt or .vtt)")
|
|
110
|
+
subs_p.add_argument("-o", "--output", help="Output file path")
|
|
111
|
+
|
|
112
|
+
# watermark
|
|
113
|
+
wm_p = subparsers.add_parser("watermark", help="Add image watermark")
|
|
114
|
+
wm_p.add_argument("input", help="Input video file")
|
|
115
|
+
wm_p.add_argument("image", help="Watermark image (PNG recommended)")
|
|
116
|
+
wm_p.add_argument("-p", "--position", default="bottom-right", choices=["top-left", "top-center", "top-right", "center-left", "center", "bottom-left", "bottom-center", "bottom-right"])
|
|
117
|
+
wm_p.add_argument("--opacity", type=float, default=0.7, help="Watermark opacity (0.0-1.0)")
|
|
118
|
+
wm_p.add_argument("--margin", type=int, default=20, help="Margin from edge in pixels")
|
|
119
|
+
wm_p.add_argument("-o", "--output", help="Output file path")
|
|
120
|
+
|
|
121
|
+
# crop
|
|
122
|
+
crop_p = subparsers.add_parser("crop", help="Crop a video to a region")
|
|
123
|
+
crop_p.add_argument("input", help="Input video file")
|
|
124
|
+
crop_p.add_argument("-w", "--width", type=int, required=True, help="Crop width in pixels")
|
|
125
|
+
crop_p.add_argument("--height", type=int, required=True, help="Crop height in pixels")
|
|
126
|
+
crop_p.add_argument("-x", type=int, default=None, help="X offset (default: center)")
|
|
127
|
+
crop_p.add_argument("-y", type=int, default=None, help="Y offset (default: center)")
|
|
128
|
+
crop_p.add_argument("-o", "--output", help="Output file path")
|
|
129
|
+
|
|
130
|
+
# rotate
|
|
131
|
+
rotate_p = subparsers.add_parser("rotate", help="Rotate and/or flip a video")
|
|
132
|
+
rotate_p.add_argument("input", help="Input video file")
|
|
133
|
+
rotate_p.add_argument("-a", "--angle", type=int, default=0, choices=[0, 90, 180, 270], help="Rotation angle in degrees")
|
|
134
|
+
rotate_p.add_argument("--flip-h", action="store_true", help="Flip horizontally")
|
|
135
|
+
rotate_p.add_argument("--flip-v", action="store_true", help="Flip vertically")
|
|
136
|
+
rotate_p.add_argument("-o", "--output", help="Output file path")
|
|
137
|
+
|
|
138
|
+
# fade
|
|
139
|
+
fade_p = subparsers.add_parser("fade", help="Add fade in/out to video")
|
|
140
|
+
fade_p.add_argument("input", help="Input video file")
|
|
141
|
+
fade_p.add_argument("--fade-in", type=float, default=0.0, help="Fade in duration (seconds)")
|
|
142
|
+
fade_p.add_argument("--fade-out", type=float, default=0.0, help="Fade out duration (seconds)")
|
|
143
|
+
fade_p.add_argument("-o", "--output", help="Output file path")
|
|
144
|
+
|
|
145
|
+
# export
|
|
146
|
+
export_p = subparsers.add_parser("export", help="Export video with quality settings")
|
|
147
|
+
export_p.add_argument("input", help="Input video file")
|
|
148
|
+
export_p.add_argument("-q", "--quality", default="high", choices=["low", "medium", "high", "ultra"])
|
|
149
|
+
export_p.add_argument("-f", "--format", default="mp4", choices=["mp4", "webm", "gif", "mov"])
|
|
150
|
+
export_p.add_argument("-o", "--output", help="Output file path")
|
|
151
|
+
|
|
152
|
+
# extract_audio
|
|
153
|
+
extract_p = subparsers.add_parser("extract-audio", help="Extract audio from video")
|
|
154
|
+
extract_p.add_argument("input", help="Input video file")
|
|
155
|
+
extract_p.add_argument("-f", "--format", default="mp3", choices=["mp3", "aac", "wav", "ogg", "flac"])
|
|
156
|
+
extract_p.add_argument("-o", "--output", help="Output audio file path")
|
|
157
|
+
|
|
158
|
+
# edit (timeline)
|
|
159
|
+
edit_p = subparsers.add_parser("edit", help="Execute timeline-based edit from JSON")
|
|
160
|
+
edit_p.add_argument("timeline", help="Path to timeline JSON file")
|
|
161
|
+
edit_p.add_argument("-o", "--output", help="Output file path")
|
|
162
|
+
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
# Default mode: run MCP server
|
|
166
|
+
if args.mcp or args.command is None:
|
|
167
|
+
from .server import mcp
|
|
168
|
+
mcp.run()
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# CLI commands
|
|
172
|
+
try:
|
|
173
|
+
if args.command == "info":
|
|
174
|
+
from .engine import probe
|
|
175
|
+
info = probe(args.input)
|
|
176
|
+
print(json.dumps(info.model_dump(), indent=2))
|
|
177
|
+
|
|
178
|
+
elif args.command == "trim":
|
|
179
|
+
from .engine import trim
|
|
180
|
+
result = trim(args.input, start=args.start, duration=args.duration, end=args.end, output_path=args.output)
|
|
181
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
182
|
+
|
|
183
|
+
elif args.command == "merge":
|
|
184
|
+
from .engine import merge
|
|
185
|
+
result = merge(args.inputs, output_path=args.output, transition=args.transition, transitions=args.transitions, transition_duration=args.transition_duration)
|
|
186
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
187
|
+
|
|
188
|
+
elif args.command == "add-text":
|
|
189
|
+
from .engine import add_text
|
|
190
|
+
result = add_text(
|
|
191
|
+
args.input, text=args.text, position=args.position,
|
|
192
|
+
font=args.font, size=args.size, color=args.color,
|
|
193
|
+
shadow=not args.no_shadow,
|
|
194
|
+
start_time=args.start_time, duration=args.duration,
|
|
195
|
+
output_path=args.output,
|
|
196
|
+
)
|
|
197
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
198
|
+
|
|
199
|
+
elif args.command == "add-audio":
|
|
200
|
+
from .engine import add_audio
|
|
201
|
+
result = add_audio(
|
|
202
|
+
args.video, args.audio, volume=args.volume,
|
|
203
|
+
fade_in=args.fade_in, fade_out=args.fade_out,
|
|
204
|
+
mix=args.mix, start_time=args.start_time,
|
|
205
|
+
output_path=args.output,
|
|
206
|
+
)
|
|
207
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
208
|
+
|
|
209
|
+
elif args.command == "resize":
|
|
210
|
+
from .engine import resize
|
|
211
|
+
result = resize(
|
|
212
|
+
args.input, width=args.width, height=args.height,
|
|
213
|
+
aspect_ratio=args.aspect_ratio, quality=args.quality,
|
|
214
|
+
output_path=args.output,
|
|
215
|
+
)
|
|
216
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
217
|
+
|
|
218
|
+
elif args.command == "speed":
|
|
219
|
+
from .engine import speed
|
|
220
|
+
result = speed(args.input, factor=args.factor, output_path=args.output)
|
|
221
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
222
|
+
|
|
223
|
+
elif args.command == "convert":
|
|
224
|
+
from .engine import convert
|
|
225
|
+
result = convert(args.input, format=args.format, quality=args.quality, output_path=args.output)
|
|
226
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
227
|
+
|
|
228
|
+
elif args.command == "thumbnail":
|
|
229
|
+
from .engine import thumbnail
|
|
230
|
+
result = thumbnail(args.input, timestamp=args.timestamp, output_path=args.output)
|
|
231
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
232
|
+
|
|
233
|
+
elif args.command == "preview":
|
|
234
|
+
from .engine import preview
|
|
235
|
+
result = preview(args.input, output_path=args.output, scale_factor=args.scale)
|
|
236
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
237
|
+
|
|
238
|
+
elif args.command == "storyboard":
|
|
239
|
+
from .engine import storyboard
|
|
240
|
+
result = storyboard(args.input, output_dir=args.output_dir, frame_count=args.frames)
|
|
241
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
242
|
+
|
|
243
|
+
elif args.command == "subtitles":
|
|
244
|
+
from .engine import subtitles
|
|
245
|
+
result = subtitles(args.input, subtitle_path=args.subtitle, output_path=args.output)
|
|
246
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
247
|
+
|
|
248
|
+
elif args.command == "watermark":
|
|
249
|
+
from .engine import watermark
|
|
250
|
+
result = watermark(
|
|
251
|
+
args.input, image_path=args.image, position=args.position,
|
|
252
|
+
opacity=args.opacity, margin=args.margin,
|
|
253
|
+
output_path=args.output,
|
|
254
|
+
)
|
|
255
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
256
|
+
|
|
257
|
+
elif args.command == "crop":
|
|
258
|
+
from .engine import crop
|
|
259
|
+
result = crop(args.input, width=args.width, height=args.height, x=args.x, y=args.y, output_path=args.output)
|
|
260
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
261
|
+
|
|
262
|
+
elif args.command == "rotate":
|
|
263
|
+
from .engine import rotate
|
|
264
|
+
result = rotate(args.input, angle=args.angle, flip_horizontal=args.flip_h, flip_vertical=args.flip_v, output_path=args.output)
|
|
265
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
266
|
+
|
|
267
|
+
elif args.command == "fade":
|
|
268
|
+
from .engine import fade
|
|
269
|
+
result = fade(args.input, fade_in=args.fade_in, fade_out=args.fade_out, output_path=args.output)
|
|
270
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
271
|
+
|
|
272
|
+
elif args.command == "export":
|
|
273
|
+
from .engine import export_video
|
|
274
|
+
result = export_video(args.input, quality=args.quality, format=args.format, output_path=args.output)
|
|
275
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
276
|
+
|
|
277
|
+
elif args.command == "extract-audio":
|
|
278
|
+
from .engine import extract_audio
|
|
279
|
+
result = extract_audio(args.input, output_path=args.output, format=args.format)
|
|
280
|
+
print(result)
|
|
281
|
+
|
|
282
|
+
elif args.command == "edit":
|
|
283
|
+
from .models import Timeline
|
|
284
|
+
with open(args.timeline) as f:
|
|
285
|
+
tl = Timeline.model_validate(json.load(f))
|
|
286
|
+
from .engine import edit_timeline
|
|
287
|
+
result = edit_timeline(tl, output_path=args.output)
|
|
288
|
+
print(json.dumps(result.model_dump(), indent=2))
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
from .errors import AgentCutError
|
|
292
|
+
if isinstance(e, AgentCutError):
|
|
293
|
+
print(json.dumps({"success": False, "error": e.to_dict()}, indent=2), file=sys.stderr)
|
|
294
|
+
else:
|
|
295
|
+
print(json.dumps({"success": False, "error": {"type": "unknown", "message": str(e)}}, indent=2), file=sys.stderr)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
main()
|
agentcut/client.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""AgentCut Python client — clean API for programmatic video editing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from .engine import (
|
|
8
|
+
add_audio as _add_audio,
|
|
9
|
+
add_text as _add_text,
|
|
10
|
+
convert as _convert,
|
|
11
|
+
crop as _crop,
|
|
12
|
+
edit_timeline as _edit_timeline,
|
|
13
|
+
export_video as _export_video,
|
|
14
|
+
extract_audio as _extract_audio,
|
|
15
|
+
fade as _fade,
|
|
16
|
+
merge as _merge,
|
|
17
|
+
preview as _preview,
|
|
18
|
+
probe as _probe,
|
|
19
|
+
resize as _resize,
|
|
20
|
+
rotate as _rotate,
|
|
21
|
+
storyboard as _storyboard,
|
|
22
|
+
subtitles as _subtitles,
|
|
23
|
+
speed as _speed,
|
|
24
|
+
thumbnail as _thumbnail,
|
|
25
|
+
trim as _trim,
|
|
26
|
+
watermark as _watermark,
|
|
27
|
+
)
|
|
28
|
+
from .models import (
|
|
29
|
+
EditResult,
|
|
30
|
+
ExportFormat,
|
|
31
|
+
Position,
|
|
32
|
+
QualityLevel,
|
|
33
|
+
StoryboardResult,
|
|
34
|
+
ThumbnailResult,
|
|
35
|
+
VideoInfo,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Client:
|
|
40
|
+
"""AgentCut client for programmatic video editing.
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
from agentcut import Client
|
|
44
|
+
editor = Client()
|
|
45
|
+
|
|
46
|
+
result = editor.trim("input.mp4", start="00:00:30", duration="00:00:15")
|
|
47
|
+
print(result.output_path)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def info(self, input_path: str) -> VideoInfo:
|
|
51
|
+
"""Get metadata about a video file."""
|
|
52
|
+
return _probe(input_path)
|
|
53
|
+
|
|
54
|
+
def trim(
|
|
55
|
+
self,
|
|
56
|
+
input: str,
|
|
57
|
+
start: str | float = 0,
|
|
58
|
+
duration: str | float | None = None,
|
|
59
|
+
end: str | float | None = None,
|
|
60
|
+
output: str | None = None,
|
|
61
|
+
) -> EditResult:
|
|
62
|
+
"""Trim a clip by start time and duration."""
|
|
63
|
+
return _trim(input, start=start, duration=duration, end=end, output_path=output)
|
|
64
|
+
|
|
65
|
+
def merge(
|
|
66
|
+
self,
|
|
67
|
+
clips: list[str],
|
|
68
|
+
output: str | None = None,
|
|
69
|
+
transitions: list[str] | None = None,
|
|
70
|
+
transition_duration: float = 1.0,
|
|
71
|
+
) -> EditResult:
|
|
72
|
+
"""Merge multiple clips into one video.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
clips: List of video file paths.
|
|
76
|
+
output: Output file path.
|
|
77
|
+
transitions: Transition types applied between each clip pair.
|
|
78
|
+
One per boundary (len = len(clips)-1). If fewer provided,
|
|
79
|
+
the last type is repeated. Example: ["fade", "dissolve", "fade"].
|
|
80
|
+
transition_duration: Duration of each transition in seconds.
|
|
81
|
+
"""
|
|
82
|
+
return _merge(clips, output_path=output, transitions=transitions, transition_duration=transition_duration)
|
|
83
|
+
|
|
84
|
+
def add_text(
|
|
85
|
+
self,
|
|
86
|
+
video: str,
|
|
87
|
+
text: str,
|
|
88
|
+
position: str = "top-center",
|
|
89
|
+
font: str | None = None,
|
|
90
|
+
size: int = 48,
|
|
91
|
+
color: str = "white",
|
|
92
|
+
shadow: bool = True,
|
|
93
|
+
start_time: float | None = None,
|
|
94
|
+
duration: float | None = None,
|
|
95
|
+
output: str | None = None,
|
|
96
|
+
) -> EditResult:
|
|
97
|
+
"""Overlay text on a video."""
|
|
98
|
+
return _add_text(
|
|
99
|
+
video, text=text, position=position, font=font,
|
|
100
|
+
size=size, color=color, shadow=shadow,
|
|
101
|
+
start_time=start_time, duration=duration,
|
|
102
|
+
output_path=output,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def add_audio(
|
|
106
|
+
self,
|
|
107
|
+
video: str,
|
|
108
|
+
audio: str,
|
|
109
|
+
volume: float = 1.0,
|
|
110
|
+
fade_in: float = 0.0,
|
|
111
|
+
fade_out: float = 0.0,
|
|
112
|
+
mix: bool = False,
|
|
113
|
+
start_time: float | None = None,
|
|
114
|
+
output: str | None = None,
|
|
115
|
+
) -> EditResult:
|
|
116
|
+
"""Add or replace audio track."""
|
|
117
|
+
return _add_audio(
|
|
118
|
+
video, audio_path=audio, volume=volume,
|
|
119
|
+
fade_in=fade_in, fade_out=fade_out, mix=mix,
|
|
120
|
+
start_time=start_time, output_path=output,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def resize(
|
|
124
|
+
self,
|
|
125
|
+
video: str,
|
|
126
|
+
width: int | None = None,
|
|
127
|
+
height: int | None = None,
|
|
128
|
+
aspect_ratio: str | None = None,
|
|
129
|
+
quality: str = "high",
|
|
130
|
+
output: str | None = None,
|
|
131
|
+
) -> EditResult:
|
|
132
|
+
"""Resize a video or change aspect ratio."""
|
|
133
|
+
return _resize(
|
|
134
|
+
video, width=width, height=height,
|
|
135
|
+
aspect_ratio=aspect_ratio, quality=quality,
|
|
136
|
+
output_path=output,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def convert(
|
|
140
|
+
self,
|
|
141
|
+
video: str,
|
|
142
|
+
format: str = "mp4",
|
|
143
|
+
quality: str = "high",
|
|
144
|
+
output: str | None = None,
|
|
145
|
+
) -> EditResult:
|
|
146
|
+
"""Convert video to a different format."""
|
|
147
|
+
return _convert(video, format=format, quality=quality, output_path=output)
|
|
148
|
+
|
|
149
|
+
def speed(
|
|
150
|
+
self,
|
|
151
|
+
video: str,
|
|
152
|
+
factor: float = 1.0,
|
|
153
|
+
output: str | None = None,
|
|
154
|
+
) -> EditResult:
|
|
155
|
+
"""Change playback speed."""
|
|
156
|
+
return _speed(video, factor=factor, output_path=output)
|
|
157
|
+
|
|
158
|
+
def thumbnail(
|
|
159
|
+
self,
|
|
160
|
+
video: str,
|
|
161
|
+
timestamp: float | None = None,
|
|
162
|
+
output: str | None = None,
|
|
163
|
+
) -> ThumbnailResult:
|
|
164
|
+
"""Extract a frame from a video."""
|
|
165
|
+
return _thumbnail(video, timestamp=timestamp, output_path=output)
|
|
166
|
+
|
|
167
|
+
def preview(
|
|
168
|
+
self,
|
|
169
|
+
video: str,
|
|
170
|
+
output: str | None = None,
|
|
171
|
+
scale_factor: int = 4,
|
|
172
|
+
) -> EditResult:
|
|
173
|
+
"""Generate a fast low-res preview."""
|
|
174
|
+
return _preview(video, output_path=output, scale_factor=scale_factor)
|
|
175
|
+
|
|
176
|
+
def storyboard(
|
|
177
|
+
self,
|
|
178
|
+
video: str,
|
|
179
|
+
output_dir: str | None = None,
|
|
180
|
+
frame_count: int = 8,
|
|
181
|
+
) -> StoryboardResult:
|
|
182
|
+
"""Extract key frames as storyboard for human review."""
|
|
183
|
+
return _storyboard(video, output_dir=output_dir, frame_count=frame_count)
|
|
184
|
+
|
|
185
|
+
def subtitles(
|
|
186
|
+
self,
|
|
187
|
+
video: str,
|
|
188
|
+
subtitle_file: str,
|
|
189
|
+
output: str | None = None,
|
|
190
|
+
) -> EditResult:
|
|
191
|
+
"""Burn subtitles into a video."""
|
|
192
|
+
return _subtitles(video, subtitle_path=subtitle_file, output_path=output)
|
|
193
|
+
|
|
194
|
+
def watermark(
|
|
195
|
+
self,
|
|
196
|
+
video: str,
|
|
197
|
+
image: str,
|
|
198
|
+
position: str = "bottom-right",
|
|
199
|
+
opacity: float = 0.7,
|
|
200
|
+
margin: int = 20,
|
|
201
|
+
output: str | None = None,
|
|
202
|
+
) -> EditResult:
|
|
203
|
+
"""Add image watermark."""
|
|
204
|
+
return _watermark(
|
|
205
|
+
video, image_path=image, position=position,
|
|
206
|
+
opacity=opacity, margin=margin, output_path=output,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def crop(
|
|
210
|
+
self,
|
|
211
|
+
video: str,
|
|
212
|
+
width: int,
|
|
213
|
+
height: int,
|
|
214
|
+
x: int | None = None,
|
|
215
|
+
y: int | None = None,
|
|
216
|
+
output: str | None = None,
|
|
217
|
+
) -> EditResult:
|
|
218
|
+
"""Crop a video to a rectangular region."""
|
|
219
|
+
return _crop(video, width=width, height=height, x=x, y=y, output_path=output)
|
|
220
|
+
|
|
221
|
+
def rotate(
|
|
222
|
+
self,
|
|
223
|
+
video: str,
|
|
224
|
+
angle: int = 0,
|
|
225
|
+
flip_horizontal: bool = False,
|
|
226
|
+
flip_vertical: bool = False,
|
|
227
|
+
output: str | None = None,
|
|
228
|
+
) -> EditResult:
|
|
229
|
+
"""Rotate and/or flip a video."""
|
|
230
|
+
return _rotate(video, angle=angle, flip_horizontal=flip_horizontal, flip_vertical=flip_vertical, output_path=output)
|
|
231
|
+
|
|
232
|
+
def fade(
|
|
233
|
+
self,
|
|
234
|
+
video: str,
|
|
235
|
+
fade_in: float = 0.0,
|
|
236
|
+
fade_out: float = 0.0,
|
|
237
|
+
output: str | None = None,
|
|
238
|
+
) -> EditResult:
|
|
239
|
+
"""Add fade in/out effect to a video."""
|
|
240
|
+
return _fade(video, fade_in=fade_in, fade_out=fade_out, output_path=output)
|
|
241
|
+
|
|
242
|
+
def export(
|
|
243
|
+
self,
|
|
244
|
+
video: str,
|
|
245
|
+
output: str | None = None,
|
|
246
|
+
quality: str = "high",
|
|
247
|
+
format: str = "mp4",
|
|
248
|
+
) -> EditResult:
|
|
249
|
+
"""Render final video with quality settings."""
|
|
250
|
+
return _export_video(video, output_path=output, quality=quality, format=format)
|
|
251
|
+
|
|
252
|
+
def edit(self, timeline: dict[str, Any], output: str | None = None) -> EditResult:
|
|
253
|
+
"""Execute a full timeline-based edit from JSON."""
|
|
254
|
+
return _edit_timeline(timeline, output_path=output)
|
|
255
|
+
|
|
256
|
+
def extract_audio(
|
|
257
|
+
self,
|
|
258
|
+
video: str,
|
|
259
|
+
output: str | None = None,
|
|
260
|
+
format: str = "mp3",
|
|
261
|
+
) -> EditResult:
|
|
262
|
+
"""Extract audio track from video."""
|
|
263
|
+
result_path = _extract_audio(video, output_path=output, format=format)
|
|
264
|
+
return EditResult(
|
|
265
|
+
output_path=result_path,
|
|
266
|
+
operation="extract_audio",
|
|
267
|
+
format=format,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Fix the circular import for resize
|
|
272
|
+
from .engine import resize as _resize
|