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 # 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 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. # 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
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
@@ -17,155 +18,185 @@ from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
def register_arguments(parser): def register_arguments(parser):
parser.description = ( parser.description = (
"Compose multiple visual layers (images/gifs/videos) on a base video " "Compose a base video with layered media defined by a JSON layout "
"using a JSON layout file. All layers are sized & positioned relative " "(images / videos / GIFs) using the lagkage compositor."
"to the base video dimensions."
) )
parser.add_argument( parser.add_argument(
"--layout-json", "--layout-json",
dest="layout_json",
required=True, required=True,
help="Path to JSON layout describing all layers." type=str,
help="Path to layout JSON describing layers.",
) )
parser.add_argument( parser.add_argument(
"--sequence-direction", "--sequence-direction",
dest="sequence_direction",
choices=["forward", "backward", "random"], 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): # ----------------- ffprobe helpers -----------------
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
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.
"""
cmd = [ cmd = [
"ffprobe", "ffprobe",
"-v", "error", "-v",
"-select_streams", "v:0", "error",
"-show_entries", "stream=width,height", "-select_streams",
"-show_entries", "format=duration", "v:0",
"-of", "json", "-show_entries",
"stream=width,height",
"-show_entries",
"format=duration",
"-of",
"json",
path, path,
] ]
proc = subprocess.run(cmd, capture_output=True, text=True) proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0: if proc.returncode != 0:
raise RuntimeError( raise RuntimeError(f"ffprobe failed for base {path}: {proc.stderr}")
f"ffprobe failed for {path} (code {proc.returncode}): {proc.stderr}" info = json.loads(proc.stdout)
)
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
width = int(info["streams"][0]["width"])
height = int(info["streams"][0]["height"])
duration = float(info["format"]["duration"])
return width, height, duration return width, height, duration
def _place_expr(layer): def _probe_video_size(path: str):
"""Return (x, y) expressions for the overlay filter.""" """
mode = layer.get("mode", "free") 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: 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 Convert GIF to a looping RGBA video with even dimensions and alpha.
the base duration. Returns path to the temp video file.
IMPORTANT: - Loops the GIF for approximately base_duration.
- We force even dimensions so filters/encoders don't choke. - Forces even width/height.
- We use an alpha-capable codec (qtrle in a MOV container), - Keeps alpha via format=rgba + qtrle.
so transparency is preserved instead of being flattened.
""" """
tmp_dir.mkdir(parents=True, exist_ok=True) tmp_dir.mkdir(parents=True, exist_ok=True)
# Use .mov since qtrle is typically stored in a QuickTime container out_path = tmp_dir / f"gif_pre_{idx:03d}.mov"
temp_path = tmp_dir / f"lagkage_gif_{idx}.mov"
vf = "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=rgba"
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
"-loglevel", "error", "-loglevel",
"-ignore_loop", "0", "error",
"-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",
"-y", "-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) proc = subprocess.run(cmd)
if proc.returncode != 0: if proc.returncode != 0:
raise RuntimeError(f"GIF preprocess failed for {src} (code {proc.returncode})") 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): def run(args):
@@ -173,78 +204,147 @@ 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}")
layout = _load_layout(layout_path) # Load layout JSON
seq, ordered_layers = _resolve_sequence(layout, args.sequence_direction) 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 base_input = args.input
if not base_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) base_w, base_h, base_duration = _probe_base_info(base_input)
# 2) Prepare inputs for main ffmpeg call # Temp dir for GIF preprocess
# index 0: base video tmp_dir = Path(tempfile.mkdtemp(prefix="lagkage_gifs_"))
# 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)
# Folder to hold temp GIF->MP4 files (next to the output file) # 1) Build input list for ffmpeg
tmp_dir = Path(args.output).with_suffix("") # 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): 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}")
# Resolve relative to layout JSON file # Relative paths are relative to the layout JSON directory.
if not os.path.isabs(filename): if not os.path.isabs(filename):
src = str(layout_path.parent / filename) src = str(layout_path.parent / filename)
else: else:
src = filename 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() 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": 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:
overlay_src = src overlay_src = src
inputs_for_ffmpeg.append(("video", overlay_src)) src_w, src_h = _probe_video_size(overlay_src)
layer_inputs.append((layer, len(inputs_for_ffmpeg) - 1, target_w))
# 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 = [] filter_parts = []
# Base video label is [0:v] directly (like overlay_img_pro) # Start with base video
current_label = "[0:v]" current_label = "[0:v]"
for idx, (layer, input_index, target_w) in enumerate(layer_inputs, start=1): for idx, (in_index, layer, src_w, src_h) in enumerate(overlay_specs, start=1):
lay_in = f"[{input_index}:v]" size_pct = float(layer.get("size", 100.0))
lay_alpha = f"[lay{idx}]" 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}]" next_label = f"[tmp{idx}]"
opacity = float(layer.get("opacity", 1.0)) layer_filters = []
# blend_mode is kept for future use but ignored here
_blend_mode = (layer.get("blend_mode") or "normal").lower()
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( filter_parts.append(
f"{lay_in}" f"{in_label}{','.join(layer_filters)}{layer_label}"
f"scale={target_w}:-1,"
f"format=rgba,colorchannelmixer=aa={opacity}"
f"{lay_alpha}"
) )
# overlay # Overlay on top of current composite
filter_parts.append( filter_parts.append(
f"{current_label}{lay_alpha}" 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"
f"{next_label}" f"{next_label}"
) )
@@ -256,20 +356,16 @@ def run(args):
filter_complex = ";".join(filter_parts) filter_complex = ";".join(filter_parts)
# 4) Build main ffmpeg command # 3) Build main ffmpeg command
command = [ command = ["ffmpeg", "-hide_banner", "-loglevel", "error"]
"ffmpeg",
"-err_detect", "ignore_err",
"-fflags", "+discardcorrupt+genpts",
]
for _typ, src in inputs_for_ffmpeg: for path in input_files:
command.extend(["-i", src]) command.extend(["-i", path])
command.extend([ command.extend([
"-filter_complex", filter_complex, "-filter_complex", filter_complex,
"-map", out_label, "-map", out_label,
"-map", "0:a", "-map", "0:a?",
"-c:v", "libx264", "-c:v", "libx264",
"-profile:v", "high", "-profile:v", "high",
"-level:v", "4.2", "-level:v", "4.2",
@@ -280,5 +376,4 @@ def run(args):
]) ])
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)