flits-video 0.2.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.
flits/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """Python wrapper for flits, the fast GPU video renderer.
2
+
3
+ Build a scene as a dict (typed with `flits.types.Scene`) and render it with the flits CLI:
4
+
5
+ import flits
6
+ scene = {"width": 1080, "height": 1920, "duration": 3,
7
+ "layers": [{"type": "text", "text": "Hello", "size": 120, "in": "pop"}]}
8
+ print(flits.check(scene))
9
+ flits.render(scene, "hello.mp4", scale=0.5)
10
+
11
+ The CLI is found via the `bin=` argument, $FLITS_BIN or PATH. Relative media paths in a scene dict resolve
12
+ against `base_dir` (default: the current directory).
13
+ """
14
+
15
+ from .compile import compile, flatten, sample
16
+ from ._cli import CheckResult, FlitsError, binary, check, render, run, scene_file, schema, sheet, still, to_args, transcribe
17
+ from .types import Layer, Scene
18
+
19
+ __all__ = [
20
+ "compile",
21
+ "flatten",
22
+ "sample",
23
+ "CheckResult",
24
+ "FlitsError",
25
+ "Layer",
26
+ "Scene",
27
+ "binary",
28
+ "check",
29
+ "render",
30
+ "run",
31
+ "scene_file",
32
+ "schema",
33
+ "sheet",
34
+ "still",
35
+ "to_args",
36
+ "transcribe",
37
+ ]
38
+ __version__ = "0.2.0"
flits/_cli.py ADDED
@@ -0,0 +1,230 @@
1
+ """Run the flits CLI from Python (subprocess; no other dependencies)."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from contextlib import contextmanager
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Union
13
+
14
+ from .compile import compile
15
+
16
+ PathLike = Union[str, "os.PathLike[str]"]
17
+ SceneInput = Union[Mapping[str, Any], PathLike]
18
+ """A scene dict (see flits.types.Scene) or the path of a scene .json file."""
19
+
20
+
21
+ class FlitsError(RuntimeError):
22
+ """flits failed (or could not be started; then returncode is None). `stderr`/`stdout` hold its output."""
23
+
24
+ def __init__(self, message: str, returncode: Optional[int] = None, stderr: str = "", args: Sequence[str] = (), stdout: str = ""):
25
+ super().__init__(message)
26
+ self.returncode = returncode
27
+ self.stderr = stderr
28
+ self.stdout = stdout
29
+ self.cmd = list(args)
30
+
31
+
32
+ @dataclass
33
+ class CheckResult:
34
+ """Outcome of `check`: ok means the scene loads and prepares (warnings don't make it fail)."""
35
+
36
+ ok: bool
37
+ warnings: List[str] = field(default_factory=list)
38
+ error: Optional[str] = None
39
+ output: str = ""
40
+
41
+
42
+ def binary(bin: Optional[PathLike] = None) -> str:
43
+ """The flits executable: `bin`, $FLITS_BIN or `flits` on PATH."""
44
+ if bin is not None:
45
+ return os.fspath(bin)
46
+ env = os.environ.get("FLITS_BIN")
47
+ if env:
48
+ return env
49
+ found = shutil.which("flits")
50
+ if not found:
51
+ raise FlitsError("flits not found: install it (https://github.com/ArjandenHartog/flits) or set FLITS_BIN")
52
+ return found
53
+
54
+
55
+ def to_args(options: Mapping[str, Any]) -> List[str]:
56
+ """{"keep_temp": True, "workers": 4} -> ["--keep-temp", "--workers", "4"]"""
57
+ args: List[str] = []
58
+ for key, value in options.items():
59
+ if value is None or value is False:
60
+ continue
61
+ flag = "--" + key.replace("_", "-")
62
+ if value is True:
63
+ args.append(flag)
64
+ elif isinstance(value, (list, tuple)):
65
+ for v in value:
66
+ args += [flag, str(v)]
67
+ else:
68
+ args += [flag, os.fspath(value) if isinstance(value, os.PathLike) else str(value)]
69
+ return args
70
+
71
+
72
+ def run(args: Sequence[str], *, bin: Optional[PathLike] = None, echo: bool = False, cwd: Optional[PathLike] = None) -> str:
73
+ """Run `flits <args>` and return stdout. With echo=True, progress (stderr) is copied to sys.stderr live."""
74
+ cmd = [binary(bin), *map(str, args)]
75
+ try:
76
+ proc = subprocess.Popen(
77
+ cmd,
78
+ cwd=cwd,
79
+ stdin=subprocess.DEVNULL,
80
+ stdout=subprocess.PIPE,
81
+ stderr=subprocess.PIPE,
82
+ text=True,
83
+ encoding="utf-8",
84
+ errors="replace",
85
+ )
86
+ except FileNotFoundError as e:
87
+ raise FlitsError(f"flits not found ({cmd[0]}): install it or set FLITS_BIN") from e
88
+ if echo:
89
+ # stdout is small (paths, timelines); stream stderr, then collect stdout
90
+ err_parts: List[str] = []
91
+ stderr, stdout = proc.stderr, proc.stdout
92
+ assert stderr is not None and stdout is not None
93
+ for chunk in iter(lambda: stderr.read(256), ""):
94
+ err_parts.append(chunk)
95
+ sys.stderr.write(chunk)
96
+ sys.stderr.flush()
97
+ out = stdout.read()
98
+ proc.wait()
99
+ err = "".join(err_parts)
100
+ else:
101
+ out, err = proc.communicate()
102
+ if proc.returncode != 0:
103
+ lines = [line for line in err.splitlines() if line.startswith("error:")]
104
+ msg = lines[-1] if lines else f"flits exited with code {proc.returncode}"
105
+ raise FlitsError(msg, proc.returncode, err, cmd, out)
106
+ return out
107
+
108
+
109
+ @contextmanager
110
+ def scene_file(scene: SceneInput, base_dir: Optional[PathLike] = None) -> Iterator[str]:
111
+ """Yield the scene as a file path. A dict is written to a temporary file in `base_dir` (default: the current
112
+ directory) so relative media paths in it resolve against that folder; the file is removed afterwards."""
113
+ if not isinstance(scene, Mapping):
114
+ yield os.fspath(scene)
115
+ return
116
+ folder = Path(base_dir if base_dir is not None else os.getcwd()).resolve()
117
+ fd, path = tempfile.mkstemp(prefix=".flits-", suffix=".scene.json", dir=folder)
118
+ try:
119
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
120
+ json.dump(compile(dict(scene)), f, ensure_ascii=False, indent=1)
121
+ yield path
122
+ finally:
123
+ try:
124
+ os.remove(path)
125
+ except OSError:
126
+ pass
127
+
128
+
129
+ def _lines(s: str) -> List[str]:
130
+ return [line.strip() for line in s.splitlines() if line.strip()]
131
+
132
+
133
+ def render(
134
+ scene: SceneInput,
135
+ out: PathLike,
136
+ *,
137
+ base_dir: Optional[PathLike] = None,
138
+ bin: Optional[PathLike] = None,
139
+ echo: bool = False,
140
+ **options: Any,
141
+ ) -> Path:
142
+ """Render a scene to a video and return the output path.
143
+
144
+ options are `flits render` flags: workers, encoder ("auto"|"nvenc"|"qsv"|"vaapi"|"cpu"|"null"),
145
+ codec ("h264"|"hevc"|"av1"), speed ("fast"|"balanced"|"quality"), quality (18..28), scale (0.5 = preview),
146
+ gpu ("auto"|"gpu"|"cpu"), keep_temp, quiet, remote.
147
+ """
148
+ out = Path(out).resolve()
149
+ with scene_file(scene, base_dir) as path:
150
+ run(["render", path, "-o", str(out), *to_args(options)], bin=bin, echo=echo)
151
+ return out
152
+
153
+
154
+ def still(
155
+ scene: SceneInput,
156
+ at: Union[float, str, Sequence[Union[float, str]]],
157
+ out: PathLike,
158
+ *,
159
+ scale: Optional[float] = None,
160
+ base_dir: Optional[PathLike] = None,
161
+ bin: Optional[PathLike] = None,
162
+ ) -> List[Path]:
163
+ """Render frame(s) at the given time(s) (seconds or "mm:ss") to PNG; returns the written files."""
164
+ times = list(at) if isinstance(at, (list, tuple)) else [at]
165
+ with scene_file(scene, base_dir) as path:
166
+ stdout = run(["still", path, *to_args({"at": times, "scale": scale}), "-o", str(Path(out).resolve())], bin=bin)
167
+ return [Path(p) for p in _lines(stdout)]
168
+
169
+
170
+ def sheet(
171
+ scene: SceneInput,
172
+ out: PathLike,
173
+ *,
174
+ base_dir: Optional[PathLike] = None,
175
+ bin: Optional[PathLike] = None,
176
+ **options: Any,
177
+ ) -> Path:
178
+ """Contact sheet: evenly spaced frames in one PNG (options: count, cols, thumb)."""
179
+ out = Path(out).resolve()
180
+ with scene_file(scene, base_dir) as path:
181
+ run(["sheet", path, "-o", str(out), *to_args(options)], bin=bin)
182
+ return out
183
+
184
+
185
+ def check(scene: SceneInput, *, base_dir: Optional[PathLike] = None, bin: Optional[PathLike] = None) -> CheckResult:
186
+ """Validate a scene the way flits sees it (typos, bad values, missing files). Does not raise for an invalid
187
+ scene: inspect `result.ok`, `result.error` and `result.warnings`."""
188
+ def warnings(output: str) -> List[str]:
189
+ return [line[len("warning: ") :] for line in _lines(output) if line.startswith("warning: ")]
190
+
191
+ with scene_file(scene, base_dir) as path:
192
+ try:
193
+ output = run(["check", path], bin=bin)
194
+ except FlitsError as e:
195
+ if e.returncode is None:
196
+ raise
197
+ msg = str(e)
198
+ error = msg[len("error: ") :] if msg.startswith("error: ") else msg
199
+ return CheckResult(ok=False, warnings=warnings(e.stdout), error=error, output=e.stdout + e.stderr)
200
+ return CheckResult(ok=True, warnings=warnings(output), output=output)
201
+
202
+
203
+ def transcribe(
204
+ inputs: Union[PathLike, Sequence[PathLike]],
205
+ out: Optional[PathLike] = None,
206
+ *,
207
+ outdir: Optional[PathLike] = None,
208
+ model: Optional[str] = None,
209
+ lang: Optional[str] = None,
210
+ device: Optional[str] = None,
211
+ python: Optional[PathLike] = None,
212
+ remote: Optional[str] = None,
213
+ bin: Optional[PathLike] = None,
214
+ echo: bool = False,
215
+ ) -> List[Path]:
216
+ """Transcribe audio/video to word-timed JSON (+ .srt/.vtt/.txt next to it); returns the .words.json paths.
217
+
218
+ model: "default" (large-v3-turbo on GPU, small on CPU), "tiny", ... "large-v3-turbo"; lang: "en", "nl", ... or "auto";
219
+ device: "auto" | "cuda" | "cpu"; remote: GPU server URL or "off".
220
+ """
221
+ files = [inputs] if isinstance(inputs, (str, os.PathLike)) else list(inputs)
222
+ opts = {"out": out, "outdir": outdir, "model": model, "lang": lang, "device": device, "python": python, "remote": remote}
223
+ stdout = run(["transcribe", *[str(Path(f).resolve()) for f in files], *to_args(opts)], bin=bin, echo=echo)
224
+ return [Path(p) for p in _lines(stdout)]
225
+
226
+
227
+ def schema(*, bin: Optional[PathLike] = None) -> Dict[str, Any]:
228
+ """The JSON Schema of the scene format, from the installed flits."""
229
+ result: Dict[str, Any] = json.loads(run(["schema"], bin=bin))
230
+ return result
flits/compile.py ADDED
@@ -0,0 +1,102 @@
1
+ """Plain scene JSON from a scene built in Python (mirrors bindings/typescript/compile.js).
2
+
3
+ * Animatable properties may be callables ``f(t, ctx) -> float`` (``t`` = seconds since the layer started,
4
+ ``ctx`` = ``{"frame", "fps", "duration", "progress"}``); they are sampled once per frame into linear
5
+ keyframes, so any maths animates exactly as computed.
6
+ * ``children``/``layers`` may be nested lists with ``None``/``False`` entries (dropped).
7
+ """
8
+
9
+ import math
10
+ from typing import Any, Callable, Dict, List, Optional
11
+
12
+ ANIMATABLE = ("x", "y", "scale", "scaleX", "scaleY", "rotation", "opacity", "zoom", "panX", "panY", "value", "progress", "volume")
13
+
14
+
15
+ def flatten(children: Any) -> List[Any]:
16
+ """Nested lists flattened; None, False and True dropped."""
17
+ out: List[Any] = []
18
+
19
+ def walk(c: Any) -> None:
20
+ if isinstance(c, (list, tuple)):
21
+ for x in c:
22
+ walk(x)
23
+ elif c is not None and c is not False and c is not True:
24
+ out.append(c)
25
+
26
+ walk(children)
27
+ return out
28
+
29
+
30
+ def sample(fn: Callable[..., float], duration: Optional[float], fps: float, what: str = "property") -> List[list]:
31
+ """``fn`` sampled every frame over ``duration`` seconds as linear keyframes (straight stretches collapsed)."""
32
+ if duration is None or not duration > 0 or math.isinf(duration):
33
+ raise ValueError(f"{what}: a function needs to know how long the layer lasts: give it (or its group, or the scene) a duration")
34
+ n = max(1, math.ceil(duration * fps - 1e-9))
35
+ pts = []
36
+ for frame in range(n + 1):
37
+ t = min(frame / fps, duration)
38
+ v = fn(t, {"frame": frame, "fps": fps, "duration": duration, "progress": t / duration})
39
+ if not isinstance(v, (int, float)) or isinstance(v, bool) or not math.isfinite(v):
40
+ raise ValueError(f"{what}: the function returned {v!r} at t={t:.3f}s (expected a finite number)")
41
+ pts.append([round(t, 6), round(float(v), 6)])
42
+ keep = [pts[0]]
43
+ for i in range(1, len(pts) - 1):
44
+ a, b, c = keep[-1], pts[i], pts[i + 1]
45
+ lin = a[1] + (c[1] - a[1]) * (b[0] - a[0]) / (c[0] - a[0])
46
+ if abs(lin - b[1]) > 1e-4:
47
+ keep.append(b)
48
+ if len(pts) > 1:
49
+ keep.append(pts[-1])
50
+ return [k if i == 0 else [k[0], k[1], "linear"] for i, k in enumerate(keep)]
51
+
52
+
53
+ def _lifetime(layer: Dict[str, Any], parent: Optional[float]) -> Optional[float]:
54
+ if isinstance(layer.get("duration"), (int, float)):
55
+ return layer["duration"]
56
+ if isinstance(layer.get("end"), (int, float)):
57
+ return layer["end"] - layer.get("start", 0)
58
+ if parent is not None:
59
+ return parent - layer.get("start", 0)
60
+ return None
61
+
62
+
63
+ def _layer(layer: Any, parent: Optional[float], fps: float, path: str) -> Dict[str, Any]:
64
+ if not isinstance(layer, dict):
65
+ raise ValueError(f"{path}: expected a layer dict, got {type(layer).__name__}")
66
+ dur = _lifetime(layer, parent)
67
+ out: Dict[str, Any] = {}
68
+ for k, v in layer.items():
69
+ if v is None:
70
+ continue
71
+ if callable(v):
72
+ if k not in ANIMATABLE:
73
+ raise ValueError(f"{path}.{k}: only animatable properties ({', '.join(ANIMATABLE)}) can be functions")
74
+ out[k] = sample(v, dur, fps, f"{path}.{k}")
75
+ elif k == "children":
76
+ kids = flatten(v)
77
+ out[k] = [_layer(c, None if layer.get("sequence") else dur, fps, f"{path}.children[{i}]") for i, c in enumerate(kids)]
78
+ else:
79
+ out[k] = v
80
+ return out
81
+
82
+
83
+ def compile(scene: Dict[str, Any]) -> Dict[str, Any]:
84
+ """Plain scene JSON: callables sampled, children flattened."""
85
+ if not isinstance(scene, dict):
86
+ raise ValueError("the scene must be a dict like {'width': ..., 'height': ..., 'layers': [...]}")
87
+ fps = scene.get("fps", 30)
88
+ out = {k: v for k, v in scene.items() if v is not None and k not in ("layers", "audio")}
89
+ out["layers"] = [_layer(l, scene.get("duration"), fps, f"layers[{i}]") for i, l in enumerate(flatten(scene.get("layers", [])))]
90
+ audio = flatten(scene.get("audio", []))
91
+ if audio:
92
+ tracks = []
93
+ for i, a in enumerate(audio):
94
+ a = dict(a)
95
+ if callable(a.get("volume")):
96
+ d = a.get("duration")
97
+ if d is None and scene.get("duration") is not None:
98
+ d = scene["duration"] - a.get("start", 0)
99
+ a["volume"] = sample(a["volume"], d, fps, f"audio[{i}].volume")
100
+ tracks.append(a)
101
+ out["audio"] = tracks
102
+ return out
flits/py.typed ADDED
File without changes
flits/types.py ADDED
@@ -0,0 +1,404 @@
1
+ """TypedDict definitions for the flits scene format.
2
+
3
+ Mirrors schema/scene.schema.json (generated from the Rust structs with `flits schema`); the Rust test
4
+ tests/schema.rs checks that every property and enum value of the schema appears here. These are plain
5
+ dicts at runtime: use them for editor completion and type checking (mypy, pyright).
6
+
7
+ Units are pixels (origin top-left) and seconds; relative paths resolve against the scene's folder.
8
+ """
9
+
10
+ import sys
11
+ from typing import Callable, Dict, List, Literal, Tuple, TypedDict, Union
12
+
13
+ if sys.version_info >= (3, 11):
14
+ from typing import Required
15
+ else: # pragma: no cover
16
+ from typing_extensions import Required
17
+
18
+ __all__ = [
19
+ "Color",
20
+ "Ease",
21
+ "KeyTime",
22
+ "Keyframe",
23
+ "FromTo",
24
+ "Animated",
25
+ "TransitionType",
26
+ "TransitionSpec",
27
+ "Transition",
28
+ "Duck",
29
+ "Padding",
30
+ "LayerType",
31
+ "Fit",
32
+ "Align",
33
+ "Reveal",
34
+ "Wrap",
35
+ "CaptionStyle",
36
+ "CaptionPreset",
37
+ "ShapeKind",
38
+ "LineCap",
39
+ "HighlightBox",
40
+ "TextBgMode",
41
+ "GradientType",
42
+ "Border",
43
+ "Shadow",
44
+ "StrokeSpec",
45
+ "TextBg",
46
+ "Filter",
47
+ "Gradient",
48
+ "AudioTrack",
49
+ "Layer",
50
+ "Scene",
51
+ ]
52
+
53
+ # ------------------------------------------------------------------ values
54
+
55
+ Color = str
56
+ """'#rgb', '#rgba', '#rrggbb', '#rrggbbaa', 'rgb(r,g,b)', 'rgba(r,g,b,a)' or a name:
57
+ "white", "black", "transparent", "red", "green", "blue", "yellow", "orange", "purple", "pink", "gray", "grey"."""
58
+
59
+ EaseName = Literal[
60
+ "linear",
61
+ "in",
62
+ "out",
63
+ "in-out",
64
+ "smooth",
65
+ "back-in",
66
+ "back-out",
67
+ "elastic",
68
+ "bounce",
69
+ "spring",
70
+ "hold",
71
+ "expo-in",
72
+ "expo-out",
73
+ "expo-in-out",
74
+ "quint-out",
75
+ "quint-in-out",
76
+ "circ-out",
77
+ # aliases
78
+ "ease-in",
79
+ "easein",
80
+ "ease-out",
81
+ "easeout",
82
+ "ease",
83
+ "inout",
84
+ "ease-in-out",
85
+ "easeinout",
86
+ "sine",
87
+ "sine-in-out",
88
+ "anticipate",
89
+ "back",
90
+ "overshoot",
91
+ "step",
92
+ "expo",
93
+ "quint",
94
+ "circ",
95
+ ]
96
+ Ease = Union[EaseName, str]
97
+ """An easing name, or a CSS curve as the string "cubic-bezier(x1, y1, x2, y2)"."""
98
+
99
+ KeyTime = Union[float, str]
100
+ """Seconds from the layer start, or "end" / "end-0.5" (relative to the layer end)."""
101
+
102
+ Keyframe = Union[Tuple[KeyTime, float], Tuple[KeyTime, float, Ease], List[Union[KeyTime, float, str]]]
103
+ """[time, value] or [time, value, ease]; the ease applies to the segment ending at this key."""
104
+
105
+
106
+ # functional syntax because "from" is a Python keyword
107
+ FromTo = TypedDict(
108
+ "FromTo",
109
+ {
110
+ "from": Required[float],
111
+ "to": Required[float],
112
+ "ease": Ease,
113
+ "start": KeyTime,
114
+ "end": KeyTime,
115
+ "comment": str,
116
+ },
117
+ total=False,
118
+ )
119
+ """Animate from one value to another over the layer (or between "start" and "end")."""
120
+
121
+ AnimFn = Callable[[float, Dict[str, float]], float]
122
+ """f(t, {"frame", "fps", "duration", "progress"}) -> value; sampled per frame by flits.compile()."""
123
+
124
+ Animated = Union[float, List[Keyframe], FromTo, AnimFn]
125
+ """A constant, keyframes [[time, value, ease?], ...] or {"from", "to", "ease", "start", "end"}."""
126
+
127
+ TransitionType = Literal[
128
+ "fade",
129
+ "slide-up",
130
+ "slide-down",
131
+ "slide-left",
132
+ "slide-right",
133
+ "slide-fade",
134
+ "zoom-in",
135
+ "zoom-out",
136
+ "zoom-fade",
137
+ "zoom-blur",
138
+ "pop",
139
+ "wipe-left",
140
+ "wipe-right",
141
+ "wipe-up",
142
+ "wipe-down",
143
+ "push-left",
144
+ "push-right",
145
+ "push-up",
146
+ "push-down",
147
+ "rotate-in",
148
+ "flip-x",
149
+ "flip-y",
150
+ "iris",
151
+ # aliases: rise = slide-fade, zoom = zoom-in, wipe = wipe-right, push = push-right, rotate = rotate-in, flip = flip-x, circle = iris
152
+ "up",
153
+ "down",
154
+ "left",
155
+ "right",
156
+ "rise",
157
+ "zoom",
158
+ "wipe",
159
+ "push",
160
+ "rotate",
161
+ "flip",
162
+ "circle",
163
+ ]
164
+
165
+ TransitionSpec = TypedDict(
166
+ "TransitionSpec",
167
+ {
168
+ "type": TransitionType, # default "fade"
169
+ "duration": float, # seconds (default 0.5; pop 0.45; iris and push 0.6)
170
+ "ease": Ease, # default "out"; pop "back-out"; wipes, push and iris "in-out"
171
+ "distance": float, # slide distance in px (default 120; slide-fade 40)
172
+ "angle": float, # rotate-in: degrees turned during the transition (default 90)
173
+ "comment": str,
174
+ },
175
+ total=False,
176
+ )
177
+
178
+ Transition = Union[str, TransitionSpec]
179
+ """"fade", "pop 0.4" (type + seconds) or a TransitionSpec."""
180
+
181
+ Duck = Union[bool, float]
182
+ """True (duck to 0.25) or the volume level (0..1) to duck to while other audio plays."""
183
+
184
+ Padding = Union[float, Tuple[float, float], List[float]]
185
+
186
+ LayerType = Literal["rect", "image", "video", "text", "counter", "captions", "waveform", "shape", "group"]
187
+ ShapeKind = Literal["rect", "ellipse", "circle", "line", "arrow", "polygon", "star", "path"]
188
+ LineCap = Literal["round", "butt", "square"]
189
+ Fit = Literal["cover", "contain", "fill", "stretch"]
190
+ Align = Literal["left", "center", "right", "start", "middle", "end"]
191
+ Reveal = Literal["words", "lines", "chars", "typewriter", "none"]
192
+ RevealStyle = Literal["rise", "mask", "fade", "blur"]
193
+ MotionBlurSpec = TypedDict("MotionBlurSpec", {"samples": int, "shutter": float}, total=False)
194
+ MotionBlur = Union[bool, MotionBlurSpec]
195
+ """True (up to 8 samples, 180 degree shutter) or the most samples per moving layer and the shutter (0.5 = 180 degrees)."""
196
+ Wrap = Literal["balance", "greedy"]
197
+ CaptionStyle = Literal[
198
+ "karaoke", "pop", "bounce", "word", "line", "highlight", "tiktok", "jump", "single", "plain", "subtitle",
199
+ ]
200
+ CaptionPreset = Literal[
201
+ "tiktok", "karaoke-box", "bounce", "minimal", "neon", "highlighter", "subtitle", "word-pop-big",
202
+ ]
203
+ HighlightBox = Literal["pill", "marker", "box", "highlighter"]
204
+ TextBgMode = Literal["box", "lines"]
205
+ GradientType = Literal["linear", "radial"]
206
+
207
+ # ------------------------------------------------------------------ objects
208
+
209
+
210
+ class Border(TypedDict, total=False):
211
+ """Border drawn outside the box."""
212
+
213
+ width: Required[float]
214
+ color: Required[Color]
215
+ comment: str
216
+
217
+
218
+ class Shadow(TypedDict, total=False):
219
+ """Drop shadow (text: text shadow; other layers: box shadow)."""
220
+
221
+ color: Color # default "rgba(0,0,0,0.5)"
222
+ blur: float # default 24
223
+ x: float # default 0
224
+ y: float # default 12
225
+ comment: str
226
+
227
+
228
+ class StrokeSpec(TypedDict, total=False):
229
+ """Text outline."""
230
+
231
+ width: Required[float]
232
+ color: Required[Color]
233
+ comment: str
234
+
235
+
236
+ class TextBg(TypedDict, total=False):
237
+ """Box (or one pill per line) behind text."""
238
+
239
+ color: Required[Color]
240
+ padding: Padding
241
+ radius: float
242
+ mode: TextBgMode
243
+ comment: str
244
+
245
+
246
+ class Filter(TypedDict, total=False):
247
+ """Colour grading for image and video layers."""
248
+
249
+ brightness: float # added, e.g. 0.05
250
+ contrast: float # factor, default 1
251
+ saturation: float # factor, default 1 (0 = gray)
252
+ warmth: float # e.g. 0.3 (negative = cooler)
253
+ grayscale: bool
254
+ comment: str
255
+
256
+
257
+ Gradient = TypedDict(
258
+ "Gradient",
259
+ {
260
+ "type": GradientType, # default "linear"
261
+ "angle": float, # CSS-style degrees: 180 = top to bottom (default)
262
+ "stops": List[Tuple[float, Color]], # [[offset 0..1, colour], ...]
263
+ "colors": List[Color], # spread evenly (alternative to stops)
264
+ "comment": str,
265
+ },
266
+ total=False,
267
+ )
268
+
269
+
270
+ class AudioTrack(TypedDict, total=False):
271
+ """An audio file in the soundtrack."""
272
+
273
+ src: Required[str]
274
+ start: float # scene time where the track starts
275
+ trim: float # seconds to skip in the source
276
+ duration: float
277
+ volume: Animated
278
+ fadeIn: float
279
+ fadeOut: float
280
+ loop: bool
281
+ duck: Duck
282
+ comment: str
283
+
284
+
285
+ Layer = TypedDict(
286
+ "Layer",
287
+ {
288
+ "type": Required[LayerType],
289
+ "id": str,
290
+ "start": float, # seconds, relative to the parent group
291
+ "end": float,
292
+ "duration": float,
293
+ "x": Animated, # anchor position in px (default: centre of the parent)
294
+ "y": Animated,
295
+ "width": float,
296
+ "height": float,
297
+ "anchor": Tuple[float, float], # [0.5, 0.5] = centre (default), [0, 0] = top-left
298
+ "scale": Animated,
299
+ "scaleX": Animated,
300
+ "scaleY": Animated,
301
+ "rotation": Animated, # degrees
302
+ "opacity": Animated,
303
+ "zoom": Animated, # image/video: Ken Burns zoom inside the box
304
+ "panX": Animated, # -1..1
305
+ "panY": Animated,
306
+ "in": Transition,
307
+ "out": Transition,
308
+ "radius": float,
309
+ "border": Border,
310
+ "shadow": Shadow,
311
+ "filter": Filter,
312
+ "clip": bool, # group: hide everything outside the box
313
+ "motionBlur": bool, # False: no motion blur for this layer and its children
314
+ "comment": str,
315
+ # image / video / captions / waveform
316
+ "src": str,
317
+ "fit": Fit,
318
+ "blur": float,
319
+ "trim": float,
320
+ "volume": Animated,
321
+ "loop": bool,
322
+ "speed": float,
323
+ "fadeIn": float,
324
+ "fadeOut": float,
325
+ # rect
326
+ "color": Color,
327
+ "gradient": Gradient,
328
+ # text (and counter/captions styling)
329
+ "text": str,
330
+ "font": str,
331
+ "size": float,
332
+ "weight": float,
333
+ "align": Align,
334
+ "maxWidth": float,
335
+ "lineHeight": float,
336
+ "letterSpacing": float,
337
+ "stroke": StrokeSpec,
338
+ "background": TextBg,
339
+ "uppercase": bool,
340
+ "reveal": Reveal,
341
+ "wrap": Wrap,
342
+ "revealDuration": float,
343
+ "revealStyle": RevealStyle, # rise (default), mask, fade, blur
344
+ # captions
345
+ "preset": CaptionPreset, # named look; layer properties override it
346
+ "style": CaptionStyle,
347
+ "highlight": Color, # active word colour (default yellow)
348
+ "highlightBg": Color, # box behind the active word
349
+ "highlightBox": HighlightBox, # "pill" (default) or "marker"
350
+ "maxWords": int,
351
+ "maxLines": int, # default 2
352
+ "colors": List[Color], # active-word colours, cycled word by word
353
+ "activeScale": float, # active word size (default 1.12 for pop, 1 otherwise)
354
+ "speakerColors": Dict[str, Color], # word colour per diarization speaker
355
+ # shape
356
+ "shape": ShapeKind, # rect (default), ellipse, circle, line, arrow, polygon, star, path
357
+ "points": List[Tuple[float, float]], # [[x, y], ...] in the layer box
358
+ "d": str, # shape "path": SVG path data in layer-box units
359
+ "sides": int, # polygon corners (6) / star points (5)
360
+ "innerRadius": float, # star: inner radius as a fraction of the outer one (0.5)
361
+ "lineCap": LineCap,
362
+ "dash": List[float], # dash pattern in px, e.g. [20, 10]
363
+ "progress": Animated, # how much of the outline is drawn, 0..1 (animate to draw on)
364
+ "headSize": float, # arrow head length in px (default 4 x the stroke width)
365
+ # waveform
366
+ "bars": int,
367
+ "mirror": bool,
368
+ "gap": float,
369
+ "gain": float,
370
+ # counter
371
+ "value": Animated,
372
+ "decimals": int,
373
+ "prefix": str,
374
+ "suffix": str,
375
+ "separator": str, # "," (default), "." or "none"
376
+ # group
377
+ "children": List["Layer"],
378
+ "sequence": bool,
379
+ "overlap": float,
380
+ },
381
+ total=False,
382
+ )
383
+ """One layer. Required per type: image/video/captions/waveform need "src", text needs "text", counter needs "value"."""
384
+
385
+ Scene = TypedDict(
386
+ "Scene",
387
+ {
388
+ "$schema": str, # ignored by flits
389
+ "width": Required[int],
390
+ "height": Required[int],
391
+ "fps": float, # default 30
392
+ "duration": float, # default: end of the last layer
393
+ "background": Color,
394
+ "fonts": Dict[str, str], # alias -> font name or file
395
+ "fontFallback": List[str], # fonts for characters a layer's font lacks (emoji, other scripts)
396
+ "audio": List[AudioTrack],
397
+ "loudness": float, # LUFS, e.g. -14
398
+ "motionBlur": MotionBlur,
399
+ "layers": List[Layer],
400
+ "comment": str,
401
+ },
402
+ total=False,
403
+ )
404
+ """A flits scene: one video described as layers on a canvas."""
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: flits-video
3
+ Version: 0.2.0
4
+ Summary: Python wrapper for the flits video renderer: build scenes as dicts, render them with the flits CLI
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/ArjandenHartog/flits
7
+ Keywords: video,rendering,flits,transcription,captions
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Topic :: Multimedia :: Video
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: typing_extensions>=4.0; python_version < "3.11"
15
+
16
+ Render flits scenes (JSON video descriptions) from Python. Needs the `flits` CLI on PATH or $FLITS_BIN. See https://github.com/ArjandenHartog/flits
@@ -0,0 +1,9 @@
1
+ flits/__init__.py,sha256=W04-Pyld293jzsy5MOg94nAJb6ihe2X6rFHfPLEupyo,1047
2
+ flits/_cli.py,sha256=T0pFYiOnXf7BVnyqA_DO4zUMMQZ_Mwss0qn2x6TwB2M,8677
3
+ flits/compile.py,sha256=mkVsinAU6Qdd0U2y2v__jO0RkU-uWeyG2uZxkccVBmM,4531
4
+ flits/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ flits/types.py,sha256=mgcsxHdhU3aT8oef2Ef-9iWMyWUXWu6J4taY-2bJ46s,12021
6
+ flits_video-0.2.0.dist-info/METADATA,sha256=fOMW0RxtIsrI_QAfwwI3AJgkxxUBuCnkNJJ55xXn0So,753
7
+ flits_video-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ flits_video-0.2.0.dist-info/top_level.txt,sha256=-3yg-zvnKSlQNvHzAu70rEiwLdCSq0oGbc5uLFAvrkg,6
9
+ flits_video-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flits