mirror of
https://github.com/vondas-network/videobeaux.git
synced 2025-12-05 15:30:02 +01:00
updated lagkage, docs and examples
This commit is contained in:
@@ -1,402 +1,179 @@
|
||||
LAGKAGE – CORE FUNCTIONALITY EXAMPLES (v2)
|
||||
========================================
|
||||
|
||||
Program:
|
||||
lagkage (Videobeaux)
|
||||
|
||||
Base usage pattern:
|
||||
videobeaux -P lagkage -i media/base.mp4 -o out/NAME.mp4 --layout-json lagkage_layouts/LAYOUT.json --force
|
||||
|
||||
This file contains 20+ examples demonstrating:
|
||||
- place vs free positioning
|
||||
- images / videos / GIFs
|
||||
- sequence_direction variations
|
||||
- per-layer zoom
|
||||
- per-layer crop
|
||||
- combinations of the above
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 01 – Minimal logo + sticker (place + free)
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_minimal.json
|
||||
|
||||
Description:
|
||||
- Top-right logo bug using place mode
|
||||
- Free-position looping GIF sticker at lower-left area
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex01_minimal_logo_sticker.mp4 \
|
||||
--layout-json lagkage_layouts/layout_minimal.json \
|
||||
1) Logo in top-right corner
|
||||
----------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex01_layout_ex01_logo_corner.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex01_logo_corner.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 02 – Same layout, different base
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_minimal.json
|
||||
|
||||
Description:
|
||||
- Reuses the same layout with a different base video (e.g. a show cut)
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base_show.mp4 \
|
||||
-o out/ex02_minimal_logo_sticker_show.mp4 \
|
||||
--layout-json lagkage_layouts/layout_minimal.json \
|
||||
2) Center GIF sticker
|
||||
----------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex02_layout_ex02_gif_center_sticker.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex02_gif_center_sticker.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 03 – Dual PIP + logo + lower third
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_pip_dual.json
|
||||
|
||||
Description:
|
||||
- Logo in top-left
|
||||
- Left and right PIP cameras using free mode
|
||||
- Image lower-third band
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex03_pip_dual_show.mp4 \
|
||||
--layout-json lagkage_layouts/layout_pip_dual.json \
|
||||
3) Dual PIP videos with per-layer gains (all audio)
|
||||
----------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex03_layout_ex03_dual_pip_videos.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex03_dual_pip_videos.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 04 – Dual PIP, backward stacking
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_pip_dual.json
|
||||
(Edit JSON: "sequence_direction": "backward")
|
||||
|
||||
Description:
|
||||
- Same positions as Example 03
|
||||
- Backward stacking puts higher layer_number on top
|
||||
- Good for making sure logo always ends above other overlays
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex04_pip_dual_backward.mp4 \
|
||||
--layout-json lagkage_layouts/layout_pip_dual.json \
|
||||
4) Lower-third band from image
|
||||
-------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex04_layout_ex04_lower_third_band.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex04_lower_third_band.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 05 – Stickers + VHS noise overlay
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_stickers.json
|
||||
|
||||
Description:
|
||||
- Full-frame looping VHS noise (low opacity)
|
||||
- Circus and bunny GIF stickers
|
||||
- Floating logo
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex05_stickers_vhs_noise.mp4 \
|
||||
--layout-json lagkage_layouts/layout_stickers.json \
|
||||
5) Center zoomed crop PIP
|
||||
--------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex05_layout_ex05_center_zoom_crop_pip.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex05_center_zoom_crop_pip.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 06 – Freeform showcase (cams, gifs, masks, noise)
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_freeform_showcase.json
|
||||
|
||||
Description:
|
||||
- Multiple layers:
|
||||
- top-right logo, top-left screen
|
||||
- left/right PIP cams, mask on edge, lower band
|
||||
- circus & bunny GIFs, center VHS noise
|
||||
- All positioned using free mode coordinates
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex06_freeform_showcase.mp4 \
|
||||
--layout-json lagkage_layouts/layout_freeform_showcase.json \
|
||||
6) Scatter GIFs + logo (random sequence)
|
||||
-----------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex06_layout_ex06_scatter_gifs_logo.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex06_scatter_gifs_logo.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 07 – 3-up horizontal video grid
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_grid_3up.json
|
||||
|
||||
Description:
|
||||
- Three video tiles left/center/right
|
||||
- Small logo in top-left
|
||||
- Good for side-by-side comparison or triptych views
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex07_grid_3up.mp4 \
|
||||
--layout-json lagkage_layouts/layout_grid_3up.json \
|
||||
7) Logo grid around edges
|
||||
--------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex07_layout_ex07_logo_grid_edges.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex07_logo_grid_edges.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 08 – 4-up grid (quad cam)
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_grid_4up.json
|
||||
|
||||
Description:
|
||||
- Four camera feeds in a 2x2 grid
|
||||
- Classic multi-cam surveillance / gallery layout
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex08_grid_4up.mp4 \
|
||||
--layout-json lagkage_layouts/layout_grid_4up.json \
|
||||
8) Story mode: band + PIP + logo (external audio)
|
||||
--------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex08_layout_ex08_story_mode_external_audio.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex08_story_mode_external_audio.json \
|
||||
--audio-mode external --audio-src media/music_track.wav \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 09 – Heavy VHS wash + mask + logo
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_vhs_heavy.json
|
||||
|
||||
Description:
|
||||
- VHS noise strongly overlayed (higher opacity)
|
||||
- Center mask image
|
||||
- Top-center logo
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex09_vhs_heavy_mask_logo.mp4 \
|
||||
--layout-json lagkage_layouts/layout_vhs_heavy.json \
|
||||
9) Split screen: left zoomed, right normal (all audio)
|
||||
-------------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex09_layout_ex09_split_screen.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex09_split_screen.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 10 – Logo wall + mid screens
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_logo_wall.json
|
||||
|
||||
Description:
|
||||
- Logos in all four corners using place mode
|
||||
- Two mid-frame screen images using free mode
|
||||
- Good for brand-heavy overlays
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex10_logo_wall.mp4 \
|
||||
--layout-json lagkage_layouts/layout_logo_wall.json \
|
||||
10) Full showcase: GIFs + videos + logo (all audio)
|
||||
---------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex10_layout_ex10_full_showcase.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex10_full_showcase.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 11 – Random spray of gifs + center logo
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_random_spray.json
|
||||
(sequence_direction set to "random")
|
||||
|
||||
Description:
|
||||
- Circus and bunny GIFs in four corners/edges
|
||||
- Logo in center
|
||||
- Randomized stacking order each run (depending on code behavior)
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex11_random_spray.mp4 \
|
||||
--layout-json lagkage_layouts/layout_random_spray.json \
|
||||
11) Silent graphics-only output
|
||||
-------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex11_layout_ex11_silent_graphics_only.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex11_silent_graphics_only.json \
|
||||
--audio-mode none \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 12 – Story mode: lower third + mask + tiny PIP
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_story_mode.json
|
||||
|
||||
Description:
|
||||
- Wide lower-third band defined via free mode
|
||||
- Side mask element
|
||||
- Small corner PIP video + logo
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex12_story_mode_package.mp4 \
|
||||
--layout-json lagkage_layouts/layout_story_mode.json \
|
||||
12) JSON-only audio with different linear gains
|
||||
-----------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex12_layout_ex12_json_only_audio_gains.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex12_json_only_audio_gains.json \
|
||||
--audio-mode json_only \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 13 – Freeform scatter demo
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_freeform_showcase.json
|
||||
|
||||
Description:
|
||||
- Same freeform layout as Example 06
|
||||
- Emphasis on scattered positioning to test boundaries
|
||||
- Demonstrates partially off-screen elements
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex13_freeform_scatter.mp4 \
|
||||
--layout-json lagkage_layouts/layout_freeform_showcase.json \
|
||||
13) Backward sequence stacked logos
|
||||
-----------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex13_layout_ex13_backward_stack.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex13_backward_stack.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 14 – Minimal zoom-only sticker
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_minimal_zoomcrop.json
|
||||
(Create JSON from README example)
|
||||
|
||||
Description:
|
||||
- Corner logo
|
||||
- One GIF sticker with zoom > 1.0 but no crop, so content is pushed in
|
||||
- Demonstrates per-layer zoom inside fixed box
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex14_minimal_zoom_sticker.mp4 \
|
||||
--layout-json lagkage_layouts/layout_minimal_zoomcrop.json \
|
||||
14) Place mode quadrants with different sizes
|
||||
---------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex14_layout_ex14_place_quadrants.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex14_place_quadrants.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 15 – Center-cropped GIF in the corner
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_cropped_gif_corner.json
|
||||
(GIF layer has crop_x/crop_y/crop_w/crop_h set to central 50%)
|
||||
|
||||
Description:
|
||||
- Uses only the core of a noisy or busy GIF
|
||||
- Placed bottom-right as a cropped badge
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex15_cropped_gif_corner.mp4 \
|
||||
--layout-json lagkage_layouts/layout_cropped_gif_corner.json \
|
||||
15) UI-style small stickers around center
|
||||
-----------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex15_layout_ex15_ui_stickers.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex15_ui_stickers.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 16 – Zoomed-in PIP camera box
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_zoomed_pip.json
|
||||
(PIP layer has zoom > 1.0)
|
||||
|
||||
Description:
|
||||
- Small PIP box, but content is zoomed in
|
||||
- Good for focusing on a region of the camera feed without changing frame size
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex16_zoomed_pip_box.mp4 \
|
||||
--layout-json lagkage_layouts/layout_zoomed_pip.json \
|
||||
16) Zoom showcase at multiple levels (all audio)
|
||||
------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex16_layout_ex16_zoom_showcase.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex16_zoom_showcase.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 17 – Combined crop + zoom (GIF)
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_crop_zoom_gif.json
|
||||
|
||||
Description:
|
||||
- Layer crops to middle 60% of GIF
|
||||
- Then applies zoom=1.8 inside overlay box
|
||||
- Great for punching into the “interesting” part of the animation
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex17_crop_zoom_gif.mp4 \
|
||||
--layout-json lagkage_layouts/layout_crop_zoom_gif.json \
|
||||
17) Crop focus 'face cam' PIP
|
||||
-----------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex17_layout_ex17_crop_focus.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex17_crop_focus.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 18 – Split-screen: left static, right zoom+crop
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_splitscreen_zoomcrop.json
|
||||
|
||||
Description:
|
||||
- Left side: standard PIP video
|
||||
- Right side: same video source but with crop+zoom on a different region
|
||||
- Demonstrates comparative views of the same source file
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex18_splitscreen_zoomcrop.mp4 \
|
||||
--layout-json lagkage_layouts/layout_splitscreen_zoomcrop.json \
|
||||
18) Off-screen pushes (half visible)
|
||||
------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex18_layout_ex18_offscreen_push.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex18_offscreen_push.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 19 – Multi-logo scatter + zoomed center logo
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_logo_scatter_zoom_center.json
|
||||
|
||||
Description:
|
||||
- Logos in corners and midpoints using place + free
|
||||
- Center logo uses zoom to feel larger within a fixed box
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex19_logo_scatter_zoom_center.mp4 \
|
||||
--layout-json lagkage_layouts/layout_logo_scatter_zoom_center.json \
|
||||
19) Media path using 'media/' root
|
||||
----------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex19_layout_ex19_media_path_root.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex19_media_path_root.json \
|
||||
--audio-mode base \
|
||||
--force
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
EXAMPLE 20 – Full package: PIPs, stickers, crop+zoom, noise
|
||||
------------------------------------------------------------
|
||||
|
||||
Layout:
|
||||
lagkage_layouts/layout_full_package_showcase.json
|
||||
|
||||
Description:
|
||||
- Combines:
|
||||
- Base logo
|
||||
- Dual PIPs
|
||||
- Multiple GIF stickers (some cropped, some zoomed)
|
||||
- VHS noise layer at low opacity
|
||||
- Designed as an “everything on” stress test for lagkage
|
||||
|
||||
Command:
|
||||
videobeaux -P lagkage -i media/base.mp4 \
|
||||
-o out/ex20_full_package_showcase.mp4 \
|
||||
--layout-json lagkage_layouts/layout_full_package_showcase.json \
|
||||
20) Many audio clips premixed with different gains
|
||||
--------------------------------------------------
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/ex20_layout_ex20_audio_premix.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex20_audio_premix.json \
|
||||
--audio-mode all \
|
||||
--force
|
||||
|
||||
|
||||
END
|
||||
|
||||
405
docs/docs-lagkage.md
Normal file
405
docs/docs-lagkage.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# `lagkage` – JSON‑driven layered compositor for videobeaux
|
||||
|
||||
`lagkage` is a videobeaux program that lets you build complex, multi‑layer video compositions from a **single JSON file**.
|
||||
|
||||
You define:
|
||||
|
||||
- A **base video** (passed via `-i` as usual)
|
||||
- A set of **layers** (images, GIFs, or videos) in a JSON layout file
|
||||
- Per‑layer properties: position, size, opacity, zoom, crop, audio gain
|
||||
- Global behavior: sequence ordering, audio mix mode
|
||||
|
||||
`lagkage` then builds an ffmpeg filtergraph that:
|
||||
|
||||
- Scales, crops, and blends each layer into the base frame
|
||||
- Treats GIFs as animated overlays (pre‑processed to loop for the base duration)
|
||||
- Handles audio according to your chosen mode (base/json/all/external/none)
|
||||
- Optionally applies **per‑layer audio gain**, so you can do a rough pre‑mix
|
||||
directly from the JSON without opening a DAW
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 1. CLI usage
|
||||
|
||||
From your videobeaux project root:
|
||||
|
||||
```bash
|
||||
videobeaux -P lagkage \
|
||||
-i media/base.mp4 \
|
||||
-o out/composite.mp4 \
|
||||
--layout-json lagkage_layouts/layout_ex01_logo_corner.json \
|
||||
--force
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
- `-i, --input`
|
||||
Base video to composite onto. **Required**.
|
||||
|
||||
- `-o, --output`
|
||||
Final rendered video. **Required**.
|
||||
|
||||
- `--layout-json PATH`
|
||||
Path to a JSON file that describes all overlay layers. **Required**.
|
||||
|
||||
- `--sequence-direction {forward,backward,random}`
|
||||
Optional override for the order in which layers are applied. If omitted, the
|
||||
layout JSON’s own `sequence_direction` is used.
|
||||
|
||||
- `--audio-mode {base,all,json_only,external,none}`
|
||||
How to construct the final audio track:
|
||||
|
||||
- `base` (default) – only the base video’s audio. JSON layers are visual only.
|
||||
- `all` – mix **base audio + all JSON layer audio** together.
|
||||
- `json_only` – ignore the base audio, use only JSON layer audio.
|
||||
- `external` – ignore base + JSON audio, use the file supplied via `--audio-src`.
|
||||
- `none` – no audio at all (silent output).
|
||||
|
||||
- `--audio-src PATH`
|
||||
Path to an external audio file. Only used when `--audio-mode=external`.
|
||||
|
||||
- `--force`
|
||||
From the global videobeaux CLI: forces overwrite of existing outputs and adds
|
||||
`-y` to ffmpeg under the hood.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. Layout JSON format
|
||||
|
||||
A layout JSON looks like this at the top level:
|
||||
|
||||
```json
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 18,
|
||||
"opacity": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1 Top level
|
||||
|
||||
- `sequence_direction` (string, optional)
|
||||
|
||||
Controls how `lagkage` orders the layers before applying them:
|
||||
|
||||
- `"forward"` – smallest `layer_number` first (default)
|
||||
- `"backward"` – largest `layer_number` first
|
||||
- `"random"` – random permutation of layers
|
||||
|
||||
You can override this per‑run using `--sequence-direction` on the CLI.
|
||||
|
||||
- `layers` (array, **required**)
|
||||
|
||||
Each element is a **layer object** describing one overlay media item.
|
||||
|
||||
|
||||
### 2.2 Per‑layer fields
|
||||
|
||||
All layers share a common set of fields. Some are optional depending on mode.
|
||||
|
||||
#### Identity and source
|
||||
|
||||
- `layer_number` (int, **recommended**)
|
||||
|
||||
Used for ordering and for sequence modes. Lowest layer_number is “first” in
|
||||
forward mode. If omitted, it defaults to 0 for sorting, but it’s best to set
|
||||
explicit numbers.
|
||||
|
||||
- `name` (string, optional)
|
||||
|
||||
Short human‑readable label for the layer. Not used by the code, but useful
|
||||
for documentation.
|
||||
|
||||
- `filename` (string, **required**)
|
||||
|
||||
Path to the media file for this layer. Resolution rules:
|
||||
|
||||
- Absolute paths (`/Users/you/...`) are used as‑is.
|
||||
- URLs (`https://...`) are passed directly to ffmpeg.
|
||||
- Paths starting with `'../media/'` are normalized to `media/...` **relative
|
||||
to your project root**.
|
||||
- Paths starting with `'media/'` are also treated as project‑root relative.
|
||||
- Any other relative path is treated as **layout‑relative**:
|
||||
resolved against the folder that contains the JSON.
|
||||
|
||||
- `type` (string, **required**)
|
||||
|
||||
The kind of media:
|
||||
- `"img"` – PNG/JPEG/etc.; single frame, no audio
|
||||
- `"gif"` – animated GIF; pre‑processed to a looping RGBA video
|
||||
- `"video"` – standard video file; may contain audio
|
||||
|
||||
This mainly affects whether we run the GIF preprocessing pipeline.
|
||||
|
||||
#### Positioning: `mode`, `place`, `pos_x`, `pos_y`
|
||||
|
||||
A layer can be positioned in **place** mode (logical corners/center) or in
|
||||
**free** mode (explicit pixel coordinates).
|
||||
|
||||
- `mode` (string, **required**)
|
||||
|
||||
- `"place"` – snap the layer into a logical slot:
|
||||
- uses `place`
|
||||
- `"free"` – place the layer at exact coordinates:
|
||||
- uses `pos_x`, `pos_y`
|
||||
|
||||
- `place` (string, required if `mode="place"`)
|
||||
|
||||
One of:
|
||||
|
||||
- `"top_left"`
|
||||
- `"top_right"`
|
||||
- `"bottom_left"`
|
||||
- `"bottom_right"`
|
||||
- `"center"`
|
||||
|
||||
- `pos_x`, `pos_y` (int, required if `mode="free"`)
|
||||
|
||||
Pixel coordinates in the **base video’s pixel space**. (0,0) is top‑left of
|
||||
the base frame. These are the top‑left of the scaled/cropped layer box.
|
||||
|
||||
You *can* push a layer partially or fully off‑screen by using negative values
|
||||
or values greater than the base width/height.
|
||||
|
||||
|
||||
#### Size and opacity
|
||||
|
||||
- `size` (float, optional; default `100`)
|
||||
|
||||
Percentage of the **base video width** that this layer should occupy. The
|
||||
layer is scaled to:
|
||||
|
||||
```text
|
||||
box_w = base_width * (size / 100)
|
||||
box_h = box_w / aspect_ratio
|
||||
```
|
||||
|
||||
Both are forced to even integers (necessary for some codecs).
|
||||
|
||||
- `opacity` (float, optional; default `1.0`)
|
||||
|
||||
Range `[0.0, 1.0]`. Implemented using `colorchannelmixer` on the layer’s
|
||||
alpha channel before overlay:
|
||||
|
||||
- `1.0` – fully opaque
|
||||
- `0.5` – half transparent
|
||||
- `0.0` – fully transparent (invisible)
|
||||
|
||||
|
||||
#### Zoom and crop
|
||||
|
||||
These operate in **layer space** before the final overlay.
|
||||
|
||||
- `zoom` (float, optional; default `1.0`)
|
||||
|
||||
- `1.0` – no zoom (layer fits exactly inside the computed `box_w x box_h`)
|
||||
- `>1.0` – zoom in; layer is scaled up to `zoom * box` then center‑cropped
|
||||
back down to the box. This gives the effect of “zooming inside” a fixed
|
||||
PIP area.
|
||||
|
||||
- `crop_x`, `crop_y`, `crop_w`, `crop_h` (float, optional)
|
||||
|
||||
**Percentages** of the original source width/height, all in `[0,100]`.
|
||||
|
||||
If all four are present, lagkage applies:
|
||||
|
||||
```text
|
||||
crop=in_w*crop_w/100 : in_h*crop_h/100 : in_w*crop_x/100 : in_h*crop_y/100
|
||||
```
|
||||
|
||||
This happens *before* zoom/scale. Use this to punch out a region from a
|
||||
larger frame (for example, turning a full‑frame video into a “face cam” PIP).
|
||||
|
||||
|
||||
#### Per‑layer audio gain
|
||||
|
||||
These fields only matter if:
|
||||
|
||||
- the layer’s media actually has audio (e.g. `type: "video"` with an audio stream),
|
||||
- and `--audio-mode` is `all` or `json_only` so layer audio is in the mix.
|
||||
|
||||
- `audio_gain_db` (float, optional)
|
||||
|
||||
Gain in decibels applied to the layer’s audio *before* mixing:
|
||||
|
||||
- `0` – keep original level
|
||||
- `-6` – roughly “half as loud” perceptually
|
||||
- `+3` – a bit louder
|
||||
- `+12` – very loud / likely to clip unless sources are quiet
|
||||
|
||||
- `audio_gain` (float, optional)
|
||||
|
||||
Linear multiplier, converted internally to dB as:
|
||||
|
||||
```text
|
||||
audio_gain_db = 20 * log10(audio_gain)
|
||||
```
|
||||
|
||||
Only used if `audio_gain_db` is not set.
|
||||
For example, `audio_gain: 0.5` ≈ `-6.02 dB`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Audio behavior in detail
|
||||
|
||||
### 3.1 Modes
|
||||
|
||||
- `--audio-mode base` (default)
|
||||
|
||||
- Map `0:a?` (base audio only).
|
||||
- JSON layers are *visual only*; per‑layer audio gains are ignored.
|
||||
|
||||
- `--audio-mode all`
|
||||
|
||||
- Take audio from:
|
||||
- base input (index 0) if it has audio
|
||||
- all JSON layers whose preprocessed media has audio
|
||||
- Apply **per‑layer gains** (where specified) on JSON layers.
|
||||
- Use `amix` to combine all sources into a single output stream.
|
||||
|
||||
- `--audio-mode json_only`
|
||||
|
||||
- Ignore base audio completely.
|
||||
- Mix only JSON layer audio (with per‑layer gains) via `amix`.
|
||||
|
||||
- `--audio-mode external`
|
||||
|
||||
- Append `--audio-src PATH` as an extra ffmpeg input.
|
||||
- Map only that input’s audio (`N:a?`) to the output.
|
||||
- JSON layer audio and base audio are ignored.
|
||||
|
||||
- `--audio-mode none`
|
||||
|
||||
- Add `-an` to ffmpeg.
|
||||
- Output video is silent.
|
||||
|
||||
|
||||
### 3.2 When do per‑layer gains matter?
|
||||
|
||||
Per‑layer audio gains (`audio_gain_db` / `audio_gain`) are applied:
|
||||
|
||||
- only on JSON layers that actually have an audio stream, and
|
||||
- only when `--audio-mode` is `all` or `json_only`.
|
||||
|
||||
They do **not** affect:
|
||||
|
||||
- the base input audio (there’s currently no `base_audio_gain_db`),
|
||||
- the external audio when `--audio-mode=external`,
|
||||
- GIF overlays (their preprocessed videos are silent),
|
||||
- image layers (no audio).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. GIF handling
|
||||
|
||||
GIFs are pre‑processed before the main pass so the compositor can treat them
|
||||
like normal video streams with alpha.
|
||||
|
||||
For each `type: "gif"` layer:
|
||||
|
||||
1. lagkage runs a helper ffmpeg command that:
|
||||
- loops the GIF (`-stream_loop -1`, `-ignore_loop 0`)
|
||||
- clamps to the base duration using `-t base_duration`
|
||||
- forces even dimensions with `scale=trunc(iw/2)*2:trunc(ih/2)*2`
|
||||
- converts to RGBA pixels (`format=rgba`)
|
||||
- writes a `.mov` with `qtrle` codec and **alpha preserved**
|
||||
2. The resulting `.mov` is used as the actual overlay source.
|
||||
|
||||
This is why animated GIFs loop smoothly across the entire base clip and retain
|
||||
their transparency when overlaid.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Path resolution rules
|
||||
|
||||
Given a `layout_exNN.json` at:
|
||||
|
||||
```text
|
||||
lagkage_layouts/layout_exNN_*.json
|
||||
```
|
||||
|
||||
and a layer `filename` value, lagkage resolves paths as follows:
|
||||
|
||||
1. If `filename` contains `"://"` → treat as URL and pass directly to ffmpeg.
|
||||
2. If `filename` is an absolute path (e.g. `/Users/you/file.mp4`) → use as‑is.
|
||||
3. If `filename` starts with `"../media/"` → normalize to `media/...` relative
|
||||
to your project root.
|
||||
4. If `filename` starts with `"media/"` → treat as project‑root relative.
|
||||
5. Otherwise → resolve relative to the **layout JSON’s directory**.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. Example layouts & commands
|
||||
|
||||
This repo includes a set of example layouts and matching commands that showcase:
|
||||
|
||||
- place vs free mode
|
||||
- cropping and zoom
|
||||
- animated GIF overlays
|
||||
- multiple layers with different sizes & opacities
|
||||
- different `sequence_direction` values
|
||||
- all audio modes, including per‑layer gain
|
||||
|
||||
See:
|
||||
|
||||
- `lagkage_layouts/` – 20 example layout JSON files (layout_ex01_*.json … layout_ex20_*.json)
|
||||
- `lagkage_examples_full.txt` – example commands, one per layout
|
||||
|
||||
You can copy one example, tweak the filenames to match your media, and then
|
||||
iterate from there.
|
||||
|
||||
---
|
||||
|
||||
## 7. Minimal “hello lagkage” example
|
||||
|
||||
`lagkage_layouts/layout_ex01_logo_corner.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_corner",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 18,
|
||||
"opacity": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
videobeaux -P lagkage -i media/base.mp4 -o out/ex01_logo_corner.mp4 --layout-json lagkage_layouts/layout_ex01_logo_corner.json --audio-mode base --force
|
||||
```
|
||||
|
||||
This is the simplest setup:
|
||||
|
||||
- Base video at `media/base.mp4`
|
||||
- One PNG logo (`reem.png`) in the top‑right corner
|
||||
- Base audio only
|
||||
- No zoom/crop or audio mixing yet
|
||||
|
||||
From here, add more layers, switch to `audio-mode=all`, and start using
|
||||
`audio_gain_db` to explore full lagkage functionality.
|
||||
16
lagkage_layouts/layout_01.json
Normal file
16
lagkage_layouts/layout_01.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_02.json
Normal file
16
lagkage_layouts/layout_02.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_03.json
Normal file
16
lagkage_layouts/layout_03.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_04.json
Normal file
16
lagkage_layouts/layout_04.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_05.json
Normal file
16
lagkage_layouts/layout_05.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_06.json
Normal file
16
lagkage_layouts/layout_06.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_07.json
Normal file
16
lagkage_layouts/layout_07.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_08.json
Normal file
16
lagkage_layouts/layout_08.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_09.json
Normal file
16
lagkage_layouts/layout_09.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_10.json
Normal file
16
lagkage_layouts/layout_10.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_11.json
Normal file
16
lagkage_layouts/layout_11.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_12.json
Normal file
16
lagkage_layouts/layout_12.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_13.json
Normal file
16
lagkage_layouts/layout_13.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_14.json
Normal file
16
lagkage_layouts/layout_14.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_15.json
Normal file
16
lagkage_layouts/layout_15.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_16.json
Normal file
16
lagkage_layouts/layout_16.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_17.json
Normal file
16
lagkage_layouts/layout_17.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_18.json
Normal file
16
lagkage_layouts/layout_18.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_19.json
Normal file
16
lagkage_layouts/layout_19.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
16
lagkage_layouts/layout_20.json
Normal file
16
lagkage_layouts/layout_20.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "example",
|
||||
"filename": "../media/sample.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 200,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
28
lagkage_layouts/layout_audio_mix_example.json
Normal file
28
lagkage_layouts/layout_audio_mix_example.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [{
|
||||
"layer_number": 1,
|
||||
"name": "loud_clip",
|
||||
"filename": "../media/base.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 200,
|
||||
"pos_y": 250,
|
||||
"size": 30,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -1
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "quiet_clip",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 150,
|
||||
"size": 30,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
15
lagkage_layouts/layout_ex01_logo_corner.json
Normal file
15
lagkage_layouts/layout_ex01_logo_corner.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_corner",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 18,
|
||||
"opacity": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
15
lagkage_layouts/layout_ex02_gif_center_sticker.json
Normal file
15
lagkage_layouts/layout_ex02_gif_center_sticker.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "gif_center",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "place",
|
||||
"place": "center",
|
||||
"size": 30,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
30
lagkage_layouts/layout_ex03_dual_pip_videos.json
Normal file
30
lagkage_layouts/layout_ex03_dual_pip_videos.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "pip_left",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 80,
|
||||
"pos_y": 200,
|
||||
"size": 28,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -6
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "pip_right_zoom",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1120,
|
||||
"pos_y": 200,
|
||||
"size": 28,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.5,
|
||||
"audio_gain_db": -12
|
||||
}
|
||||
]
|
||||
}
|
||||
20
lagkage_layouts/layout_ex04_lower_third_band.json
Normal file
20
lagkage_layouts/layout_ex04_lower_third_band.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "lower_third_band",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 160,
|
||||
"pos_y": 780,
|
||||
"size": 65,
|
||||
"opacity": 0.95,
|
||||
"crop_x": 0,
|
||||
"crop_y": 40,
|
||||
"crop_w": 100,
|
||||
"crop_h": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
21
lagkage_layouts/layout_ex05_center_zoom_crop_pip.json
Normal file
21
lagkage_layouts/layout_ex05_center_zoom_crop_pip.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "zoom_crop_pip",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "place",
|
||||
"place": "center",
|
||||
"size": 35,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.8,
|
||||
"crop_x": 25,
|
||||
"crop_y": 25,
|
||||
"crop_w": 50,
|
||||
"crop_h": 50,
|
||||
"audio_gain_db": -6
|
||||
}
|
||||
]
|
||||
}
|
||||
49
lagkage_layouts/layout_ex06_scatter_gifs_logo.json
Normal file
49
lagkage_layouts/layout_ex06_scatter_gifs_logo.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"sequence_direction": "random",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_small",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 60,
|
||||
"pos_y": 80,
|
||||
"size": 10,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "gif_top_left",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": -50,
|
||||
"pos_y": -20,
|
||||
"size": 20,
|
||||
"opacity": 1.0
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "gif_bottom_right",
|
||||
"filename": "../media/bunny.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 1180,
|
||||
"pos_y": 720,
|
||||
"size": 18,
|
||||
"opacity": 1.0
|
||||
},
|
||||
{
|
||||
"layer_number": 4,
|
||||
"name": "gif_left_mid",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": -120,
|
||||
"pos_y": 400,
|
||||
"size": 22,
|
||||
"opacity": 0.8
|
||||
}
|
||||
]
|
||||
}
|
||||
45
lagkage_layouts/layout_ex07_logo_grid_edges.json
Normal file
45
lagkage_layouts/layout_ex07_logo_grid_edges.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "tl_logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_left",
|
||||
"size": 10,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "tr_logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 10,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "bl_logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "bottom_left",
|
||||
"size": 10,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 4,
|
||||
"name": "br_logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "bottom_right",
|
||||
"size": 10,
|
||||
"opacity": 0.9
|
||||
}
|
||||
]
|
||||
}
|
||||
43
lagkage_layouts/layout_ex08_story_mode_external_audio.json
Normal file
43
lagkage_layouts/layout_ex08_story_mode_external_audio.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "lower_band",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 120,
|
||||
"pos_y": 780,
|
||||
"size": 70,
|
||||
"opacity": 0.9,
|
||||
"crop_x": 0,
|
||||
"crop_y": 50,
|
||||
"crop_w": 100,
|
||||
"crop_h": 50
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "pip_story",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 80,
|
||||
"pos_y": 160,
|
||||
"size": 30,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.2,
|
||||
"audio_gain_db": -9
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "logo_corner",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_left",
|
||||
"size": 14,
|
||||
"opacity": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
34
lagkage_layouts/layout_ex09_split_screen.json
Normal file
34
lagkage_layouts/layout_ex09_split_screen.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "left_zoomed",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 0,
|
||||
"pos_y": 0,
|
||||
"size": 50,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.6,
|
||||
"crop_x": 10,
|
||||
"crop_y": 10,
|
||||
"crop_w": 80,
|
||||
"crop_h": 80,
|
||||
"audio_gain_db": -8
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "right_normal",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 960,
|
||||
"pos_y": 0,
|
||||
"size": 50,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -8
|
||||
}
|
||||
]
|
||||
}
|
||||
62
lagkage_layouts/layout_ex10_full_showcase.json
Normal file
62
lagkage_layouts/layout_ex10_full_showcase.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_corner",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 14,
|
||||
"opacity": 0.95
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "pip_left",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 80,
|
||||
"pos_y": 220,
|
||||
"size": 26,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -10
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "pip_right_zoom",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1180,
|
||||
"pos_y": 220,
|
||||
"size": 26,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.4,
|
||||
"audio_gain_db": -12
|
||||
},
|
||||
{
|
||||
"layer_number": 4,
|
||||
"name": "gif_circus",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 420,
|
||||
"pos_y": 260,
|
||||
"size": 18,
|
||||
"opacity": 1.0
|
||||
},
|
||||
{
|
||||
"layer_number": 5,
|
||||
"name": "gif_bunny",
|
||||
"filename": "../media/bunny.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 1320,
|
||||
"pos_y": 260,
|
||||
"size": 16,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layers": [{
|
||||
"layer_number": 1,
|
||||
"name": "corner_logo",
|
||||
"filename": "../media/reem.png",
|
||||
@@ -28,7 +27,7 @@
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1180,
|
||||
"pos_x": 180,
|
||||
"pos_y": 220,
|
||||
"size": 24,
|
||||
"opacity": 1.0,
|
||||
|
||||
26
lagkage_layouts/layout_ex11_silent_graphics_only.json
Normal file
26
lagkage_layouts/layout_ex11_silent_graphics_only.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_center",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "center",
|
||||
"size": 25,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "gif_bottom",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 400,
|
||||
"pos_y": 700,
|
||||
"size": 22,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
29
lagkage_layouts/layout_ex12_json_only_audio_gains.json
Normal file
29
lagkage_layouts/layout_ex12_json_only_audio_gains.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "clip_soft",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 120,
|
||||
"pos_y": 200,
|
||||
"size": 30,
|
||||
"opacity": 1.0,
|
||||
"audio_gain": 0.5
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "clip_loud",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 980,
|
||||
"pos_y": 200,
|
||||
"size": 30,
|
||||
"opacity": 1.0,
|
||||
"audio_gain": 2.0
|
||||
}
|
||||
]
|
||||
}
|
||||
38
lagkage_layouts/layout_ex13_backward_stack.json
Normal file
38
lagkage_layouts/layout_ex13_backward_stack.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"sequence_direction": "backward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo1",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 100,
|
||||
"pos_y": 100,
|
||||
"size": 12,
|
||||
"opacity": 0.7
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "logo2",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 140,
|
||||
"pos_y": 140,
|
||||
"size": 14,
|
||||
"opacity": 0.7
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "logo3",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 180,
|
||||
"pos_y": 180,
|
||||
"size": 16,
|
||||
"opacity": 0.7
|
||||
}
|
||||
]
|
||||
}
|
||||
45
lagkage_layouts/layout_ex14_place_quadrants.json
Normal file
45
lagkage_layouts/layout_ex14_place_quadrants.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "tl_small",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_left",
|
||||
"size": 8,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "tr_medium",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_right",
|
||||
"size": 12,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "bl_big",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "bottom_left",
|
||||
"size": 20,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 4,
|
||||
"name": "br_huge",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "bottom_right",
|
||||
"size": 26,
|
||||
"opacity": 0.9
|
||||
}
|
||||
]
|
||||
}
|
||||
37
lagkage_layouts/layout_ex15_ui_stickers.json
Normal file
37
lagkage_layouts/layout_ex15_ui_stickers.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "center_logo",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "center",
|
||||
"size": 18,
|
||||
"opacity": 1.0
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "gif_left",
|
||||
"filename": "../media/bunny.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 300,
|
||||
"pos_y": 400,
|
||||
"size": 12,
|
||||
"opacity": 1.0
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "gif_right",
|
||||
"filename": "../media/circus.gif",
|
||||
"type": "gif",
|
||||
"mode": "free",
|
||||
"pos_x": 1300,
|
||||
"pos_y": 400,
|
||||
"size": 12,
|
||||
"opacity": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
44
lagkage_layouts/layout_ex16_zoom_showcase.json
Normal file
44
lagkage_layouts/layout_ex16_zoom_showcase.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "zoom1",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 200,
|
||||
"pos_y": 150,
|
||||
"size": 24,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.0,
|
||||
"audio_gain_db": -6
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "zoom2",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 600,
|
||||
"pos_y": 150,
|
||||
"size": 24,
|
||||
"opacity": 1.0,
|
||||
"zoom": 1.5,
|
||||
"audio_gain_db": -12
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "zoom3",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1000,
|
||||
"pos_y": 150,
|
||||
"size": 24,
|
||||
"opacity": 1.0,
|
||||
"zoom": 2.0,
|
||||
"audio_gain_db": -18
|
||||
}
|
||||
]
|
||||
}
|
||||
22
lagkage_layouts/layout_ex17_crop_focus.json
Normal file
22
lagkage_layouts/layout_ex17_crop_focus.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "crop_focus",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1400,
|
||||
"pos_y": 500,
|
||||
"size": 18,
|
||||
"opacity": 1.0,
|
||||
"crop_x": 35,
|
||||
"crop_y": 20,
|
||||
"crop_w": 30,
|
||||
"crop_h": 40,
|
||||
"zoom": 1.2,
|
||||
"audio_gain_db": -8
|
||||
}
|
||||
]
|
||||
}
|
||||
27
lagkage_layouts/layout_ex18_offscreen_push.json
Normal file
27
lagkage_layouts/layout_ex18_offscreen_push.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "left_half_off",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": -200,
|
||||
"pos_y": 300,
|
||||
"size": 40,
|
||||
"opacity": 0.9
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "right_half_off",
|
||||
"filename": "../media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "free",
|
||||
"pos_x": 1600,
|
||||
"pos_y": 300,
|
||||
"size": 40,
|
||||
"opacity": 0.9
|
||||
}
|
||||
]
|
||||
}
|
||||
15
lagkage_layouts/layout_ex19_media_path_root.json
Normal file
15
lagkage_layouts/layout_ex19_media_path_root.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "logo_media_root",
|
||||
"filename": "media/reem.png",
|
||||
"type": "img",
|
||||
"mode": "place",
|
||||
"place": "top_left",
|
||||
"size": 18,
|
||||
"opacity": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
41
lagkage_layouts/layout_ex20_audio_premix.json
Normal file
41
lagkage_layouts/layout_ex20_audio_premix.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"sequence_direction": "forward",
|
||||
"layers": [
|
||||
{
|
||||
"layer_number": 1,
|
||||
"name": "clip1_bg",
|
||||
"filename": "../media/schwwaaa.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 80,
|
||||
"pos_y": 180,
|
||||
"size": 22,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -12
|
||||
},
|
||||
{
|
||||
"layer_number": 2,
|
||||
"name": "clip2_mid",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 600,
|
||||
"pos_y": 220,
|
||||
"size": 22,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -6
|
||||
},
|
||||
{
|
||||
"layer_number": 3,
|
||||
"name": "clip3_fg",
|
||||
"filename": "../media/faith.mp4",
|
||||
"type": "video",
|
||||
"mode": "free",
|
||||
"pos_x": 1120,
|
||||
"pos_y": 260,
|
||||
"size": 22,
|
||||
"opacity": 1.0,
|
||||
"audio_gain_db": -3
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
BIN
media/gross.png
Normal file
BIN
media/gross.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 800 KiB |
BIN
media/music_track.wav
Normal file
BIN
media/music_track.wav
Normal file
Binary file not shown.
BIN
media/reem.png
BIN
media/reem.png
Binary file not shown.
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 323 KiB |
BIN
out/avs-2025-bulgaria-sub4.wav
Normal file
BIN
out/avs-2025-bulgaria-sub4.wav
Normal file
Binary file not shown.
@@ -18,7 +18,7 @@ srt==3.5.3
|
||||
tqdm==4.67.1
|
||||
typing_extensions==4.14.1
|
||||
urllib3==2.5.0
|
||||
-e git+ssh://git@github.com/vondas-network/videobeaux.git@09684ac5b5173f312d59b780e03c3ad5f1bf25e9#egg=videobeaux
|
||||
videobeaux @ file:///Users/tgm/Documents/SPLASH/videobeaux
|
||||
videogrep==2.3.0
|
||||
vosk==0.3.44
|
||||
websockets==15.0.1
|
||||
|
||||
306
videobeaux/programs/lagkage-old.py
Normal file
306
videobeaux/programs/lagkage-old.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# videobeaux/programs/lagkage.py
|
||||
#
|
||||
# Compose multiple visual layers (images/gifs/videos) on top of a base video
|
||||
# using a single JSON layout file.
|
||||
#
|
||||
# GIF layers are preprocessed into finite videos that loop for roughly the base
|
||||
# video duration, so the main overlay graph stays simple and stable.
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||||
|
||||
|
||||
def register_arguments(parser):
|
||||
parser.description = (
|
||||
"Compose a base video with layered media defined by a JSON layout "
|
||||
"(images / videos / GIFs) using the lagkage compositor."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--layout-json",
|
||||
dest="layout_json",
|
||||
required=True,
|
||||
type=str,
|
||||
help="Path to layout JSON describing layers.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sequence-direction",
|
||||
dest="sequence_direction",
|
||||
choices=["forward", "backward", "random"],
|
||||
default=None,
|
||||
help="Override sequence_direction from JSON (forward|backward|random).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-mode",
|
||||
dest="audio_mode",
|
||||
choices=["base", "all", "json_only", "external", "none"],
|
||||
default="base",
|
||||
help=(
|
||||
"How to build the audio track: "
|
||||
"base (default base video audio only), "
|
||||
"all (base + all JSON media audio mixed), "
|
||||
"json_only (only JSON media audio), "
|
||||
"external (use --audio-src), "
|
||||
"none (no audio)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-src",
|
||||
dest="audio_src",
|
||||
type=str,
|
||||
help="External audio file when --audio-mode=external.",
|
||||
)
|
||||
|
||||
|
||||
# ----------------- ffprobe helpers -----------------
|
||||
|
||||
|
||||
def _probe_base_info(path: str):
|
||||
"""
|
||||
Return (width, height, duration_seconds) for the base video.
|
||||
"""
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"json",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"ffprobe failed for base {path}: {proc.stderr}")
|
||||
info = json.loads(proc.stdout)
|
||||
|
||||
width = int(info["streams"][0]["width"])
|
||||
height = int(info["streams"][0]["height"])
|
||||
duration = float(info["format"]["duration"])
|
||||
return width, height, duration
|
||||
|
||||
|
||||
def _probe_video_size(path: str):
|
||||
"""
|
||||
Return (width, height) of the first video/image stream.
|
||||
"""
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=s=x:p=0",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"ffprobe size failed for {path}: {proc.stderr}")
|
||||
line = proc.stdout.strip()
|
||||
if not line:
|
||||
raise RuntimeError(f"ffprobe size returned empty output for {path}")
|
||||
w_str, h_str = line.split("x")
|
||||
return int(w_str), int(h_str)
|
||||
|
||||
|
||||
def _probe_has_audio(path: str) -> bool:
|
||||
"""
|
||||
Return True if the file has at least one audio stream.
|
||||
"""
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=index",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return proc.returncode == 0 and proc.stdout.strip() != ""
|
||||
|
||||
|
||||
# ----------------- small helpers -----------------
|
||||
|
||||
|
||||
def _even(x: int) -> int:
|
||||
"""
|
||||
Force an integer to be even (needed for some codecs / filters).
|
||||
"""
|
||||
return x if x % 2 == 0 else x + 1
|
||||
|
||||
|
||||
def _preprocess_gif(src: str, base_duration: float, tmp_dir: Path, idx: int) -> str:
|
||||
"""
|
||||
Convert GIF to a looping RGBA video with even dimensions and alpha.
|
||||
Returns path to the temp video file.
|
||||
|
||||
- Loops the GIF for approximately base_duration.
|
||||
- Forces even width/height.
|
||||
- Keeps alpha via format=rgba + qtrle.
|
||||
"""
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = tmp_dir / f"gif_pre_{idx:03d}.mov"
|
||||
|
||||
vf = "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=rgba"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-ignore_loop",
|
||||
"0",
|
||||
"-stream_loop",
|
||||
"-1",
|
||||
"-i",
|
||||
src,
|
||||
"-t",
|
||||
f"{base_duration:.3f}",
|
||||
"-vf",
|
||||
vf,
|
||||
"-c:v",
|
||||
"qtrle",
|
||||
"-an",
|
||||
str(out_path),
|
||||
]
|
||||
proc = subprocess.run(cmd)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"GIF preprocess failed for {src} (code {proc.returncode})")
|
||||
|
||||
return str(out_path)
|
||||
|
||||
|
||||
def _compute_place_coordinates(place: str, base_w: int, base_h: int,
|
||||
box_w: int, box_h: int):
|
||||
"""
|
||||
Compute overlay x,y for place mode, given base and box dimensions.
|
||||
"""
|
||||
place = (place or "center").lower()
|
||||
if place == "top_left":
|
||||
return 0, 0
|
||||
elif place == "top_right":
|
||||
return base_w - box_w, 0
|
||||
elif place == "bottom_left":
|
||||
return 0, base_h - box_h
|
||||
elif place == "bottom_right":
|
||||
return base_w - box_w, base_h - box_h
|
||||
elif place == "center":
|
||||
return (base_w - box_w) // 2, (base_h - box_h) // 2
|
||||
else:
|
||||
# Fallback to center for unknown place values
|
||||
return (base_w - box_w) // 2, (base_h - box_h) // 2
|
||||
|
||||
|
||||
def _compute_overlay_box(base_w: int, src_w: int, src_h: int, size_pct: float):
|
||||
"""
|
||||
Compute the final overlay box width/height (even integers) based on:
|
||||
- base width
|
||||
- layer source aspect ratio
|
||||
- size percentage of base width.
|
||||
"""
|
||||
if size_pct <= 0:
|
||||
size_pct = 1.0
|
||||
|
||||
target_w = int(base_w * (size_pct / 100.0))
|
||||
if target_w < 2:
|
||||
target_w = 2
|
||||
target_w = _even(target_w)
|
||||
|
||||
if src_h > 0:
|
||||
ar = src_w / src_h
|
||||
target_h = int(target_w / ar) if ar > 0 else target_w
|
||||
else:
|
||||
target_h = target_w
|
||||
|
||||
if target_h < 2:
|
||||
target_h = 2
|
||||
target_h = _even(target_h)
|
||||
|
||||
return target_w, target_h
|
||||
|
||||
|
||||
def _resolve_layer_path(filename: str, layout_path: Path) -> str:
|
||||
"""
|
||||
Resolve the media path for a layer in a way that:
|
||||
- Leaves absolute paths and URLs alone.
|
||||
- Treats '../media/...' as project-root 'media/...'.
|
||||
- Treats 'media/...' as-is (relative to current working dir).
|
||||
- For simple basenames, keeps layout-relative behavior.
|
||||
"""
|
||||
# URLs
|
||||
if "://" in filename:
|
||||
return filename
|
||||
|
||||
# Absolute path
|
||||
if os.path.isabs(filename):
|
||||
return filename
|
||||
|
||||
# Special-case ../media/... -> media/...
|
||||
if filename.startswith("../media/"):
|
||||
return os.path.normpath("media/" + filename[len("../media/"):])
|
||||
|
||||
# Special-case media/... -> as-is (cwd-relative)
|
||||
if filename.startswith("media/"):
|
||||
return filename
|
||||
|
||||
# Everything else: layout-relative
|
||||
return str((layout_path.parent / filename).resolve())
|
||||
|
||||
|
||||
# ----------------- main entry -----------------
|
||||
|
||||
|
||||
def run(args):
|
||||
layout_path = Path(args.layout_json)
|
||||
if not layout_path.exists():
|
||||
raise FileNotFoundError(f"Layout JSON not found: {layout_path}")
|
||||
|
||||
audio_mode = getattr(args, "audio_mode", "base") or "base"
|
||||
audio_src = getattr(args, "audio_src", None)
|
||||
|
||||
if audio_mode == "external" and not audio_src:
|
||||
raise ValueError("--audio-mode=external requires --audio-src")
|
||||
|
||||
# Load layout JSON
|
||||
layout = json.loads(layout_path.read_text())
|
||||
layers = layout.get("layers", [])
|
||||
if not isinstance(layers, list) or not layers:
|
||||
raise ValueError("Layout JSON has no 'layers' array or it's empty.")
|
||||
|
||||
# Determine sequence direction
|
||||
seq_dir = args.sequence_direction or layout.get("sequence_direction", "forward")
|
||||
if seq_dir not in ("forward", "backward", "random"):
|
||||
seq_dir = "forward"
|
||||
|
||||
# Order layers by layer_number, then adjust sequence if needed
|
||||
ordered_layers = sorted(layers, key=lambda L: L.get("layer_number", 0))
|
||||
if seq_dir == "backward":
|
||||
ordered_layers = list(reversed(ordered_layers))
|
||||
elif seq_dir == "random":
|
||||
ordered_layers = random.sample(ordered_layers, len(ordered_layers))
|
||||
|
||||
base_input = args.input
|
||||
if not base_input:
|
||||
raise ValueError("Global --input (base video) is required for lagkage.")
|
||||
|
||||
# Probe base
|
||||
base_w, base_h, base_duration
|
||||
|
||||
::contentReference[oaicite:0]{index=0}
|
||||
@@ -3,14 +3,15 @@
|
||||
# Compose multiple visual layers (images/gifs/videos) on top of a base video
|
||||
# using a single JSON layout file.
|
||||
#
|
||||
# GIF layers are preprocessed into finite videos that loop for roughly the base
|
||||
# video duration, so the main overlay graph stays simple and stable.
|
||||
# GIF layers are preprocessed into finite-length videos that loop for roughly
|
||||
# the base video duration, so the main overlay graph stays simple and stable.
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||||
@@ -35,11 +36,40 @@ def register_arguments(parser):
|
||||
default=None,
|
||||
help="Override sequence_direction from JSON (forward|backward|random).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-mode",
|
||||
dest="audio_mode",
|
||||
choices=["base", "all", "json_only", "external", "none"],
|
||||
default="base",
|
||||
help=(
|
||||
"How to build the audio track: "
|
||||
"base (default base video audio only), "
|
||||
"all (base + all JSON media audio mixed), "
|
||||
"json_only (only JSON media audio), "
|
||||
"external (use --audio-src), "
|
||||
"none (no audio)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-src",
|
||||
dest="audio_src",
|
||||
type=str,
|
||||
help="External audio file when --audio-mode=external.",
|
||||
)
|
||||
|
||||
|
||||
# ----------------- ffprobe helpers -----------------
|
||||
|
||||
|
||||
def _run_ffprobe(cmd):
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"ffprobe failed (code {proc.returncode}): {proc.stderr.strip()}"
|
||||
)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _probe_base_info(path: str):
|
||||
"""
|
||||
Return (width, height, duration_seconds) for the base video.
|
||||
@@ -58,14 +88,17 @@ def _probe_base_info(path: str):
|
||||
"json",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"ffprobe failed for base {path}: {proc.stderr}")
|
||||
info = json.loads(proc.stdout)
|
||||
out = _run_ffprobe(cmd)
|
||||
data = json.loads(out)
|
||||
|
||||
width = int(info["streams"][0]["width"])
|
||||
height = int(info["streams"][0]["height"])
|
||||
duration = float(info["format"]["duration"])
|
||||
streams = data.get("streams") or []
|
||||
if not streams:
|
||||
raise RuntimeError(f"No video stream found in {path!r}")
|
||||
|
||||
s0 = streams[0]
|
||||
width = int(s0.get("width") or 0)
|
||||
height = int(s0.get("height") or 0)
|
||||
duration = float(data["format"].get("duration") or 0.0)
|
||||
return width, height, duration
|
||||
|
||||
|
||||
@@ -85,16 +118,34 @@ def _probe_video_size(path: str):
|
||||
"csv=s=x:p=0",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"ffprobe size failed for {path}: {proc.stderr}")
|
||||
line = proc.stdout.strip()
|
||||
out = _run_ffprobe(cmd)
|
||||
line = out.strip()
|
||||
if not line:
|
||||
raise RuntimeError(f"ffprobe size returned empty output for {path}")
|
||||
w_str, h_str = line.split("x")
|
||||
return int(w_str), int(h_str)
|
||||
|
||||
|
||||
def _probe_has_audio(path: str) -> bool:
|
||||
"""
|
||||
Return True if the file has at least one audio stream.
|
||||
"""
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=index",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
path,
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return proc.returncode == 0 and proc.stdout.strip() != ""
|
||||
|
||||
|
||||
# ----------------- small helpers -----------------
|
||||
|
||||
|
||||
@@ -196,6 +247,54 @@ def _compute_overlay_box(base_w: int, src_w: int, src_h: int, size_pct: float):
|
||||
return target_w, target_h
|
||||
|
||||
|
||||
def _resolve_layer_path(filename: str, layout_path: Path) -> str:
|
||||
"""
|
||||
Resolve the media path for a layer in a way that:
|
||||
- Leaves absolute paths and URLs alone.
|
||||
- Treats '../media/...' as project-root 'media/...'.
|
||||
- Treats 'media/...' as-is (relative to current working dir).
|
||||
- For simple basenames, keeps layout-relative behavior.
|
||||
"""
|
||||
# URLs
|
||||
if "://" in filename:
|
||||
return filename
|
||||
|
||||
# Absolute path
|
||||
if os.path.isabs(filename):
|
||||
return filename
|
||||
|
||||
# Special-case ../media/... -> media/...
|
||||
if filename.startswith("../media/"):
|
||||
return os.path.normpath("media/" + filename[len("../media/"):])
|
||||
|
||||
# Special-case media/... -> as-is (cwd-relative)
|
||||
if filename.startswith("media/"):
|
||||
return filename
|
||||
|
||||
# Everything else: layout-relative
|
||||
return str((layout_path.parent / filename).resolve())
|
||||
|
||||
|
||||
def _load_layout(layout_path: Path):
|
||||
data = json.loads(layout_path.read_text())
|
||||
if "layers" not in data or not data["layers"]:
|
||||
raise ValueError("Layout JSON has no 'layers' array or it's empty.")
|
||||
return data
|
||||
|
||||
|
||||
def _resolve_sequence(layout: dict, cli_seq: str):
|
||||
seq = cli_seq or layout.get("sequence_direction") or "forward"
|
||||
if seq not in ("forward", "backward", "random"):
|
||||
seq = "forward"
|
||||
|
||||
layers = sorted(layout["layers"], key=lambda L: L.get("layer_number", 0))
|
||||
if seq == "backward":
|
||||
layers = list(reversed(layers))
|
||||
elif seq == "random":
|
||||
layers = random.sample(layers, len(layers))
|
||||
return seq, layers
|
||||
|
||||
|
||||
# ----------------- main entry -----------------
|
||||
|
||||
|
||||
@@ -204,53 +303,39 @@ def run(args):
|
||||
if not layout_path.exists():
|
||||
raise FileNotFoundError(f"Layout JSON not found: {layout_path}")
|
||||
|
||||
# Load layout JSON
|
||||
layout = json.loads(layout_path.read_text())
|
||||
layers = layout.get("layers", [])
|
||||
if not isinstance(layers, list) or not layers:
|
||||
raise ValueError("Layout JSON has no 'layers' array or it's empty.")
|
||||
audio_mode = getattr(args, "audio_mode", "base") or "base"
|
||||
audio_src = getattr(args, "audio_src", None)
|
||||
|
||||
# Determine sequence direction
|
||||
seq_dir = args.sequence_direction or layout.get("sequence_direction", "forward")
|
||||
if seq_dir not in ("forward", "backward", "random"):
|
||||
seq_dir = "forward"
|
||||
if audio_mode == "external" and not audio_src:
|
||||
raise ValueError("--audio-mode=external requires --audio-src")
|
||||
|
||||
# Order layers by layer_number, then adjust sequence if needed
|
||||
ordered_layers = sorted(layers, key=lambda L: L.get("layer_number", 0))
|
||||
if seq_dir == "backward":
|
||||
ordered_layers = list(reversed(ordered_layers))
|
||||
elif seq_dir == "random":
|
||||
ordered_layers = random.sample(ordered_layers, len(ordered_layers))
|
||||
layout = _load_layout(layout_path)
|
||||
seq, ordered_layers = _resolve_sequence(layout, args.sequence_direction)
|
||||
|
||||
base_input = args.input
|
||||
if not base_input:
|
||||
raise ValueError("Global --input (base video) is required for lagkage.")
|
||||
|
||||
# Probe base
|
||||
# 1) Probe base video info once
|
||||
base_w, base_h, base_duration = _probe_base_info(base_input)
|
||||
base_has_audio = _probe_has_audio(base_input)
|
||||
|
||||
# Temp dir for GIF preprocess
|
||||
# 2) Prepare inputs for main ffmpeg call
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="lagkage_gifs_"))
|
||||
|
||||
# 1) Build input list for ffmpeg
|
||||
# input 0 = base, 1..N = overlays
|
||||
input_files = [base_input]
|
||||
input_files = [base_input] # index 0 = base
|
||||
overlay_specs = [] # (input_index, layer_dict, src_w, src_h)
|
||||
overlay_audio_indices = [] # which overlay inputs actually have audio
|
||||
audio_vol_db_by_index = {} # per-input gain in dB
|
||||
|
||||
for idx, layer in enumerate(ordered_layers, start=1):
|
||||
filename = layer.get("filename")
|
||||
if not filename:
|
||||
raise ValueError(f"Layer missing 'filename': {layer}")
|
||||
|
||||
# Relative paths are relative to the layout JSON directory.
|
||||
if not os.path.isabs(filename):
|
||||
src = str(layout_path.parent / filename)
|
||||
else:
|
||||
src = filename
|
||||
|
||||
src = _resolve_layer_path(filename, layout_path)
|
||||
layer_type = (layer.get("type") or "").lower()
|
||||
|
||||
# If GIF, pre-process to finite-length video that loops to base duration
|
||||
if layer_type == "gif":
|
||||
overlay_src = _preprocess_gif(src, base_duration, tmp_dir, idx)
|
||||
else:
|
||||
@@ -262,10 +347,51 @@ def run(args):
|
||||
in_index = len(input_files) - 1
|
||||
overlay_specs.append((in_index, layer, src_w, src_h))
|
||||
|
||||
# 2) Build filter_complex
|
||||
if _probe_has_audio(overlay_src):
|
||||
overlay_audio_indices.append(in_index)
|
||||
|
||||
# Per-layer audio gain: audio_gain_db or audio_gain (linear)
|
||||
gain_db = None
|
||||
if "audio_gain_db" in layer:
|
||||
try:
|
||||
gain_db = float(layer["audio_gain_db"])
|
||||
except Exception:
|
||||
gain_db = None
|
||||
elif "audio_gain" in layer:
|
||||
try:
|
||||
g = float(layer["audio_gain"])
|
||||
if g > 0:
|
||||
gain_db = 20.0 * math.log10(g)
|
||||
except Exception:
|
||||
gain_db = None
|
||||
|
||||
if gain_db is not None:
|
||||
audio_vol_db_by_index[in_index] = gain_db
|
||||
|
||||
# external audio as extra input
|
||||
external_audio_index = None
|
||||
if audio_mode == "external":
|
||||
if not os.path.isabs(audio_src):
|
||||
audio_src_resolved = audio_src # cwd-relative
|
||||
else:
|
||||
audio_src_resolved = audio_src
|
||||
input_files.append(audio_src_resolved)
|
||||
external_audio_index = len(input_files) - 1
|
||||
|
||||
# figure out which indices feed audio mix
|
||||
audio_mix_indices = []
|
||||
if audio_mode == "all":
|
||||
if base_has_audio:
|
||||
audio_mix_indices.append(0)
|
||||
audio_mix_indices.extend(overlay_audio_indices)
|
||||
elif audio_mode == "json_only":
|
||||
audio_mix_indices.extend(overlay_audio_indices)
|
||||
# base/external/none handled later
|
||||
|
||||
# 3) Build filter_complex (video + optional audio mix)
|
||||
filter_parts = []
|
||||
|
||||
# Start with base video
|
||||
# video chain: start from base
|
||||
current_label = "[0:v]"
|
||||
|
||||
for idx, (in_index, layer, src_w, src_h) in enumerate(overlay_specs, start=1):
|
||||
@@ -280,16 +406,15 @@ def run(args):
|
||||
if zoom < 1.0:
|
||||
zoom = 1.0
|
||||
|
||||
# Compute final overlay box size on the base
|
||||
# compute overlay box size
|
||||
box_w, box_h = _compute_overlay_box(base_w, src_w, src_h, size_pct)
|
||||
|
||||
mode = (layer.get("mode") or "place").lower()
|
||||
|
||||
if mode == "free":
|
||||
x_expr = str(int(layer.get("pos_x", 0)))
|
||||
y_expr = str(int(layer.get("pos_y", 0)))
|
||||
else:
|
||||
place = layer.get("place", "center")
|
||||
place = (layer.get("place") or "center").lower()
|
||||
px, py = _compute_place_coordinates(place, base_w, base_h, box_w, box_h)
|
||||
x_expr, y_expr = str(px), str(py)
|
||||
|
||||
@@ -299,7 +424,7 @@ def run(args):
|
||||
|
||||
layer_filters = []
|
||||
|
||||
# Optional crop (percent of source, BEFORE zoom/scale)
|
||||
# optional crop (percent of source BEFORE zoom/scale)
|
||||
cx = layer.get("crop_x")
|
||||
cy = layer.get("crop_y")
|
||||
cw = layer.get("crop_w")
|
||||
@@ -317,7 +442,7 @@ def run(args):
|
||||
# If parsing fails, skip cropping instead of blowing up.
|
||||
pass
|
||||
|
||||
# Zoom INSIDE fixed box:
|
||||
# zoom inside fixed box:
|
||||
# 1) scale up to zoom * box size
|
||||
# 2) crop center back down to box_w x box_h
|
||||
scaled_w = _even(int(box_w * zoom))
|
||||
@@ -338,11 +463,9 @@ def run(args):
|
||||
layer_filters.append("format=rgba")
|
||||
layer_filters.append(f"colorchannelmixer=aa={opacity}")
|
||||
|
||||
filter_parts.append(
|
||||
f"{in_label}{','.join(layer_filters)}{layer_label}"
|
||||
)
|
||||
filter_parts.append(f"{in_label}{','.join(layer_filters)}{layer_label}")
|
||||
|
||||
# Overlay on top of current composite
|
||||
# overlay
|
||||
filter_parts.append(
|
||||
f"{current_label}{layer_label}"
|
||||
f"overlay=x={x_expr}:y={y_expr}:format=auto"
|
||||
@@ -351,29 +474,97 @@ def run(args):
|
||||
|
||||
current_label = next_label
|
||||
|
||||
out_label = "[out_v]"
|
||||
filter_parts.append(f"{current_label}format=yuv420p{out_label}")
|
||||
out_v_label = "[out_v]"
|
||||
filter_parts.append(f"{current_label}format=yuv420p{out_v_label}")
|
||||
|
||||
# audio mix (for all/json_only + per-layer gain)
|
||||
audio_filter_output = None
|
||||
|
||||
if audio_mode in ("all", "json_only") and len(audio_mix_indices) >= 1:
|
||||
if len(audio_mix_indices) == 1:
|
||||
# Single audio source: optionally apply volume, no amix needed
|
||||
idx = audio_mix_indices[0]
|
||||
gain_db = audio_vol_db_by_index.get(idx)
|
||||
if gain_db is not None:
|
||||
audio_filter_output = "[out_a]"
|
||||
filter_parts.append(
|
||||
f"[{idx}:a]volume={gain_db}dB{audio_filter_output}"
|
||||
)
|
||||
else:
|
||||
# direct map of the input's audio stream
|
||||
audio_filter_output = f"{idx}:a"
|
||||
else:
|
||||
# Multiple sources: per-input volume (if any), then amix
|
||||
audio_inputs = []
|
||||
for idx in audio_mix_indices:
|
||||
gain_db = audio_vol_db_by_index.get(idx)
|
||||
if gain_db is not None:
|
||||
lbl = f"[av{idx}]"
|
||||
filter_parts.append(
|
||||
f"[{idx}:a]volume={gain_db}dB{lbl}"
|
||||
)
|
||||
audio_inputs.append(lbl)
|
||||
else:
|
||||
audio_inputs.append(f"[{idx}:a]")
|
||||
|
||||
audio_filter_output = "[out_a]"
|
||||
filter_parts.append(
|
||||
"".join(audio_inputs)
|
||||
+ f"amix=inputs={len(audio_mix_indices)}:normalize=0{audio_filter_output}"
|
||||
)
|
||||
|
||||
filter_complex = ";".join(filter_parts)
|
||||
|
||||
# 3) Build main ffmpeg command
|
||||
# 4) Build main ffmpeg command
|
||||
command = ["ffmpeg", "-hide_banner", "-loglevel", "error"]
|
||||
|
||||
for path in input_files:
|
||||
command.extend(["-i", path])
|
||||
|
||||
command.extend([
|
||||
"-filter_complex", filter_complex,
|
||||
"-map", out_label,
|
||||
"-map", "0:a?",
|
||||
"-c:v", "libx264",
|
||||
"-profile:v", "high",
|
||||
"-level:v", "4.2",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-movflags", "+faststart",
|
||||
"-c:a", "aac",
|
||||
args.output,
|
||||
])
|
||||
command.extend(["-filter_complex", filter_complex, "-map", out_v_label])
|
||||
|
||||
# audio mapping according to mode
|
||||
if audio_mode == "base":
|
||||
command.extend(["-map", "0:a?"])
|
||||
elif audio_mode in ("all", "json_only"):
|
||||
if len(audio_mix_indices) == 0:
|
||||
# no audio
|
||||
pass
|
||||
elif len(audio_mix_indices) == 1:
|
||||
if audio_filter_output == "[out_a]":
|
||||
command.extend(["-map", audio_filter_output])
|
||||
else:
|
||||
# audio_filter_output is like "N:a"
|
||||
command.extend(["-map", audio_filter_output])
|
||||
else:
|
||||
command.extend(["-map", audio_filter_output or "0:a?"])
|
||||
elif audio_mode == "external":
|
||||
if external_audio_index is not None:
|
||||
command.extend(["-map", f"{external_audio_index}:a?"])
|
||||
elif audio_mode == "none":
|
||||
command.append("-an")
|
||||
|
||||
# video codec settings
|
||||
command.extend(
|
||||
[
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-profile:v",
|
||||
"high",
|
||||
"-level:v",
|
||||
"4.2",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
]
|
||||
)
|
||||
|
||||
# audio codec if not explicitly disabled
|
||||
if audio_mode != "none":
|
||||
command.extend(["-c:a", "aac"])
|
||||
|
||||
command.append(args.output)
|
||||
|
||||
final_cmd = (command[:1] + ["-y"] + command[1:]) if args.force else command
|
||||
run_ffmpeg_with_progress(final_cmd, args.input, args.output)
|
||||
|
||||
Reference in New Issue
Block a user