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)
|
1) Logo in top-right corner
|
||||||
========================================
|
----------------------------
|
||||||
|
videobeaux -P lagkage \
|
||||||
Program:
|
-i media/base.mp4 \
|
||||||
lagkage (Videobeaux)
|
-o out/ex01_layout_ex01_logo_corner.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex01_logo_corner.json \
|
||||||
Base usage pattern:
|
--audio-mode base \
|
||||||
videobeaux -P lagkage -i media/base.mp4 -o out/NAME.mp4 --layout-json lagkage_layouts/LAYOUT.json --force
|
--force
|
||||||
|
|
||||||
This file contains 20+ examples demonstrating:
|
2) Center GIF sticker
|
||||||
- place vs free positioning
|
----------------------
|
||||||
- images / videos / GIFs
|
videobeaux -P lagkage \
|
||||||
- sequence_direction variations
|
-i media/base.mp4 \
|
||||||
- per-layer zoom
|
-o out/ex02_layout_ex02_gif_center_sticker.mp4 \
|
||||||
- per-layer crop
|
--layout-json lagkage_layouts/layout_ex02_gif_center_sticker.json \
|
||||||
- combinations of the above
|
--audio-mode base \
|
||||||
|
--force
|
||||||
|
|
||||||
------------------------------------------------------------
|
3) Dual PIP videos with per-layer gains (all audio)
|
||||||
EXAMPLE 01 – Minimal logo + sticker (place + free)
|
----------------------------------------------------
|
||||||
------------------------------------------------------------
|
videobeaux -P lagkage \
|
||||||
|
-i media/base.mp4 \
|
||||||
Layout:
|
-o out/ex03_layout_ex03_dual_pip_videos.mp4 \
|
||||||
lagkage_layouts/layout_minimal.json
|
--layout-json lagkage_layouts/layout_ex03_dual_pip_videos.json \
|
||||||
|
--audio-mode all \
|
||||||
Description:
|
--force
|
||||||
- Top-right logo bug using place mode
|
|
||||||
- Free-position looping GIF sticker at lower-left area
|
4) Lower-third band from image
|
||||||
|
-------------------------------
|
||||||
Command:
|
videobeaux -P lagkage \
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
-i media/base.mp4 \
|
||||||
-o out/ex01_minimal_logo_sticker.mp4 \
|
-o out/ex04_layout_ex04_lower_third_band.mp4 \
|
||||||
--layout-json lagkage_layouts/layout_minimal.json \
|
--layout-json lagkage_layouts/layout_ex04_lower_third_band.json \
|
||||||
--force
|
--audio-mode base \
|
||||||
|
--force
|
||||||
|
|
||||||
------------------------------------------------------------
|
5) Center zoomed crop PIP
|
||||||
EXAMPLE 02 – Same layout, different base
|
--------------------------
|
||||||
------------------------------------------------------------
|
videobeaux -P lagkage \
|
||||||
|
-i media/base.mp4 \
|
||||||
Layout:
|
-o out/ex05_layout_ex05_center_zoom_crop_pip.mp4 \
|
||||||
lagkage_layouts/layout_minimal.json
|
--layout-json lagkage_layouts/layout_ex05_center_zoom_crop_pip.json \
|
||||||
|
--audio-mode all \
|
||||||
Description:
|
--force
|
||||||
- Reuses the same layout with a different base video (e.g. a show cut)
|
|
||||||
|
6) Scatter GIFs + logo (random sequence)
|
||||||
Command:
|
-----------------------------------------
|
||||||
videobeaux -P lagkage -i media/base_show.mp4 \
|
videobeaux -P lagkage \
|
||||||
-o out/ex02_minimal_logo_sticker_show.mp4 \
|
-i media/base.mp4 \
|
||||||
--layout-json lagkage_layouts/layout_minimal.json \
|
-o out/ex06_layout_ex06_scatter_gifs_logo.mp4 \
|
||||||
--force
|
--layout-json lagkage_layouts/layout_ex06_scatter_gifs_logo.json \
|
||||||
|
--audio-mode base \
|
||||||
|
--force
|
||||||
------------------------------------------------------------
|
|
||||||
EXAMPLE 03 – Dual PIP + logo + lower third
|
7) Logo grid around edges
|
||||||
------------------------------------------------------------
|
--------------------------
|
||||||
|
videobeaux -P lagkage \
|
||||||
Layout:
|
-i media/base.mp4 \
|
||||||
lagkage_layouts/layout_pip_dual.json
|
-o out/ex07_layout_ex07_logo_grid_edges.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex07_logo_grid_edges.json \
|
||||||
Description:
|
--audio-mode base \
|
||||||
- Logo in top-left
|
--force
|
||||||
- Left and right PIP cameras using free mode
|
|
||||||
- Image lower-third band
|
8) Story mode: band + PIP + logo (external audio)
|
||||||
|
--------------------------------------------------
|
||||||
Command:
|
videobeaux -P lagkage \
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
-i media/base.mp4 \
|
||||||
-o out/ex03_pip_dual_show.mp4 \
|
-o out/ex08_layout_ex08_story_mode_external_audio.mp4 \
|
||||||
--layout-json lagkage_layouts/layout_pip_dual.json \
|
--layout-json lagkage_layouts/layout_ex08_story_mode_external_audio.json \
|
||||||
--force
|
--audio-mode external --audio-src media/music_track.wav \
|
||||||
|
--force
|
||||||
|
|
||||||
------------------------------------------------------------
|
9) Split screen: left zoomed, right normal (all audio)
|
||||||
EXAMPLE 04 – Dual PIP, backward stacking
|
-------------------------------------------------------
|
||||||
------------------------------------------------------------
|
videobeaux -P lagkage \
|
||||||
|
-i media/base.mp4 \
|
||||||
Layout:
|
-o out/ex09_layout_ex09_split_screen.mp4 \
|
||||||
lagkage_layouts/layout_pip_dual.json
|
--layout-json lagkage_layouts/layout_ex09_split_screen.json \
|
||||||
(Edit JSON: "sequence_direction": "backward")
|
--audio-mode all \
|
||||||
|
--force
|
||||||
Description:
|
|
||||||
- Same positions as Example 03
|
10) Full showcase: GIFs + videos + logo (all audio)
|
||||||
- Backward stacking puts higher layer_number on top
|
---------------------------------------------------
|
||||||
- Good for making sure logo always ends above other overlays
|
videobeaux -P lagkage \
|
||||||
|
-i media/base.mp4 \
|
||||||
Command:
|
-o out/ex10_layout_ex10_full_showcase.mp4 \
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
--layout-json lagkage_layouts/layout_ex10_full_showcase.json \
|
||||||
-o out/ex04_pip_dual_backward.mp4 \
|
--audio-mode all \
|
||||||
--layout-json lagkage_layouts/layout_pip_dual.json \
|
--force
|
||||||
--force
|
|
||||||
|
11) Silent graphics-only output
|
||||||
|
-------------------------------
|
||||||
------------------------------------------------------------
|
videobeaux -P lagkage \
|
||||||
EXAMPLE 05 – Stickers + VHS noise overlay
|
-i media/base.mp4 \
|
||||||
------------------------------------------------------------
|
-o out/ex11_layout_ex11_silent_graphics_only.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex11_silent_graphics_only.json \
|
||||||
Layout:
|
--audio-mode none \
|
||||||
lagkage_layouts/layout_stickers.json
|
--force
|
||||||
|
|
||||||
Description:
|
12) JSON-only audio with different linear gains
|
||||||
- Full-frame looping VHS noise (low opacity)
|
-----------------------------------------------
|
||||||
- Circus and bunny GIF stickers
|
videobeaux -P lagkage \
|
||||||
- Floating logo
|
-i media/base.mp4 \
|
||||||
|
-o out/ex12_layout_ex12_json_only_audio_gains.mp4 \
|
||||||
Command:
|
--layout-json lagkage_layouts/layout_ex12_json_only_audio_gains.json \
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
--audio-mode json_only \
|
||||||
-o out/ex05_stickers_vhs_noise.mp4 \
|
--force
|
||||||
--layout-json lagkage_layouts/layout_stickers.json \
|
|
||||||
--force
|
13) Backward sequence stacked logos
|
||||||
|
-----------------------------------
|
||||||
|
videobeaux -P lagkage \
|
||||||
------------------------------------------------------------
|
-i media/base.mp4 \
|
||||||
EXAMPLE 06 – Freeform showcase (cams, gifs, masks, noise)
|
-o out/ex13_layout_ex13_backward_stack.mp4 \
|
||||||
------------------------------------------------------------
|
--layout-json lagkage_layouts/layout_ex13_backward_stack.json \
|
||||||
|
--audio-mode base \
|
||||||
Layout:
|
--force
|
||||||
lagkage_layouts/layout_freeform_showcase.json
|
|
||||||
|
14) Place mode quadrants with different sizes
|
||||||
Description:
|
---------------------------------------------
|
||||||
- Multiple layers:
|
videobeaux -P lagkage \
|
||||||
- top-right logo, top-left screen
|
-i media/base.mp4 \
|
||||||
- left/right PIP cams, mask on edge, lower band
|
-o out/ex14_layout_ex14_place_quadrants.mp4 \
|
||||||
- circus & bunny GIFs, center VHS noise
|
--layout-json lagkage_layouts/layout_ex14_place_quadrants.json \
|
||||||
- All positioned using free mode coordinates
|
--audio-mode base \
|
||||||
|
--force
|
||||||
Command:
|
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
15) UI-style small stickers around center
|
||||||
-o out/ex06_freeform_showcase.mp4 \
|
-----------------------------------------
|
||||||
--layout-json lagkage_layouts/layout_freeform_showcase.json \
|
videobeaux -P lagkage \
|
||||||
--force
|
-i media/base.mp4 \
|
||||||
|
-o out/ex15_layout_ex15_ui_stickers.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex15_ui_stickers.json \
|
||||||
------------------------------------------------------------
|
--audio-mode base \
|
||||||
EXAMPLE 07 – 3-up horizontal video grid
|
--force
|
||||||
------------------------------------------------------------
|
|
||||||
|
16) Zoom showcase at multiple levels (all audio)
|
||||||
Layout:
|
------------------------------------------------
|
||||||
lagkage_layouts/layout_grid_3up.json
|
videobeaux -P lagkage \
|
||||||
|
-i media/base.mp4 \
|
||||||
Description:
|
-o out/ex16_layout_ex16_zoom_showcase.mp4 \
|
||||||
- Three video tiles left/center/right
|
--layout-json lagkage_layouts/layout_ex16_zoom_showcase.json \
|
||||||
- Small logo in top-left
|
--audio-mode all \
|
||||||
- Good for side-by-side comparison or triptych views
|
--force
|
||||||
|
|
||||||
Command:
|
17) Crop focus 'face cam' PIP
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
-----------------------------
|
||||||
-o out/ex07_grid_3up.mp4 \
|
videobeaux -P lagkage \
|
||||||
--layout-json lagkage_layouts/layout_grid_3up.json \
|
-i media/base.mp4 \
|
||||||
--force
|
-o out/ex17_layout_ex17_crop_focus.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex17_crop_focus.json \
|
||||||
|
--audio-mode all \
|
||||||
------------------------------------------------------------
|
--force
|
||||||
EXAMPLE 08 – 4-up grid (quad cam)
|
|
||||||
------------------------------------------------------------
|
18) Off-screen pushes (half visible)
|
||||||
|
------------------------------------
|
||||||
Layout:
|
videobeaux -P lagkage \
|
||||||
lagkage_layouts/layout_grid_4up.json
|
-i media/base.mp4 \
|
||||||
|
-o out/ex18_layout_ex18_offscreen_push.mp4 \
|
||||||
Description:
|
--layout-json lagkage_layouts/layout_ex18_offscreen_push.json \
|
||||||
- Four camera feeds in a 2x2 grid
|
--audio-mode base \
|
||||||
- Classic multi-cam surveillance / gallery layout
|
--force
|
||||||
|
|
||||||
Command:
|
19) Media path using 'media/' root
|
||||||
videobeaux -P lagkage -i media/base.mp4 \
|
----------------------------------
|
||||||
-o out/ex08_grid_4up.mp4 \
|
videobeaux -P lagkage \
|
||||||
--layout-json lagkage_layouts/layout_grid_4up.json \
|
-i media/base.mp4 \
|
||||||
--force
|
-o out/ex19_layout_ex19_media_path_root.mp4 \
|
||||||
|
--layout-json lagkage_layouts/layout_ex19_media_path_root.json \
|
||||||
|
--audio-mode base \
|
||||||
------------------------------------------------------------
|
--force
|
||||||
EXAMPLE 09 – Heavy VHS wash + mask + logo
|
|
||||||
------------------------------------------------------------
|
20) Many audio clips premixed with different gains
|
||||||
|
--------------------------------------------------
|
||||||
Layout:
|
videobeaux -P lagkage \
|
||||||
lagkage_layouts/layout_vhs_heavy.json
|
-i media/base.mp4 \
|
||||||
|
-o out/ex20_layout_ex20_audio_premix.mp4 \
|
||||||
Description:
|
--layout-json lagkage_layouts/layout_ex20_audio_premix.json \
|
||||||
- VHS noise strongly overlayed (higher opacity)
|
--audio-mode all \
|
||||||
- Center mask image
|
--force
|
||||||
- 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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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 \
|
|
||||||
--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,76 +1,75 @@
|
|||||||
{
|
{
|
||||||
"sequence_direction": "forward",
|
"sequence_direction": "forward",
|
||||||
"layers": [
|
"layers": [{
|
||||||
{
|
"layer_number": 1,
|
||||||
"layer_number": 1,
|
"name": "corner_logo",
|
||||||
"name": "corner_logo",
|
"filename": "../media/reem.png",
|
||||||
"filename": "../media/reem.png",
|
"type": "img",
|
||||||
"type": "img",
|
"mode": "place",
|
||||||
"mode": "place",
|
"place": "top_left",
|
||||||
"place": "top_left",
|
"size": 14,
|
||||||
"size": 14,
|
"opacity": 0.95
|
||||||
"opacity": 0.95
|
},
|
||||||
},
|
{
|
||||||
{
|
"layer_number": 2,
|
||||||
"layer_number": 2,
|
"name": "pip_left",
|
||||||
"name": "pip_left",
|
"filename": "../media/schwwaaa.mp4",
|
||||||
"filename": "../media/schwwaaa.mp4",
|
"type": "video",
|
||||||
"type": "video",
|
"mode": "free",
|
||||||
"mode": "free",
|
"pos_x": 120,
|
||||||
"pos_x": 120,
|
"pos_y": 220,
|
||||||
"pos_y": 220,
|
"size": 24,
|
||||||
"size": 24,
|
"opacity": 1.0
|
||||||
"opacity": 1.0
|
},
|
||||||
},
|
{
|
||||||
{
|
"layer_number": 3,
|
||||||
"layer_number": 3,
|
"name": "pip_right_zoom",
|
||||||
"name": "pip_right_zoom",
|
"filename": "../media/faith.mp4",
|
||||||
"filename": "../media/faith.mp4",
|
"type": "video",
|
||||||
"type": "video",
|
"mode": "free",
|
||||||
"mode": "free",
|
"pos_x": 180,
|
||||||
"pos_x": 1180,
|
"pos_y": 220,
|
||||||
"pos_y": 220,
|
"size": 24,
|
||||||
"size": 24,
|
"opacity": 1.0,
|
||||||
"opacity": 1.0,
|
"zoom": 1.5
|
||||||
"zoom": 1.5
|
},
|
||||||
},
|
{
|
||||||
{
|
"layer_number": 4,
|
||||||
"layer_number": 4,
|
"name": "lower_third_band",
|
||||||
"name": "lower_third_band",
|
"filename": "../media/gross.png",
|
||||||
"filename": "../media/gross.png",
|
"type": "img",
|
||||||
"type": "img",
|
"mode": "free",
|
||||||
"mode": "free",
|
"pos_x": 260,
|
||||||
"pos_x": 260,
|
"pos_y": 780,
|
||||||
"pos_y": 780,
|
"size": 54,
|
||||||
"size": 54,
|
"opacity": 0.95
|
||||||
"opacity": 0.95
|
},
|
||||||
},
|
{
|
||||||
{
|
"layer_number": 5,
|
||||||
"layer_number": 5,
|
"name": "circus_zoomcrop",
|
||||||
"name": "circus_zoomcrop",
|
"filename": "../media/circus.gif",
|
||||||
"filename": "../media/circus.gif",
|
"type": "gif",
|
||||||
"type": "gif",
|
"mode": "free",
|
||||||
"mode": "free",
|
"pos_x": 420,
|
||||||
"pos_x": 420,
|
"pos_y": 260,
|
||||||
"pos_y": 260,
|
"size": 18,
|
||||||
"size": 18,
|
"opacity": 1.0,
|
||||||
"opacity": 1.0,
|
"zoom": 1.8,
|
||||||
"zoom": 1.8,
|
"crop_x": 20,
|
||||||
"crop_x": 20,
|
"crop_y": 20,
|
||||||
"crop_y": 20,
|
"crop_w": 60,
|
||||||
"crop_w": 60,
|
"crop_h": 60
|
||||||
"crop_h": 60
|
},
|
||||||
},
|
{
|
||||||
{
|
"layer_number": 6,
|
||||||
"layer_number": 6,
|
"name": "bunny_free",
|
||||||
"name": "bunny_free",
|
"filename": "../media/bunny.gif",
|
||||||
"filename": "../media/bunny.gif",
|
"type": "gif",
|
||||||
"type": "gif",
|
"mode": "free",
|
||||||
"mode": "free",
|
"pos_x": 1320,
|
||||||
"pos_x": 1320,
|
"pos_y": 260,
|
||||||
"pos_y": 260,
|
"size": 16,
|
||||||
"size": 16,
|
"opacity": 1.0
|
||||||
"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
|
tqdm==4.67.1
|
||||||
typing_extensions==4.14.1
|
typing_extensions==4.14.1
|
||||||
urllib3==2.5.0
|
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
|
videogrep==2.3.0
|
||||||
vosk==0.3.44
|
vosk==0.3.44
|
||||||
websockets==15.0.1
|
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
|
# Compose multiple visual layers (images/gifs/videos) on top of a base video
|
||||||
# using a single JSON layout file.
|
# using a single JSON layout file.
|
||||||
#
|
#
|
||||||
# GIF layers are preprocessed into finite videos that loop for roughly the base
|
# GIF layers are preprocessed into finite-length videos that loop for roughly
|
||||||
# video duration, so the main overlay graph stays simple and stable.
|
# the base video duration, so the main overlay graph stays simple and stable.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||||||
@@ -35,11 +36,40 @@ def register_arguments(parser):
|
|||||||
default=None,
|
default=None,
|
||||||
help="Override sequence_direction from JSON (forward|backward|random).",
|
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 -----------------
|
# ----------------- 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):
|
def _probe_base_info(path: str):
|
||||||
"""
|
"""
|
||||||
Return (width, height, duration_seconds) for the base video.
|
Return (width, height, duration_seconds) for the base video.
|
||||||
@@ -58,14 +88,17 @@ def _probe_base_info(path: str):
|
|||||||
"json",
|
"json",
|
||||||
path,
|
path,
|
||||||
]
|
]
|
||||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
out = _run_ffprobe(cmd)
|
||||||
if proc.returncode != 0:
|
data = json.loads(out)
|
||||||
raise RuntimeError(f"ffprobe failed for base {path}: {proc.stderr}")
|
|
||||||
info = json.loads(proc.stdout)
|
|
||||||
|
|
||||||
width = int(info["streams"][0]["width"])
|
streams = data.get("streams") or []
|
||||||
height = int(info["streams"][0]["height"])
|
if not streams:
|
||||||
duration = float(info["format"]["duration"])
|
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
|
return width, height, duration
|
||||||
|
|
||||||
|
|
||||||
@@ -85,16 +118,34 @@ def _probe_video_size(path: str):
|
|||||||
"csv=s=x:p=0",
|
"csv=s=x:p=0",
|
||||||
path,
|
path,
|
||||||
]
|
]
|
||||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
out = _run_ffprobe(cmd)
|
||||||
if proc.returncode != 0:
|
line = out.strip()
|
||||||
raise RuntimeError(f"ffprobe size failed for {path}: {proc.stderr}")
|
|
||||||
line = proc.stdout.strip()
|
|
||||||
if not line:
|
if not line:
|
||||||
raise RuntimeError(f"ffprobe size returned empty output for {path}")
|
raise RuntimeError(f"ffprobe size returned empty output for {path}")
|
||||||
w_str, h_str = line.split("x")
|
w_str, h_str = line.split("x")
|
||||||
return int(w_str), int(h_str)
|
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 -----------------
|
# ----------------- 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
|
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 -----------------
|
# ----------------- main entry -----------------
|
||||||
|
|
||||||
|
|
||||||
@@ -204,53 +303,39 @@ def run(args):
|
|||||||
if not layout_path.exists():
|
if not layout_path.exists():
|
||||||
raise FileNotFoundError(f"Layout JSON not found: {layout_path}")
|
raise FileNotFoundError(f"Layout JSON not found: {layout_path}")
|
||||||
|
|
||||||
# Load layout JSON
|
audio_mode = getattr(args, "audio_mode", "base") or "base"
|
||||||
layout = json.loads(layout_path.read_text())
|
audio_src = getattr(args, "audio_src", None)
|
||||||
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
|
if audio_mode == "external" and not audio_src:
|
||||||
seq_dir = args.sequence_direction or layout.get("sequence_direction", "forward")
|
raise ValueError("--audio-mode=external requires --audio-src")
|
||||||
if seq_dir not in ("forward", "backward", "random"):
|
|
||||||
seq_dir = "forward"
|
|
||||||
|
|
||||||
# Order layers by layer_number, then adjust sequence if needed
|
layout = _load_layout(layout_path)
|
||||||
ordered_layers = sorted(layers, key=lambda L: L.get("layer_number", 0))
|
seq, ordered_layers = _resolve_sequence(layout, args.sequence_direction)
|
||||||
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
|
base_input = args.input
|
||||||
if not base_input:
|
if not base_input:
|
||||||
raise ValueError("Global --input (base video) is required for lagkage.")
|
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_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_"))
|
tmp_dir = Path(tempfile.mkdtemp(prefix="lagkage_gifs_"))
|
||||||
|
|
||||||
# 1) Build input list for ffmpeg
|
input_files = [base_input] # index 0 = base
|
||||||
# input 0 = base, 1..N = overlays
|
overlay_specs = [] # (input_index, layer_dict, src_w, src_h)
|
||||||
input_files = [base_input]
|
overlay_audio_indices = [] # which overlay inputs actually have audio
|
||||||
overlay_specs = [] # (input_index, layer_dict, src_w, src_h)
|
audio_vol_db_by_index = {} # per-input gain in dB
|
||||||
|
|
||||||
for idx, layer in enumerate(ordered_layers, start=1):
|
for idx, layer in enumerate(ordered_layers, start=1):
|
||||||
filename = layer.get("filename")
|
filename = layer.get("filename")
|
||||||
if not filename:
|
if not filename:
|
||||||
raise ValueError(f"Layer missing 'filename': {layer}")
|
raise ValueError(f"Layer missing 'filename': {layer}")
|
||||||
|
|
||||||
# Relative paths are relative to the layout JSON directory.
|
src = _resolve_layer_path(filename, layout_path)
|
||||||
if not os.path.isabs(filename):
|
|
||||||
src = str(layout_path.parent / filename)
|
|
||||||
else:
|
|
||||||
src = filename
|
|
||||||
|
|
||||||
layer_type = (layer.get("type") or "").lower()
|
layer_type = (layer.get("type") or "").lower()
|
||||||
|
|
||||||
# If GIF, pre-process to finite-length video that loops to base duration
|
|
||||||
if layer_type == "gif":
|
if layer_type == "gif":
|
||||||
overlay_src = _preprocess_gif(src, base_duration, tmp_dir, idx)
|
overlay_src = _preprocess_gif(src, base_duration, tmp_dir, idx)
|
||||||
else:
|
else:
|
||||||
@@ -262,10 +347,51 @@ def run(args):
|
|||||||
in_index = len(input_files) - 1
|
in_index = len(input_files) - 1
|
||||||
overlay_specs.append((in_index, layer, src_w, src_h))
|
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 = []
|
filter_parts = []
|
||||||
|
|
||||||
# Start with base video
|
# video chain: start from base
|
||||||
current_label = "[0:v]"
|
current_label = "[0:v]"
|
||||||
|
|
||||||
for idx, (in_index, layer, src_w, src_h) in enumerate(overlay_specs, start=1):
|
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:
|
if zoom < 1.0:
|
||||||
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)
|
box_w, box_h = _compute_overlay_box(base_w, src_w, src_h, size_pct)
|
||||||
|
|
||||||
mode = (layer.get("mode") or "place").lower()
|
mode = (layer.get("mode") or "place").lower()
|
||||||
|
|
||||||
if mode == "free":
|
if mode == "free":
|
||||||
x_expr = str(int(layer.get("pos_x", 0)))
|
x_expr = str(int(layer.get("pos_x", 0)))
|
||||||
y_expr = str(int(layer.get("pos_y", 0)))
|
y_expr = str(int(layer.get("pos_y", 0)))
|
||||||
else:
|
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)
|
px, py = _compute_place_coordinates(place, base_w, base_h, box_w, box_h)
|
||||||
x_expr, y_expr = str(px), str(py)
|
x_expr, y_expr = str(px), str(py)
|
||||||
|
|
||||||
@@ -299,7 +424,7 @@ def run(args):
|
|||||||
|
|
||||||
layer_filters = []
|
layer_filters = []
|
||||||
|
|
||||||
# Optional crop (percent of source, BEFORE zoom/scale)
|
# optional crop (percent of source BEFORE zoom/scale)
|
||||||
cx = layer.get("crop_x")
|
cx = layer.get("crop_x")
|
||||||
cy = layer.get("crop_y")
|
cy = layer.get("crop_y")
|
||||||
cw = layer.get("crop_w")
|
cw = layer.get("crop_w")
|
||||||
@@ -317,7 +442,7 @@ def run(args):
|
|||||||
# If parsing fails, skip cropping instead of blowing up.
|
# If parsing fails, skip cropping instead of blowing up.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Zoom INSIDE fixed box:
|
# zoom inside fixed box:
|
||||||
# 1) scale up to zoom * box size
|
# 1) scale up to zoom * box size
|
||||||
# 2) crop center back down to box_w x box_h
|
# 2) crop center back down to box_w x box_h
|
||||||
scaled_w = _even(int(box_w * zoom))
|
scaled_w = _even(int(box_w * zoom))
|
||||||
@@ -338,11 +463,9 @@ def run(args):
|
|||||||
layer_filters.append("format=rgba")
|
layer_filters.append("format=rgba")
|
||||||
layer_filters.append(f"colorchannelmixer=aa={opacity}")
|
layer_filters.append(f"colorchannelmixer=aa={opacity}")
|
||||||
|
|
||||||
filter_parts.append(
|
filter_parts.append(f"{in_label}{','.join(layer_filters)}{layer_label}")
|
||||||
f"{in_label}{','.join(layer_filters)}{layer_label}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Overlay on top of current composite
|
# overlay
|
||||||
filter_parts.append(
|
filter_parts.append(
|
||||||
f"{current_label}{layer_label}"
|
f"{current_label}{layer_label}"
|
||||||
f"overlay=x={x_expr}:y={y_expr}:format=auto"
|
f"overlay=x={x_expr}:y={y_expr}:format=auto"
|
||||||
@@ -351,29 +474,97 @@ def run(args):
|
|||||||
|
|
||||||
current_label = next_label
|
current_label = next_label
|
||||||
|
|
||||||
out_label = "[out_v]"
|
out_v_label = "[out_v]"
|
||||||
filter_parts.append(f"{current_label}format=yuv420p{out_label}")
|
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)
|
filter_complex = ";".join(filter_parts)
|
||||||
|
|
||||||
# 3) Build main ffmpeg command
|
# 4) Build main ffmpeg command
|
||||||
command = ["ffmpeg", "-hide_banner", "-loglevel", "error"]
|
command = ["ffmpeg", "-hide_banner", "-loglevel", "error"]
|
||||||
|
|
||||||
for path in input_files:
|
for path in input_files:
|
||||||
command.extend(["-i", path])
|
command.extend(["-i", path])
|
||||||
|
|
||||||
command.extend([
|
command.extend(["-filter_complex", filter_complex, "-map", out_v_label])
|
||||||
"-filter_complex", filter_complex,
|
|
||||||
"-map", out_label,
|
# audio mapping according to mode
|
||||||
"-map", "0:a?",
|
if audio_mode == "base":
|
||||||
"-c:v", "libx264",
|
command.extend(["-map", "0:a?"])
|
||||||
"-profile:v", "high",
|
elif audio_mode in ("all", "json_only"):
|
||||||
"-level:v", "4.2",
|
if len(audio_mix_indices) == 0:
|
||||||
"-pix_fmt", "yuv420p",
|
# no audio
|
||||||
"-movflags", "+faststart",
|
pass
|
||||||
"-c:a", "aac",
|
elif len(audio_mix_indices) == 1:
|
||||||
args.output,
|
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
|
final_cmd = (command[:1] + ["-y"] + command[1:]) if args.force else command
|
||||||
run_ffmpeg_with_progress(final_cmd, args.input, args.output)
|
run_ffmpeg_with_progress(final_cmd, args.input, args.output)
|
||||||
|
|||||||
Reference in New Issue
Block a user