# Source Layout Probe (`andrew_babo/source-layout`) Actor

Panels, borders, gutters, logos and layout changes of composite video — pure geometry on grayscale samples (ffmpeg, no ML).

- **URL**: https://apify.com/andrew_babo/source-layout.md
- **Developed by:** [Andrew Babo](https://apify.com/andrew_babo) (community)
- **Stats:** 24 total users, 24 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## source-layout — bố cục bên trong khung hình nguồn

Actor Apify thuần hình học (Node + ffmpeg, **không** mô hình ML): tìm các *ô hình*
trong một nguồn ghép — podcast hai ô, lưới Zoom, chia sẻ màn hình + webcam,
letterbox/pillarbox, khung thương hiệu, hình-trong-hình — rồi trả về:

- `states[]`: từng khoảng thời gian có cùng bố cục, mỗi state gồm các `panels`
  (toạ độ chuẩn hoá 0..1, loại `camera | screen | static`, cờ `pip`, cạnh nào
  thật sự có biên), `decor` (viền, gutter, logo tĩnh), `signature`, `confidence`;
- `changes[]`: mốc đổi bố cục, `hard: true` khi trùng điểm cắt cảnh;
- `stats`: số khung lấy mẫu, số cảnh, thời gian tính.

Cùng một đầu vào ⇒ cùng một kết quả từng byte (không có ngẫu nhiên, không có
đồng hồ trong thuật toán).

### Cách làm (một đoạn)

Lấy mẫu 2 fps ở 480 px rộng, chỉ kênh xám. Mỗi cảnh (hoặc mỗi lát ≤ 12 s của
cảnh dài) được gom thành **ảnh trung bình** và **ảnh phương sai**. Vùng động
(phương sai cao) là chỗ có người/nội dung; đường thẳng tĩnh của ảnh trung bình
là khung ô, mép gutter, mép letterbox. Mỗi khối động nở ra tới đường thẳng gần
nhất ở bốn phía; một đường chỉ được nhận là *biên* khi phần bên kia nó "chết"
(không chuyển động, hoặc là một ô khác, hoặc là dải phẳng như letterbox). Luật
góc kín (đường cạnh chạy xuyên qua góc ⇒ đó là vạch chia *trong* một màn hình)
và luật ô tĩnh (người ngồi im bên cạnh vẫn là một ô) hoàn thiện bức tranh. Với
khung **người + màn hình** (slide, mã, biểu đồ) bước **mặt** (`src/layout/surfaces.js`)
chạy trước: đo "ít mức xám" theo cột/hàng để chia khung thành mặt màn hình và mặt
người, mặt màn hình là một ô trọn (trong đó chỉ tìm webcam có khung), mặt người đi
đường nở khối cũ nhưng bị rào trong mặt. Mọi ngưỡng nằm trong **một** bộ
`LAYOUT_POLICY` (`src/layout/panels.js`; túi `surfaces` = `SURFACE_POLICY`).

### Đầu vào

```jsonc
{
  "op": "layout",                       // hoặc "capabilities"
  "source": "https://… | kv:<storeId>/<key>",
  "options": { "start_sec": 0, "duration_sec": null, "sample_fps": 2, "analysis_width": 480 },
  "output": { "signed_upload_url": null },
  "cleanup": "on_success",
  "callback": { "url": "…", "secret_header": { "name": "x-…", "value": "…" } }
}
```

### Đầu ra

Artifact `layout` (`layout.json` trong KV store của run, hoặc đẩy thẳng tới
`signed_upload_url`):

```jsonc
{
  "version": 1,
  "probe": { "name": "source-layout", "version": "1.5.0", "sampleFps": 2, "analysisWidth": 480, "analysisHeight": 270 },
  "source": { "w": 1920, "h": 1080, "durationSec": 3120.4 },
  "states": [
    {
      "id": "s0", "startSec": 0, "endSec": 7, "kind": "two-up-h", "confidence": 1,
      "panels": [
        { "id": "p0", "rect": { "x": 0.0167, "y": 0.1519, "w": 0.4625, "h": 0.7704 }, "kind": "camera",
          "confidence": 1, "motion": 0.51, "bounded": { "left": true, "top": true, "right": true, "bottom": true } },
        { "id": "p1", "rect": { "x": 0.5167, "y": 0.1519, "w": 0.4646, "h": 0.763 }, "kind": "camera", "…": "…" }
      ],
      "decor": [ { "kind": "border", "rect": { "…": "…" } }, { "kind": "gutter", "rect": { "…": "…" } }, { "kind": "logo", "rect": { "…": "…" }, "panelId": "p1" } ],
      "signature": "2|1,8,24,46;26,8,49,46",
      "notes": []
    }
  ],
  "changes": [ { "atSec": 7, "fromStateId": "s0", "toStateId": "s1", "hard": true } ],
  "stats": { "frames": 120, "shots": 5, "slices": 7, "computeMs": 278, "totalMs": 577 }
}
```

`kind` của state: `single | two-up-h | two-up-v | grid | pip | screen+cam | unknown`.
Một nguồn một máy quay cho đúng một state `single` với một ô phủ cả khung và
`bounded` toàn `false` — máy dựng khung phía sau không cần phân nhánh.

### Chi phí

\~0,3 s tính toán cho mỗi phút video (2 fps, 480 px) cộng thời gian giải mã của
ffmpeg; bộ nhớ dưới 300 MB. Chạy trên proxy 480p của `video-audio-toolkit` là
rẻ nhất.

### Kiểm thử

```bash
npm test          # 20 cảnh giả lập vẽ bằng số: hai ô, người ngồi im, pillarbox + vạch chia,
                  # letterbox, mép tường trong phòng, PiP góc, lưới 2×2, cắt cảnh, state dài,
                  # khe hẹp có viền nhấp nháy…
```

Không có ngẫu nhiên nên kết quả thử là tất định; mọi lỗi tìm thấy trên video thật
phải được tái hiện bằng một cảnh giả lập mới trong `src/layout/layout.test.js`
trước khi sửa.

### Lịch sử máy dò (`probe.version`)

Đổi số hiệu khi LUẬT đọc bố cục đổi — sổ bố cục ghi theo `(project, probe_version)`
nên bản mới không đè bản cũ.

- `1.7.0` (09/10/2026): **băng chữ vắt ngang toàn khung là một vật thật, đo MỘT nơi ở bước mặt** (`frameBand`,
  D-SS-14). Human QA Greencap 3254 s: băng câu hỏi hai tông (thân xanh + dòng tên trắng) đè đáy cả người lẫn
  slide trong 13,5 s — `sharedStrip` (1.6.4) chỉ bắt được khi đường nở của ô người đã dừng đúng mép băng; băng
  có chữ thì dải "không phải nền chết" nên ô người bị thả xuống tận đáy (ôm băng), và khi băng trượt vào thì
  ảnh trung bình để ô người dừng lưng chừng trong băng. Nay băng được nhận trực tiếp: vạch cứng chạy suốt
  TỪNG mặt ở cùng độ cao, dải tới mép khung ≤ `sharedStripShare`, ít mức xám (≥ `sharedStripMode`), và hồ sơ
  HÀNG hai bên trùng nền (màu trội từng hàng chênh ≤ `sharedStripTone`; trung bình hàng chỉ làm trần
  `sharedStripMeanTone`) — người | slide lệch ~100, băng thật 11. `detectSurfaces` trả `band`; chưa chia được
  (băng che phần dưới vạch ghép dọc, độ phủ tụt dưới `snapCoverage`) thì so hai nửa trái/phải, bỏ băng ra khỏi
  khung nội dung rồi chia lại. `surfaceCandidates` RÀO mọi mặt tại vạch (ô người nở trong rào dừng ở đó, ô màn
  hình cắt ở đó) ⇒ băng thành `border` dưới hợp các ô; `completeScreenSiblings` đo "chạm hai mép" theo rào.
  Bộ Framing v2 12 ca và 32 ca Bài giảng & Màn hình không đổi một chỉ số.
- `1.6.4` (08/10/2026): **băng vắt ngang đáy/đỉnh CẢ người lẫn màn hình là viền chung.** Human QA
  Greencap: băng bình luận xanh 15 % khung chạy dưới cả hai ô — ô người đã dừng ở mép băng (đường
  nở dừng ở vạch) nhưng ô màn hình ôm luôn băng (nền slide + băng đều ít mức xám) ⇒ khung "Chỉ màn
  hình" mang theo băng. Nay `sharedStrip` (panels.js): mặt màn hình có vạch cứng chạy suốt đúng ở mép
  ô người, dải ngoài ≤ `sharedStripShare`, hai bên cùng tông (≤ `sharedStripTone`) và cùng màu trội
  (≥ `sharedStripMode`) ⇒ cắt ô màn hình ở vạch; băng thành `border` dưới hợp các ô. Phản chứng: vạch
  kẻ trong slide tình cờ ngang mép ô người không cắt (khác tông). Bộ Framing v2 12 ca và bộ 32 ca
  Bài giảng & Màn hình không đổi một chỉ số.
- `1.6.3` (08/10/2026): **dải trống chừa vai vạch; đảo mép gộp SAU đảo trong; ffmpeg ghim
  2 luồng.** Tìm ra gốc của "cửa sổ 2689 s sai trên mây, đúng tại chỗ": tái hiện được bằng
  ffmpeg 6 (bản trong hộp) so với 8 (tại chỗ) trên CÙNG tệp — bộ co ảnh đặt vạch cứng lệch
  một điểm (240 ↔ 239) nên cột vai sát vạch lọt vào dải đo, nét 0,031 > 0,03 ⇒ không kéo
  vạch ⇒ hồ sơ ngang thắng ("hai màn hình chồng"). Nay dải đo chừa `1 + insetPx` điểm phía
  vạch (`bandEmptyBetween`, có bài kiểm). Đảo mép 1.6.2 cũng không nổ: hàng xóm 553 s bị
  một đảo trong ở giây 48 chẻ làm "36 s" nên chưa "dài" lúc xét ⇒ nay gộp đảo trong trước,
  đảo mép sau (lượt riêng). ffmpeg `-threads 2` (xem `docs/ops/perf/screen-analysis.md`).
- `1.6.2` (08/10/2026): **đảo ở mép mảnh**: cửa sổ đầu/cuối của một mảnh (không có hàng
  xóm phía kia) đo lệch ≤ 2 cửa sổ, không có cú cắt ngăn với hàng xóm DÀI ≥ 4 cửa sổ ⇒
  nhập vào hàng xóm — cùng lý với đảo ngắn (hình không nhảy thì bố cục không đổi). Lượt
  chạy thật 1.6.1 trên bài giảng 69 phút: cửa sổ đầu mảnh 2689 s ra "hai màn hình chồng
  18/82 %" trên đám mây (cùng cửa sổ chạy tại chỗ ra đúng — khác biệt giải mã/scale giữa
  hai máy làm vạch ghép mờ dưới ngưỡng ở đúng một cửa sổ), rồi 553 s chia đôi liền sau
  không cú cắt. Hàng xóm cũng ngắn ⇒ không ai nuốt ai; có cú cắt ⇒ cảnh mở đầu thật, giữ.
- `1.6.1` (08/10/2026): **độ sống dọc đứng trước mức xám ngang; dải trống đo bằng vệt kẻ;
  mặt 50 % không là banner; dòng tên không là máy quay** (lượt chạy thật 1.6.0 trên bài
  giảng 69 phút: 64 → 19 trạng thái, còn hai cửa sổ sai). (1) Thứ tự cửa: dọc mức xám →
  dọc độ sống → ngang mức xám → ngang độ sống — cặp người | slide mà mức xám không chia
  được thì hồ sơ NGANG vẫn gãy ở thanh tiêu đề slide và cắt qua cả hai ("hai màn hình chồng
  18/82 %"). (2) Dải trống khi kéo vạch: nét ≤ 0,03, HOẶC ≤ 0,08 mà không có vệt kẻ chạy
  suốt (nguồn nén nét hơn bản xem: hạt tường thành cạnh, chữ slide thì xếp hàng). (3) Mặt
  màn hình theo mức xám không chứa ô đường cũ chỉ là "dải trang trí" khi MỎNG ≤ 35 % trục
  chia; mặt theo độ sống không hỏi đường cũ (băng bình luận vắt ngang đáy hai ô làm đường
  cũ nở lệch, slide 50 % từng bị bỏ ⇒ cả khung thành màn hình + "camera" 9 % đáy).
  (4) Ô nở trong mặt người có cạnh ngắn ≤ 12 % khung và dẹt ≥ 4:1 (dòng tên + ảnh đại diện
  dưới băng) ⇒ bỏ, không phải máy quay thứ hai. Bài kiểm: `lectureSplitCaptionBand`.
  Vai của dải nằm (băng 100 % × 18–23 %) là việc của `panelRoles.ts:stripShape` ⇒ trang trí.
- `1.6.0` (08/10/2026): **vạch ghép cứng thắng điểm gãy mức xám; cặp theo độ sống; lưới
  phải là lưới; đảo ngắn gộp** (chạy thật bài giảng chia đôi 69 phút, 08/10/2026 — một
  bố cục giữ 47 phút mà máy dò ra 64 trạng thái): (1) điểm gãy mức xám rơi ở MÉP NGƯỜI
  (tường tối sau lưng cũng "ít mức xám" như slide) ⇒ chia ở 36 % khung với tin cậy 0,9
  trong khi vạch ghép ở 50 %. Nay không có vạch sát điểm gãy thì **kéo vạch chia qua
  dải trống** (nét ≤ 0,03, đo thật 0,007–0,018) tới vạch cứng gần nhất về phía màn
  hình (≤ 30 % bề mặt); dải có nội dung thì không kéo — vạch bên trong slide không cướp
  mép. (2) Slide mang ảnh/biểu đồ (không "ít mức xám") + tường tối: mức xám không chia
  được và cửa "khung chết ≥ 55 %" gọi cả khung là màn hình ⇒ **cặp theo độ sống**
  (`splitByLiveness`): tại vạch cứng chạy suốt, bên chết ≥ 0,55 có nét là màn hình,
  bên chết ≤ 0,7 có khối đặc SỐNG cỡ ≥ một webcam là người (đo thật: slide 0,92–0,99
  chết, nửa người 0,03–0,30). (3) Giả thuyết lưới 2×2 "ranh 0,83" bịa trên người +
  slide: ranh nay phải **nổi hơn làn đối chứng** cách 6 px ≥ 0,2 (vân đều của ảnh cho
  cạnh ở mọi làn) và **không bị khối động mạnh vắt qua** (> 15 % ranh có động hai bên ⇒
  không phải khe giữa hai luồng hình). (4) `buildStates`: **đảo ngắn** ≤ 2 cửa sổ kẹp
  giữa hai trạng thái cùng hình học, KHÔNG có cú cắt ở hai đầu ⇒ gộp (hình không nhảy
  thì bố cục không đổi); có cú cắt ⇒ giữ. Năm cửa sổ lỗi thật (two-up-v, pip, lưới,
  chia 0,36) đều về đúng `camera 0–237 | screen 241–479`; bộ Framing v2 không đổi; bộ
  Bài giảng & Màn hình 32 ca không đổi (ô IoU 0,968). Bài kiểm: `lectureSplitDarkWall`,
  `lectureSplitDeadDoor`, đảo ngắn trong `buildStates`. Chạy `LAYOUT_DEBUG=1 node --test
  …` để in ô + ghi chú chẩn đoán của từng cảnh.
- `1.5.0` (08/10/2026): **màn hình toàn khung + webcam không viền** (chạy thật trên
  video chia sẻ màn hình, 08/10/2026): đường cũ chia màn hình thành nhiều "ô" là cửa
  sổ con của ứng dụng và bỏ sót webcam nhỏ góc dưới. Nay cả khung là mặt số khi đặc
  trưng ít mức xám **theo làn** (trung bình theo cột và theo hàng ≥ 0,8) + nét + ít
  động, HOẶC khi ≥ 55 % khung **chết** (độ sống < 0,12 — trang web nhiều màu làm biểu
  đồ xám loãng như ảnh người nhưng điểm ảnh số không lăn tăn; đo: màn hình chia sẻ
  0,63–0,93 chết, camera toàn khung 0,00–0,30) — và khi ấy mặt thắng, đường cũ chỉ còn
  quyền khi ra bố cục ghép **kín, tin cậy cao VÀ có người** (tường phẳng của podcast
  sáng). Khối đặc lớn chỉ là "người toàn khung" khi nó SỐNG; hộp màu trang web đổi vài
  lần rồi đứng im là nội dung số. Trên mặt màn hình, **ô chèn** (webcam) đo bằng **độ
  sống** từng điểm ảnh (phần cặp khung liên tiếp mà điểm ảnh đổi —
  `FrameAccumulator.liveness`): ảnh máy quay không bao giờ đứng im (nhiễu cảm biến;
  tường phẳng bị nén còn 0,1–0,3), mặt số chết hẳn (≤ 0,06), một lần đổi slide chỉ lật
  một cặp. Lấy LÕI là khối động mạnh **và sống** lớn nhất (hộp màu đổi vài lần cũng
  "mạnh" nhưng không sống — tính vào lõi thì webcam sát bên nhập chung thành dải dài),
  gọt mép chết, rồi NỞ từng dải 3 px chừng nào dải còn sống ≥ 0,12; dừng sớm ở **vạch
  mép** (bậc ≥ 24 mức xám dọc ≥ 60 % cạnh) khi 8–16 px bên kia đã chết — nén video làm
  độ sống tràn ra ngoài mép chừng một khối 16 px nên dải sát mép vẫn "sống". Hộp nở sai
  dáng (lan sang chữ cuộn kề bên) ⇒ nở lại NGHIÊM, dừng ở mọi vạch mép. Sau đó xét cỡ
  (≤ pipMaxArea), dáng (≤ 2,2:1, cao ≥ 10 % khung), ít mức xám < modeBox trừ khi cả ô
  sống ≥ 0,3, có khối người; **đồ hoạ động** (cột biểu đồ chạy: ≥ 15 % hộp lệch chuẩn
  > 60 mà ảnh trung bình không có vân) bị loại — webcam hiện/ẩn giữa cửa sổ cũng đổi
  > mạnh nhưng vẫn giữ vân. Hộp webcam **có vạch khung** chỉ là CẬN TRÊN: gọt theo độ
  > sống; bên trong không phải ảnh máy quay thì bỏ, phần sống chỉ chiếm góc nhỏ thì khung
  > là ô giao diện (hai ô lưới có viền ôm webcam) và lấy phần sống. Mặt "màn hình" không
  > chứa ô nào của đường cũ (banner tên kênh phẳng có chữ trên hai camera: 20VC) là dải
  > trang trí — bỏ phép chia, giữ hai ô như 1.3.1. Bộ kiểm đơn vị vẽ phòng có nhiễu cảm
  > biến ±2 (ảnh thật không bao giờ đứng im tuyệt đối). Bộ Framing v2 không đổi; bộ Bài
  > giảng & Màn hình 32 ca (nhãn webcam trôi = hộp quét): ô IoU 0,95 → 0,967, vai F1
  > 0,976 → 0,983, kiểu 0,87 → 0,893, sai nguồn 0,046 → 0,033, webcam đè IoU 0,891. Video
  > chia sẻ màn hình thật 18,6 phút: 24 trạng thái lộn xộn → 9, mọi trạng thái có webcam
  > đúng góc (sai mép ≤ 7 px ở 480 px), còn sót một cửa sổ người ngồi im 12 s (không có
  > khối động ⇒ chỉ màn hình).
- `1.4.0` (07/10/2026): **mặt màn hình** (`surfaces.js`, Bài giảng & Màn hình): trước
  khi nở khối, hỏi khung có mấy MẶT. Đặc trưng "ít mức xám" theo cột/hàng (5 bin đông
  nhất) tách slide/mã/biểu đồ (0,8–1,0) khỏi ảnh người (0,55–0,65); chia bằng điểm gãy
  bình phương tối thiểu, chỉ nhận cặp **người + màn hình** (màn hình ít động hơn người);
  cả khung là màn hình khi ít mức xám + nét + ít động và không có khối người đặc (phần
  khung ngoài khối vẫn là mặt số ⇒ webcam đè). Trong mặt màn hình chỉ tìm **webcam có
  khung** (bốn đường quanh mảng động, cho phép sát mép, chẻ khối dính hoạt hình kề;
  loại mảng mặt số/ít động). Đường cũ giữ quyền khi nó ra bố cục ghép tin cậy hoặc ô có
  người đặc — bộ Framing v2 và podcast không đổi. Bộ tổng hợp 32 ca: ô IoU 0,62 → 0,95,
  kiểu đúng 0,53 → 0,83, sai nguồn 0,145 → 0,049. Lề cùng màu nền trội là mặt, không
  phải viền (slide trắng không mất lề trắng).
- `1.3.1` (07/10/2026): khe trang trí chỉ giữa hai ô KỀ NHAU — hai ô cách nhau một ô
  (hàng ba cột, hàng dưới căn giữa) từng sinh "khe" đè trọn ô giữa, bộ lập kế hoạch
  khoét mất ô ấy (`decor-in-frame` ở mọi khung cột giữa Zoom 3×3).
- `1.3.0` (07/10/2026): **lưới hội họp** (Zoom/Meet/Teams) đi cửa riêng `gallery.js`
  (D-F2-20): giả thuyết lưới cột×hàng từ hộp sáng trên nền tối, kiểm bằng chứng ở
  ranh lưới dự đoán, cắt từng hàng theo dải sáng (hàng cuối căn giữa), gọt pillarbox
  trong ô; ô lưới luôn là người (camera/static), tin cậy theo bằng chứng ranh. Zoom
  8 người thật: trước là MỘT "màn hình" hai hàng, nay 3×3 → 8 ô đúng, tin cậy 1,00.
- `1.2.0` (07/10/2026): mép ô dừng **trước** vạch khung (D-F2-19): lùi qua hết dải
  vạch khi vầng động liếm vào sườn trong; dải viền chuyển sắc/vạch mảnh là nền chết
  (làn phẳng), không phải nội dung; dải mỏng toàn làn phẳng kề banner có logo là
  vạch, ô dừng ở đường. 20VC: hộp hai ô nằm trọn trong vạch trắng, tin cậy 0,60 → 1,00.
- `1.1.0` (07/10/2026): độ chết của khe giữa hai ô đo trên **làn nền** (các làn
  phẳng cùng độ sáng), không đo cả hộp khe. Viền sáng kề nội dung tối nhấp nháy
  theo nén video (20VC: cột viền động 0,51, nền động 0) từng làm hai camera bị
  gọi là một ô rộng suốt 20 giây.
- `1.0.0`: bản đầu.

# Actor input Schema

## `op` (type: `string`):

layout = sample the video and return panels / decor / layout states over time. capabilities = return supported ops, features and the threshold policy without running any work (no source needed).

## `source` (type: `string`):

https:// URL, or kv:<storeId>/<key> from an earlier run (e.g. the 480p proxy from video-audio-toolkit — the probe analyses at 480 px wide anyway, so the proxy is the cheapest input).

## `options` (type: `object`):

{ start_sec, duration_sec } bound a shard (timestamps in the result are shifted back onto the source timeline); sample_fps (1–4, default 2); analysis_width (320–640, default 480).

## `output` (type: `object`):

{ signed_upload_url } to PUT the layout JSON straight into your own storage.

## `cleanup` (type: `string`):

on_success = keep only the layout artifact in the run's key-value store. always = also drop it after the signed upload. off = keep everything for debugging.

## `callback` (type: `object`):

{ url, secret_header: { name, value } } — the run result JSON is POSTed there when the run ends.

## Actor input object example

```json
{
  "op": "capabilities",
  "options": {
    "sample_fps": 2,
    "analysis_width": 480
  },
  "cleanup": "on_success"
}
```

# Actor output Schema

## `results` (type: `string`):

Run result JSON: status, op, artifacts \[{name, kv_key, url, bytes}], meta (states summary, counts, composite flag), timings, errors.

## `resultRecord` (type: `string`):

The same result JSON stored as the RESULT record of the default key-value store.

## `layout` (type: `string`):

SourceLayoutDoc v1: probe, source, states\[], changes\[], stats.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "op": "capabilities",
    "options": {
        "sample_fps": 2,
        "analysis_width": 480
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("andrew_babo/source-layout").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "op": "capabilities",
    "options": {
        "sample_fps": 2,
        "analysis_width": 480,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("andrew_babo/source-layout").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "op": "capabilities",
  "options": {
    "sample_fps": 2,
    "analysis_width": 480
  }
}' |
apify call andrew_babo/source-layout --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,andrew_babo/source-layout"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Mcwsl0wL4EABqjyQh/builds/wYlWNhultXQoAOojG/openapi.json
