added lagkage and updating

This commit is contained in:
Christopher Konopka
2025-11-22 14:42:28 -05:00
parent 9b894aa226
commit 09684ac5b5
32 changed files with 2103 additions and 149 deletions

View File

@@ -0,0 +1,402 @@
LAGKAGE CORE FUNCTIONALITY EXAMPLES (v2)
========================================
Program:
lagkage (Videobeaux)
Base usage pattern:
videobeaux -P lagkage -i media/base.mp4 -o out/NAME.mp4 --layout-json lagkage_layouts/LAYOUT.json --force
This file contains 20+ examples demonstrating:
- place vs free positioning
- images / videos / GIFs
- sequence_direction variations
- per-layer zoom
- per-layer crop
- combinations of the above
------------------------------------------------------------
EXAMPLE 01 Minimal logo + sticker (place + free)
------------------------------------------------------------
Layout:
lagkage_layouts/layout_minimal.json
Description:
- Top-right logo bug using place mode
- Free-position looping GIF sticker at lower-left area
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex01_minimal_logo_sticker.mp4 \
--layout-json lagkage_layouts/layout_minimal.json \
--force
------------------------------------------------------------
EXAMPLE 02 Same layout, different base
------------------------------------------------------------
Layout:
lagkage_layouts/layout_minimal.json
Description:
- Reuses the same layout with a different base video (e.g. a show cut)
Command:
videobeaux -P lagkage -i media/base_show.mp4 \
-o out/ex02_minimal_logo_sticker_show.mp4 \
--layout-json lagkage_layouts/layout_minimal.json \
--force
------------------------------------------------------------
EXAMPLE 03 Dual PIP + logo + lower third
------------------------------------------------------------
Layout:
lagkage_layouts/layout_pip_dual.json
Description:
- Logo in top-left
- Left and right PIP cameras using free mode
- Image lower-third band
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex03_pip_dual_show.mp4 \
--layout-json lagkage_layouts/layout_pip_dual.json \
--force
------------------------------------------------------------
EXAMPLE 04 Dual PIP, backward stacking
------------------------------------------------------------
Layout:
lagkage_layouts/layout_pip_dual.json
(Edit JSON: "sequence_direction": "backward")
Description:
- Same positions as Example 03
- Backward stacking puts higher layer_number on top
- Good for making sure logo always ends above other overlays
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex04_pip_dual_backward.mp4 \
--layout-json lagkage_layouts/layout_pip_dual.json \
--force
------------------------------------------------------------
EXAMPLE 05 Stickers + VHS noise overlay
------------------------------------------------------------
Layout:
lagkage_layouts/layout_stickers.json
Description:
- Full-frame looping VHS noise (low opacity)
- Circus and bunny GIF stickers
- Floating logo
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex05_stickers_vhs_noise.mp4 \
--layout-json lagkage_layouts/layout_stickers.json \
--force
------------------------------------------------------------
EXAMPLE 06 Freeform showcase (cams, gifs, masks, noise)
------------------------------------------------------------
Layout:
lagkage_layouts/layout_freeform_showcase.json
Description:
- Multiple layers:
- top-right logo, top-left screen
- left/right PIP cams, mask on edge, lower band
- circus & bunny GIFs, center VHS noise
- All positioned using free mode coordinates
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex06_freeform_showcase.mp4 \
--layout-json lagkage_layouts/layout_freeform_showcase.json \
--force
------------------------------------------------------------
EXAMPLE 07 3-up horizontal video grid
------------------------------------------------------------
Layout:
lagkage_layouts/layout_grid_3up.json
Description:
- Three video tiles left/center/right
- Small logo in top-left
- Good for side-by-side comparison or triptych views
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex07_grid_3up.mp4 \
--layout-json lagkage_layouts/layout_grid_3up.json \
--force
------------------------------------------------------------
EXAMPLE 08 4-up grid (quad cam)
------------------------------------------------------------
Layout:
lagkage_layouts/layout_grid_4up.json
Description:
- Four camera feeds in a 2x2 grid
- Classic multi-cam surveillance / gallery layout
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex08_grid_4up.mp4 \
--layout-json lagkage_layouts/layout_grid_4up.json \
--force
------------------------------------------------------------
EXAMPLE 09 Heavy VHS wash + mask + logo
------------------------------------------------------------
Layout:
lagkage_layouts/layout_vhs_heavy.json
Description:
- VHS noise strongly overlayed (higher opacity)
- Center mask image
- Top-center logo
Command:
videobeaux -P lagkage -i media/base.mp4 \
-o out/ex09_vhs_heavy_mask_logo.mp4 \
--layout-json lagkage_layouts/layout_vhs_heavy.json \
--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

Binary file not shown.

View File

@@ -0,0 +1,88 @@
Lagkage Zoom + Crop Example Commands
====================================
These examples assume:
- You are in the project root.
- Base video is at: media/base.mp4
- JSON layouts are in: lagkage_layouts/
- Output directory: out/
1) Corner logo with zoom
------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex01_logo_corner_zoom.mp4 \
--layout-json lagkage_layouts/layout_ex01_logo_corner_zoom.json \
--force
2) GIF sticker with zoom + crop
-------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex02_gif_zoomcrop_sticker.mp4 \
--layout-json lagkage_layouts/layout_ex02_gif_zoomcrop_sticker.json \
--force
3) Dual PIP, right pane zoomed
------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex03_dual_pip_zoom_right.mp4 \
--layout-json lagkage_layouts/layout_ex03_dual_pip_zoom_right.json \
--force
4) Lower-third band via crop
----------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex04_lower_third_crop_band.mp4 \
--layout-json lagkage_layouts/layout_ex04_lower_third_crop_band.json \
--force
5) Center mask, cropped + zoomed
--------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex05_mask_crop_zoom_center.mp4 \
--layout-json lagkage_layouts/layout_ex05_mask_crop_zoom_center.json \
--force
6) Freeform spray (GIFs + logo, random sequence)
------------------------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex06_freeform_spray_zoom.mp4 \
--layout-json lagkage_layouts/layout_ex06_freeform_spray_zoom.json \
--force
7) Logo grid with zoomed center
-------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex07_logo_grid_zoom_center.mp4 \
--layout-json lagkage_layouts/layout_ex07_logo_grid_zoom_center.json \
--force
8) Story mode: band + zoomed PIP + logo
---------------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex08_story_mode_zoom.mp4 \
--layout-json lagkage_layouts/layout_ex08_story_mode_zoom.json \
--force
9) Split-screen: left zoom+crop, right normal
---------------------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex09_split_screen_zoom_left.mp4 \
--layout-json lagkage_layouts/layout_ex09_split_screen_zoom_left.json \
--force
10) Full showcase (logos, bands, PIPs, GIFs)
--------------------------------------------
videobeaux -P lagkage \
-i media/base.mp4 \
-o out/ex10_full_showcase_zoomcrop.mp4 \
--layout-json lagkage_layouts/layout_ex10_full_showcase_zoomcrop.json \
--force

View File

@@ -0,0 +1,93 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_top_right_free",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 20,
"pos_y": 420,
"size": 30,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "cropped_circus",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "place",
"place": "bottom_right",
"size": 20,
"opacity": 1.0,
"zoom": 1.0,
"crop_x": 25,
"crop_y": 25,
"crop_w": 50,
"crop_h": 50
},
{
"layer_number": 4,
"name": "zoomed_bunny",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 200,
"pos_y": 500,
"size": 15,
"opacity": 1.0,
"zoom": 2.0
}
,
{
"layer_number": 6,
"name": "gross_lower_band_free",
"filename": "../media/gross.jpg",
"type": "img",
"mode": "free",
"pos_x": 180,
"pos_y": 180,
"size": 25,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 7,
"name": "circus_sticker_top_center_free",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 60,
"pos_y": 260,
"size": 50,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 8,
"name": "bunny_sticker_right_mid_free",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 50,
"pos_y": 365,
"size": 75,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 9,
"name": "vhs_noise_center_free",
"filename": "../media/media.gif",
"type": "gif",
"mode": "free",
"pos_x": 175,
"pos_y": 350,
"size": 60,
"opacity": 0.3,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,34 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_top_center",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 840,
"pos_y": 40,
"size": 18,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "crop_zoom_circus",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 260,
"pos_y": 260,
"size": 22,
"opacity": 1.0,
"zoom": 1.8,
"crop_x": 20,
"crop_y": 20,
"crop_w": 60,
"crop_h": 60,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,31 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "corner_logo",
"filename": "../media/logo.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "cropped_circus_corner",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "place",
"place": "bottom_right",
"size": 20,
"opacity": 1.0,
"crop_x": 25,
"crop_y": 25,
"crop_w": 50,
"crop_h": 50,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_zoom_top_right",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 42,
"opacity": 0.95,
"zoom": 4.5
}
]
}

View File

@@ -0,0 +1,21 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "gif_zoomcrop_sticker",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 150,
"pos_y": 420,
"size": 18,
"opacity": 1.0,
"zoom": 1.8,
"crop_x": 10,
"crop_y": 10,
"crop_w": 80,
"crop_h": 80
}
]
}

View File

@@ -0,0 +1,28 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "pip_left_normal",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 160,
"size": 28,
"opacity": 1.0
},
{
"layer_number": 2,
"name": "pip_right_zoomed",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1180,
"pos_y": 160,
"size": 28,
"opacity": 1.0,
"zoom": 1.7
}
]
}

View File

@@ -0,0 +1,20 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "lower_third_band_crop",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 260,
"pos_y": 780,
"size": 54,
"opacity": 0.95,
"crop_x": 0,
"crop_y": 20,
"crop_w": 100,
"crop_h": 60
}
]
}

View File

@@ -0,0 +1,20 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "mask_center_zoomcrop",
"filename": "../media/mask1.png",
"type": "img",
"mode": "place",
"place": "center",
"size": 30,
"opacity": 0.9,
"zoom": 1.6,
"crop_x": 10,
"crop_y": 10,
"crop_w": 80,
"crop_h": 80
}
]
}

View File

@@ -0,0 +1,39 @@
{
"sequence_direction": "random",
"layers": [
{
"layer_number": 1,
"name": "circus_left_zoom",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 120,
"pos_y": 200,
"size": 20,
"opacity": 1.0,
"zoom": 1.5
},
{
"layer_number": 2,
"name": "bunny_right_zoom",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 100,
"pos_y": 320,
"size": 26,
"opacity": 1.0,
"zoom": 2.5
},
{
"layer_number": 3,
"name": "logo_center",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "center",
"size": 48,
"opacity": 4.95
}
]
}

View File

@@ -0,0 +1,56 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_tl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9
},
{
"layer_number": 2,
"name": "logo_tr",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 10,
"opacity": 0.9
},
{
"layer_number": 3,
"name": "logo_bl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_left",
"size": 10,
"opacity": 0.9
},
{
"layer_number": 4,
"name": "logo_br",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_right",
"size": 10,
"opacity": 0.9
},
{
"layer_number": 5,
"name": "logo_center_zoom",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "center",
"size": 16,
"opacity": 0.95,
"zoom": 1.7
}
]
}

View File

@@ -0,0 +1,37 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "band_bottom",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 220,
"pos_y": 800,
"size": 60,
"opacity": 0.95
},
{
"layer_number": 2,
"name": "pip_top_right_zoom",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "place",
"place": "top_right",
"size": 22,
"opacity": 1.0,
"zoom": 1.6
},
{
"layer_number": 3,
"name": "logo_top_left",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 12,
"opacity": 0.95
}
]
}

View File

@@ -0,0 +1,32 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "pip_left_zoomcrop",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 160,
"size": 42,
"opacity": 1.0,
"zoom": 1.8,
"crop_x": 10,
"crop_y": 10,
"crop_w": 80,
"crop_h": 80
},
{
"layer_number": 2,
"name": "pip_right_normal",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 1040,
"pos_y": 160,
"size": 42,
"opacity": 1.0
}
]
}

View File

@@ -0,0 +1,76 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "corner_logo",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 14,
"opacity": 0.95
},
{
"layer_number": 2,
"name": "pip_left",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 120,
"pos_y": 220,
"size": 24,
"opacity": 1.0
},
{
"layer_number": 3,
"name": "pip_right_zoom",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1180,
"pos_y": 220,
"size": 24,
"opacity": 1.0,
"zoom": 1.5
},
{
"layer_number": 4,
"name": "lower_third_band",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 260,
"pos_y": 780,
"size": 54,
"opacity": 0.95
},
{
"layer_number": 5,
"name": "circus_zoomcrop",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 420,
"pos_y": 260,
"size": 18,
"opacity": 1.0,
"zoom": 1.8,
"crop_x": 20,
"crop_y": 20,
"crop_w": 60,
"crop_h": 60
},
{
"layer_number": 6,
"name": "bunny_free",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 1320,
"pos_y": 260,
"size": 16,
"opacity": 1.0
}
]
}

View File

@@ -0,0 +1,113 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_top_right_free",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 1500,
"pos_y": 40,
"size": 14,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "screen_top_left_free",
"filename": "../media/screen.png",
"type": "img",
"mode": "free",
"pos_x": 120,
"pos_y": 70,
"size": 12,
"opacity": 0.8,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "pip_cam_a_left_mid_free",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 260,
"pos_y": 260,
"size": 24,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "pip_cam_b_right_low_free",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1180,
"pos_y": 540,
"size": 34,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 5,
"name": "mask_left_edge_half_off_free",
"filename": "../media/mask1.png",
"type": "img",
"mode": "free",
"pos_x": -50,
"pos_y": 360,
"size": 22,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 6,
"name": "gross_lower_band_free",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 340,
"pos_y": 780,
"size": 50,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 7,
"name": "circus_sticker_top_center_free",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 760,
"pos_y": 120,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 8,
"name": "bunny_sticker_right_mid_free",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 1450,
"pos_y": 320,
"size": 13,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 9,
"name": "vhs_noise_center_free",
"filename": "../media/media.gif",
"type": "gif",
"mode": "free",
"pos_x": 0,
"pos_y": 0,
"size": 110,
"opacity": 0.3,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,106 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "corner_logo",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 14,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "pip_left",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 120,
"pos_y": 200,
"size": 24,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "pip_right",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1180,
"pos_y": 200,
"size": 24,
"opacity": 1.0,
"zoom": 1.4,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "lower_third_band",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 260,
"pos_y": 780,
"size": 54,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 5,
"name": "mask_left_edge",
"filename": "../media/mask1.png",
"type": "img",
"mode": "free",
"pos_x": -40,
"pos_y": 320,
"size": 26,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 6,
"name": "circus_sticker_zoomcrop",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 420,
"pos_y": 260,
"size": 18,
"opacity": 1.0,
"zoom": 1.8,
"crop_x": 20,
"crop_y": 20,
"crop_w": 60,
"crop_h": 60,
"blend_mode": "normal"
},
{
"layer_number": 7,
"name": "bunny_sticker_free",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 1320,
"pos_y": 260,
"size": 16,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 8,
"name": "vhs_noise_background",
"filename": "../media/media.gif",
"type": "gif",
"mode": "free",
"pos_x": 0,
"pos_y": 0,
"size": 110,
"opacity": 0.3,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,52 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_top_left_small",
"filename": "../media/logo.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "tile_left",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 160,
"size": 28,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "tile_center",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 640,
"pos_y": 160,
"size": 28,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "tile_right",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 1200,
"pos_y": 160,
"size": 28,
"opacity": 1.0,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,53 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "cam_tl",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 80,
"size": 40,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "cam_tr",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1040,
"pos_y": 80,
"size": 40,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "cam_bl",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 540,
"size": 40,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "cam_br",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1040,
"pos_y": 540,
"size": 40,
"opacity": 1.0,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,85 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_tl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "logo_tr",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "logo_bl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "logo_br",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_right",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 5,
"name": "logo_mid_left",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 220,
"pos_y": 360,
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 6,
"name": "logo_mid_right",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 1400,
"pos_y": 360,
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 7,
"name": "logo_center_zoomed",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "center",
"size": 18,
"opacity": 0.95,
"zoom": 1.6,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,73 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_tl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "logo_tr",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "logo_bl",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "logo_br",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "bottom_right",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 5,
"name": "screen_mid_left",
"filename": "../media/screen.png",
"type": "img",
"mode": "free",
"pos_x": 200,
"pos_y": 300,
"size": 14,
"opacity": 0.8,
"blend_mode": "normal"
},
{
"layer_number": 6,
"name": "screen_mid_right",
"filename": "../media/screen.png",
"type": "img",
"mode": "free",
"pos_x": 1300,
"pos_y": 300,
"size": 14,
"opacity": 0.8,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,28 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "corner_logo",
"filename": "../media/logo.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 12,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "looping_sticker",
"filename": "../media/sticker.gif",
"type": "gif",
"mode": "free",
"pos_x": 160,
"pos_y": 420,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,33 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "corner_logo",
"filename": "../media/logo.png",
"type": "img",
"mode": "place",
"place": "top_right",
"size": 12,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "zoomcrop_sticker",
"filename": "../media/sticker.gif",
"type": "gif",
"mode": "free",
"pos_x": 160,
"pos_y": 420,
"size": 18,
"opacity": 1.0,
"zoom": 1.5,
"crop_x": 10,
"crop_y": 10,
"crop_w": 80,
"crop_h": 80,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,52 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "logo_top_left",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 16,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "pip_cam_left",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 120,
"size": 26,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "pip_cam_right",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "free",
"pos_x": 1180,
"pos_y": 120,
"size": 26,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "lower_third_band",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 260,
"pos_y": 780,
"size": 54,
"opacity": 0.95,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,64 @@
{
"sequence_direction": "random",
"layers": [
{
"layer_number": 1,
"name": "circus_left",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 120,
"pos_y": 200,
"size": 22,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "circus_right",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 1400,
"pos_y": 100,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "bunny_low_left",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 80,
"pos_y": 640,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "bunny_low_right",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 1480,
"pos_y": 640,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 5,
"name": "logo_center",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "center",
"size": 20,
"opacity": 0.95,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,34 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "pip_left_normal",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 80,
"pos_y": 160,
"size": 40,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "pip_right_zoomcrop",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "free",
"pos_x": 1040,
"pos_y": 160,
"size": 40,
"opacity": 1.0,
"zoom": 1.6,
"crop_x": 15,
"crop_y": 15,
"crop_w": 70,
"crop_h": 70,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,53 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "vhs_noise_center_overlay",
"filename": "../media/media.gif",
"type": "gif",
"mode": "free",
"pos_x": 0,
"pos_y": 0,
"size": 110,
"opacity": 0.3,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "circus_sticker",
"filename": "../media/circus.gif",
"type": "gif",
"mode": "free",
"pos_x": 220,
"pos_y": 260,
"size": 18,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "bunny_sticker",
"filename": "../media/bunny.gif",
"type": "gif",
"mode": "free",
"pos_x": 1460,
"pos_y": 260,
"size": 14,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "logo_floating",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 1500,
"pos_y": 40,
"size": 14,
"opacity": 0.95,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,51 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "lower_third_band",
"filename": "../media/gross.png",
"type": "img",
"mode": "free",
"pos_x": 200,
"pos_y": 800,
"size": 60,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "mask_right_mid",
"filename": "../media/mask1.png",
"type": "img",
"mode": "free",
"pos_x": 1200,
"pos_y": 360,
"size": 28,
"opacity": 0.9,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "pip_corner_small",
"filename": "../media/faith.mp4",
"type": "video",
"mode": "place",
"place": "top_right",
"size": 20,
"opacity": 1.0,
"blend_mode": "normal"
},
{
"layer_number": 4,
"name": "logo_top_left_small",
"filename": "../media/logo.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 10,
"opacity": 0.9,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,41 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "vhs_noise_full",
"filename": "../media/media.gif",
"type": "gif",
"mode": "free",
"pos_x": 0,
"pos_y": 0,
"size": 120,
"opacity": 0.5,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "mask_center",
"filename": "../media/mask1.png",
"type": "img",
"mode": "free",
"pos_x": 640,
"pos_y": 260,
"size": 40,
"opacity": 0.95,
"blend_mode": "normal"
},
{
"layer_number": 3,
"name": "logo_top_center",
"filename": "../media/reem.png",
"type": "img",
"mode": "free",
"pos_x": 840,
"pos_y": 40,
"size": 18,
"opacity": 0.95,
"blend_mode": "normal"
}
]
}

View File

@@ -0,0 +1,28 @@
{
"sequence_direction": "forward",
"layers": [
{
"layer_number": 1,
"name": "pip_zoomed_small",
"filename": "../media/schwwaaa.mp4",
"type": "video",
"mode": "place",
"place": "top_right",
"size": 22,
"opacity": 1.0,
"zoom": 1.8,
"blend_mode": "normal"
},
{
"layer_number": 2,
"name": "logo_top_left",
"filename": "../media/reem.png",
"type": "img",
"mode": "place",
"place": "top_left",
"size": 14,
"opacity": 0.95,
"blend_mode": "normal"
}
]
}

View File

@@ -3,13 +3,14 @@
# 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 MP4s that loop for roughly the base
# 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
@@ -17,155 +18,185 @@ from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
def register_arguments(parser):
parser.description = (
"Compose multiple visual layers (images/gifs/videos) on a base video "
"using a JSON layout file. All layers are sized & positioned relative "
"to the base video dimensions."
"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,
help="Path to JSON layout describing all layers."
type=str,
help="Path to layout JSON describing layers.",
)
parser.add_argument(
"--sequence-direction",
dest="sequence_direction",
choices=["forward", "backward", "random"],
help="Override sequence_direction in the JSON (optional)."
default=None,
help="Override sequence_direction from JSON (forward|backward|random).",
)
def _load_layout(path: Path):
with open(path, "r", encoding="utf-8") as f:
layout = json.load(f)
if "layers" not in layout or not layout["layers"]:
raise ValueError("Layout JSON must contain a non-empty 'layers' array.")
return layout
def _resolve_sequence(layout, override=None):
seq = override or layout.get("sequence_direction", "forward")
layers = layout["layers"]
layers_sorted = sorted(layers, key=lambda L: L.get("layer_number", 0))
if seq == "forward":
ordered = layers_sorted
elif seq == "backward":
ordered = list(reversed(layers_sorted))
elif seq == "random":
ordered = layers_sorted[:]
random.shuffle(ordered)
else:
ordered = layers_sorted
return seq, ordered
# ----------------- ffprobe helpers -----------------
def _probe_base_info(path: str):
"""Return (width, height, duration_seconds) for the base video."""
"""
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",
"-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 {path} (code {proc.returncode}): {proc.stderr}"
)
data = json.loads(proc.stdout)
streams = data.get("streams") or []
if not streams:
raise RuntimeError(f"No video stream found in {path}")
s0 = streams[0]
width = int(s0.get("width", 0) or 0)
height = int(s0.get("height", 0) or 0)
if width <= 0 or height <= 0:
raise RuntimeError(f"Invalid video size from ffprobe for {path}: {width}x{height}")
fmt = data.get("format") or {}
dur_str = fmt.get("duration") or "0"
try:
duration = float(dur_str)
except ValueError:
duration = 0.0
if duration <= 0:
duration = 0.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 _place_expr(layer):
"""Return (x, y) expressions for the overlay filter."""
mode = layer.get("mode", "free")
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)
if mode == "free":
x = str(layer.get("pos_x", 0))
y = str(layer.get("pos_y", 0))
return x, y
slot = layer.get("place", "center")
# ----------------- small helpers -----------------
if slot == "top_left":
return "0", "0"
if slot == "top_right":
return "W-w", "0"
if slot == "bottom_left":
return "0", "H-h"
if slot == "bottom_right":
return "W-w", "H-h"
return "(W-w)/2", "(H-h)/2"
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:
"""
Transcode a GIF to a looping video with alpha that roughly matches
the base duration.
Convert GIF to a looping RGBA video with even dimensions and alpha.
Returns path to the temp video file.
IMPORTANT:
- We force even dimensions so filters/encoders don't choke.
- We use an alpha-capable codec (qtrle in a MOV container),
so transparency is preserved instead of being flattened.
- 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)
# Use .mov since qtrle is typically stored in a QuickTime container
temp_path = tmp_dir / f"lagkage_gif_{idx}.mov"
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",
"-ignore_loop", "0",
"-stream_loop", "-1",
"-i", src,
# Make width/height even and ensure we have RGBA (with alpha)
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=rgba",
]
# If we know the base duration, trim to that; otherwise just loop and
# let the main overlay graph's base video duration cap play-out.
if base_duration > 0:
cmd += ["-t", f"{base_duration:.3f}"]
cmd += [
# Alpha-capable codec
"-c:v", "qtrle",
"-an",
"-loglevel",
"error",
"-y",
str(temp_path),
"-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(temp_path)
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
# ----------------- main entry -----------------
def run(args):
@@ -173,78 +204,147 @@ def run(args):
if not layout_path.exists():
raise FileNotFoundError(f"Layout JSON not found: {layout_path}")
layout = _load_layout(layout_path)
seq, ordered_layers = _resolve_sequence(layout, args.sequence_direction)
# 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 json_layers.")
raise ValueError("Global --input (base video) is required for lagkage.")
# 1) Probe base video info once
# Probe base
base_w, base_h, base_duration = _probe_base_info(base_input)
# 2) Prepare inputs for main ffmpeg call
# index 0: base video
# index 1..N: layer sources (with GIFs preprocessed to MP4)
inputs_for_ffmpeg = [("base", base_input)]
layer_inputs = [] # (layer_dict, input_index, target_width_px)
# Temp dir for GIF preprocess
tmp_dir = Path(tempfile.mkdtemp(prefix="lagkage_gifs_"))
# Folder to hold temp GIF->MP4 files (next to the output file)
tmp_dir = Path(args.output).with_suffix("")
# 1) Build input list for ffmpeg
# input 0 = base, 1..N = overlays
input_files = [base_input]
overlay_specs = [] # (input_index, layer_dict, src_w, src_h)
for idx, layer in enumerate(ordered_layers, start=1):
filename = layer.get("filename")
if not filename:
raise ValueError(f"Layer missing 'filename': {layer}")
# Resolve relative to layout JSON file
# Relative paths are relative to the layout JSON directory.
if not os.path.isabs(filename):
src = str(layout_path.parent / filename)
else:
src = filename
size_pct = float(layer.get("size", 100)) / 100.0
target_w = max(1, int(base_w * size_pct))
layer_type = (layer.get("type") or "").lower()
# If GIF, pre-process to finite-length MP4 that loops to base duration
# If GIF, pre-process to finite-length video that loops to base duration
if layer_type == "gif":
overlay_src = _preprocess_gif(src, base_duration, tmp_dir, idx)
else:
overlay_src = src
inputs_for_ffmpeg.append(("video", overlay_src))
layer_inputs.append((layer, len(inputs_for_ffmpeg) - 1, target_w))
src_w, src_h = _probe_video_size(overlay_src)
# 3) Build filter_complex
input_files.append(overlay_src)
in_index = len(input_files) - 1
overlay_specs.append((in_index, layer, src_w, src_h))
# 2) Build filter_complex
filter_parts = []
# Base video label is [0:v] directly (like overlay_img_pro)
# Start with base video
current_label = "[0:v]"
for idx, (layer, input_index, target_w) in enumerate(layer_inputs, start=1):
lay_in = f"[{input_index}:v]"
lay_alpha = f"[lay{idx}]"
for idx, (in_index, layer, src_w, src_h) in enumerate(overlay_specs, start=1):
size_pct = float(layer.get("size", 100.0))
opacity = float(layer.get("opacity", 1.0))
if opacity < 0.0:
opacity = 0.0
if opacity > 1.0:
opacity = 1.0
zoom = float(layer.get("zoom", 1.0))
if zoom < 1.0:
zoom = 1.0
# Compute final overlay box size on the base
box_w, box_h = _compute_overlay_box(base_w, src_w, src_h, size_pct)
mode = (layer.get("mode") or "place").lower()
if mode == "free":
x_expr = str(int(layer.get("pos_x", 0)))
y_expr = str(int(layer.get("pos_y", 0)))
else:
place = layer.get("place", "center")
px, py = _compute_place_coordinates(place, base_w, base_h, box_w, box_h)
x_expr, y_expr = str(px), str(py)
in_label = f"[{in_index}:v]"
layer_label = f"[lay{idx}]"
next_label = f"[tmp{idx}]"
opacity = float(layer.get("opacity", 1.0))
# blend_mode is kept for future use but ignored here
_blend_mode = (layer.get("blend_mode") or "normal").lower()
layer_filters = []
x_expr, y_expr = _place_expr(layer)
# Optional crop (percent of source, BEFORE zoom/scale)
cx = layer.get("crop_x")
cy = layer.get("crop_y")
cw = layer.get("crop_w")
ch = layer.get("crop_h")
if cx is not None and cy is not None and cw is not None and ch is not None:
try:
cx_f = float(cx) / 100.0
cy_f = float(cy) / 100.0
cw_f = float(cw) / 100.0
ch_f = float(ch) / 100.0
layer_filters.append(
f"crop=in_w*{cw_f}:in_h*{ch_f}:in_w*{cx_f}:in_h*{cy_f}"
)
except Exception:
# If parsing fails, skip cropping instead of blowing up.
pass
# Zoom INSIDE fixed box:
# 1) scale up to zoom * box size
# 2) crop center back down to box_w x box_h
scaled_w = _even(int(box_w * zoom))
scaled_h = _even(int(box_h * zoom))
if scaled_w < 2:
scaled_w = 2
if scaled_h < 2:
scaled_h = 2
layer_filters.append(f"scale={scaled_w}:{scaled_h}")
if zoom > 1.0:
layer_filters.append(
f"crop={box_w}:{box_h}:(iw-{box_w})/2:(ih-{box_h})/2"
)
# Force RGBA and apply opacity
layer_filters.append("format=rgba")
layer_filters.append(f"colorchannelmixer=aa={opacity}")
# scale + alpha
filter_parts.append(
f"{lay_in}"
f"scale={target_w}:-1,"
f"format=rgba,colorchannelmixer=aa={opacity}"
f"{lay_alpha}"
f"{in_label}{','.join(layer_filters)}{layer_label}"
)
# overlay
# Overlay on top of current composite
filter_parts.append(
f"{current_label}{lay_alpha}"
f"{current_label}{layer_label}"
f"overlay=x={x_expr}:y={y_expr}:format=auto"
f"{next_label}"
)
@@ -256,20 +356,16 @@ def run(args):
filter_complex = ";".join(filter_parts)
# 4) Build main ffmpeg command
command = [
"ffmpeg",
"-err_detect", "ignore_err",
"-fflags", "+discardcorrupt+genpts",
]
# 3) Build main ffmpeg command
command = ["ffmpeg", "-hide_banner", "-loglevel", "error"]
for _typ, src in inputs_for_ffmpeg:
command.extend(["-i", src])
for path in input_files:
command.extend(["-i", path])
command.extend([
"-filter_complex", filter_complex,
"-map", out_label,
"-map", "0:a",
"-map", "0:a?",
"-c:v", "libx264",
"-profile:v", "high",
"-level:v", "4.2",
@@ -280,5 +376,4 @@ def run(args):
])
final_cmd = (command[:1] + ["-y"] + command[1:]) if args.force else command
run_ffmpeg_with_progress(final_cmd, args.input, args.output)