# CremyX Vision (`andrew_babo/cremyx-vision`) Actor

Face detection + tracking (SCRFD ONNX), torch-free active-speaker detection (mouth motion x audio envelope) and auto-reframe: returns normalized 9:16 crop keyframes ready for an editplan.

- **URL**: https://apify.com/andrew\_babo/cremyx-vision.md
- **Developed by:** [Andrew Babo](https://apify.com/andrew_babo) (community)
- **Categories:** AI, Videos
- **Stats:** 1,250 total users, 747 monthly users, 96.2% 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## cremyx-vision

Nhận diện + bám khuôn mặt (SCRFD ONNX), **active-speaker detection** (ai đang
nói — không cần torch) và **auto-reframe**: trả về crop keyframes chuẩn hoá,
dán thẳng vào editplan của `cremyx-render-engine`.
Thay cho việc bắt user cài torch/UniFace trên máy.

- Base image: `node:22-bookworm-slim` (onnxruntime-node là binary glibc)
- Model nướng sẵn: SCRFD-10g (`antelopev2/detection`, ~17 MB) — không tải weight lúc chạy

### Input

```jsonc
{
  "op": "reframe",                    // faces | reframe | asd
  "source": "kv:<storeId>/proxy.mp4", // hoặc https://…
  "options": {
    "sample_fps": 2,                  // số frame phân tích mỗi giây (tối đa 10; mặc định 8 cho asd)
    "score_threshold": 0.5,
    "start_sec": 0,
    "duration_sec": null,
    "threads": null,
    "track_iou": 0.3,                 // ghép detection thành track
    "track_max_gap_ms": 700,          // mất mặt lâu hơn ngần này thì cắt track
    "min_track_ms": 400,
    "include_frames": false,          // kèm box từng frame vào artifact

    // chỉ dùng cho op=reframe (và asd khi muốn crop theo người nói)
    "target_aspect": "9:16",
    "track_id": null,                 // mặc định: track "nổi bật nhất" (bị bỏ qua khi asd có segments)
    "smooth_window": 5,               // median filter, lớn hơn = camera ì hơn
    "dead_zone": 0.02,                // bỏ rung nhỏ hơn 2% chiều khung
    "headroom": 0.12,                 // đẩy mặt lên trên tâm một chút
    "interp": "linear",               // linear | hold

    // chỉ dùng cho op=asd
    "silence_ratio": 0.25,            // RMS < 25% peak coi như im lặng
    "min_segment_ms": 600,            // đoạn nói ngắn hơn ngần này bị gộp/bỏ (chống giật)
    "min_correlation": 0.05           // track có score thấp hơn coi như "không bao giờ nói"
  },
  "output": { "signed_upload_url": null },
  "cleanup": "on_success",
  "callback": null
}
```

**Nên phân tích trên bản proxy 480p** (`op=proxy` của `cremyx-media-tools`) —
nhanh hơn nhiều lần và kết quả chuẩn hoá theo tỉ lệ nên áp lại lên bản master
vẫn đúng.

### Output

```jsonc
{
  "status": "success",
  "op": "reframe",
  "artifacts": [{ "name": "reframe", "kv_key": "reframe.json" }],
  "meta": {
    "engine": "scrfd-onnx",
    "width": 1280, "height": 720, "duration_sec": 61.4,
    "sample_fps": 2, "frame_count": 123, "face_frame_ratio": 0.951,
    "track_count": 2,
    "tracks": [
      { "id": "face_01", "start_ms": 0, "end_ms": 58000, "duration_ms": 58000,
        "sample_count": 112, "avg_area_px": 20480, "avg_score": 0.912 }
    ],
    "reframe": {
      "target_aspect": 0.5625,
      "crop_px": { "w": 405, "h": 720 },
      "track_id": "face_01",
      "face_coverage": 0.951,
      "keyframes": [{ "t_ms": 0, "rect": { "x": 0.31, "y": 0, "w": 0.316, "h": 1 }, "interp": "linear" }]
    }
  }
}
```

- `tracks[].samples` (box từng thời điểm) nằm trong artifact JSON, không nằm trong
  dòng dataset cho nhẹ.
- `rect` là **toạ độ chuẩn hoá 0..1** so với khung nguồn → không phụ thuộc độ phân giải.

### Dùng kết quả để render dọc

`reframe.keyframes` khớp đúng schema `crop.keyframes` của render engine:

```jsonc
{
  "mode": "editplan",
  "plan": {
    "canvas": { "width": 1080, "height": 1920, "fps": 30 },
    "tracks": [{
      "type": "media",
      "clips": [{
        "source": "https://…/master.mp4",
        "start_ms": 0, "end_ms": 61400,
        "layout": "single-center",
        "crop": { "keyframes": [ /* dán nguyên reframe.keyframes vào đây */ ] }
      }]
    }]
  }
}
```

### op=asd — ai đang nói?

Active-speaker detection theo hướng **không cần torch/LR-ASD**: với mỗi face
track, actor đo chuyển động vùng miệng theo thời gian rồi đối chiếu với envelope
âm lượng của audio. Miệng cử động *đúng lúc có tiếng* → người đó đang nói.

```jsonc
// meta.asd
{
  "segments": [
    { "track_id": "face_01", "start_ms": 0, "end_ms": 4000 },
    { "track_id": "face_02", "start_ms": 4000, "end_ms": 8000 }
  ],
  "track_scores": [
    { "track_id": "face_01", "speech_score": 0.41, "speaking_ms": 4000 },
    { "track_id": "face_02", "speech_score": 0.41, "speaking_ms": 4000 }
  ],
  "speech_ratio": 1.0,
  "note": null
}
```

- `segments` — khoảng thời gian từng người nói (đã qua hysteresis `min_segment_ms`).
- `track_scores[].speaking_ms` — tổng thời gian nói ước lượng của mỗi track.
- Truyền thêm `target_aspect` (vd `"9:16"`) → `meta.reframe.keyframes` sẽ **bám
  theo người đang nói** thay vì một track cố định (`track_id` lúc này là
  `"active_speaker"`). Dán keyframes vào editplan của render engine như thường.
- Kết hợp `cremyx-diarize` khi cần đối chiếu theo giọng nói (audio-only) —
  hai tín hiệu bổ sung nhau: diarize không cần nhìn mặt, asd không cần tách giọng.

### Giới hạn cần biết

- ASD dạng này cần **nhìn thấy miệng**: người quay lưng, che miệng, hoặc video
  không có mặt người thì không kết luận được (trả `note` giải thích). Với
  podcast phỏng vấn/talking-head quay chính diện thì hoạt động tốt.
- `sample_fps` cao làm chi phí tăng tuyến tính; 2 fps là đủ cho reframe mượt
  (đã có smoothing + nội suy), 8 fps mặc định cho asd vì miệng cử động nhanh.
- Không phát hiện được mặt che khuất/quay lưng — những đoạn đó dùng khung của
  keyframe gần nhất.

### Ví dụ nối chuỗi

```js
const proxy = await tools.mediaTools({ op: 'proxy', source: masterUrl, options: { height: 480 } });
const vis   = await tools.vision({ op: 'reframe', source: proxy.kv('proxy'), options: { target_aspect: '9:16' } });
plan.tracks[0].clips[0].crop = { keyframes: vis.meta.reframe.keyframes };
```

### Shard window (v2)

`options.start_sec` / `duration_sec`: mọi timestamp trong kết quả (`frames[].time_ms`, `tracks[].start_ms/end_ms`, `samples[].time_ms`, `asd.segments[]`, `reframe.keyframes[].t_ms`) đều được offset về timeline gốc; `meta.shard.offset_applied: true`.

Memory mặc định run: 16 GB (4 vCPU); có thể override khi start run nhưng 16 GB là cấu hình khuyến nghị cho ASD.

### Chạy nhanh op=asd (v0.3)

ASD chậm không phải vì thuật toán mà vì **detect mọi frame ở 640×640**. Bản này
tách hai nhịp: `sample_fps` (nhịp đọc miệng) và `detect_fps` (nhịp thật sự chạy
detector — box giữa hai lần detect được nội suy).

Mặc định mới cho `asd`: `sample_fps=8`, `detect_fps=2`, `det_size=480`,
`batch_size=8`, dùng **toàn bộ vCPU** → giảm ~4x số lần inference.

Thứ tự tối ưu (nhanh nhất → chậm nhất):

1. **Proxy 480p trước.** `cremyx-media-tools` `op: "proxy"` → lấy
   `artifacts[].url` làm `source` cho vision. Đừng phân tích bản master.
2. **Tái dùng faces.** Chạy `op: "faces"` một lần, rồi truyền URL công khai của
   kết quả vào `options.faces_url` khi chạy `asd` → **bỏ hẳn** bước detect,
   track id giữ nguyên giữa hai run.
3. **Chỉ chấm điểm chỗ có tiếng.** Truyền `options.words_url` (kết quả
   `cremyx-asr`) hoặc `options.vad_url` → phần im lặng không tính mouth motion.

```jsonc
{
  "op": "asd",
  "source": "https://…/proxy.mp4",          // 480p proxy, KHÔNG phải master
  "options": {
    "start_sec": 0, "duration_sec": 120,     // client tự chia shard
    "faces_url": "https://…/faces.json",     // tuỳ chọn
    "words_url": "https://…/asr.json",       // tuỳ chọn
    "det_size": 480, "detect_fps": 2, "sample_fps": 8, "batch_size": 8,
    "target_aspect": "9:16"
  }
}
```

#### Nguyên nhân thật của "chậm 10–12x" (đo được)

`os.cpus()` trong container Apify trả về **số core của máy chủ (16)**, không phải
quota thật của run. Actor cũ set `intraOpNumThreads` theo con số đó → 16 luồng
ONNX giành nhau 2 core → mỗi frame ~0.7–1.0s. Bản 0.4 tính vCPU theo
`ACTOR_MEMORY_MBYTES / 4096` (đúng quy tắc Apify: 1 vCPU / 4 GB).

Đo trên cùng một video, 30 frame detect, `det_size=320`:

| Bản | Memory | threads | detect | ms/frame |
|---|---|---|---|---|
| 0.3 (os.cpus) | 8 GB | 16 | 23.8s | ~790 ms |
| 0.3 (os.cpus) | 8 GB | 8 | 20.7s | ~690 ms |
| **0.4** | **16 GB** | **4** | **1.02s** | **~34 ms** |

→ nhanh hơn ~20x. Với shard 120s, `detect_fps=2` (240 frame) phần detect chỉ còn
khoảng **8–10 giây**; thời gian còn lại là decode + mouth motion.

**Chạy với 16384 MB (4 vCPU)** cho `op=asd`. 8192 MB vẫn chạy nhưng chậm hơn ~2x;
4096 MB (1 vCPU) thì đừng dùng — log sẽ cảnh báo.

`meta` trả thêm để đối chiếu: `detect_fps`, `det_size`, `detect_frames`,
`batch_size`, `batched`, `threads`, `vcpus`, `faces_url_used`, `speech_gated`,
và `timings.decode_ms` / `detect_ms` / `mouth_ms` để biết nút thắt nằm ở đâu.

Actor **không tự chia shard**: client vẫn tự fan-out bằng `start_sec` /
`duration_sec` và tự khử vùng overlap.

### Reframe quality (v0.7) — sửa A-B IoU 0.54

Ba nguyên nhân làm crop cloud lệch so với local, đã sửa hết trong 0.7:

1. **Tracker vỡ track.** Matching cũ chỉ dùng IoU giữa 2 frame liên tiếp; ở
   2-4 fps, mặt di chuyển hơn 1 bề rộng box là IoU về 0 → một người bị tách
   thành nhiều track, crop nhảy sang người khác. Nay matching dùng **box dự
   đoán theo vận tốc** + fallback **khoảng cách tâm / tỉ lệ kích thước**, và
   track sống lâu được ưu tiên nhận box trước.
2. **Không có định danh bám dính.** `reframeKeyframes` trước đây fallback về
   "mặt to nhất mỗi frame" → mặt phụ to hơn sẽ cướp khung. Nay thứ tự ưu tiên
   là `active speaker > track_id ép > track đang bám > mặt to nhất`, giữ khung
   qua khoảng mất nhận diện ngắn (`sticky_hold_ms`, mặc định 600 ms). Mọi
   keyframe đều mang `track_id` thật; kết quả có thêm `track_ids` và `params`.
3. **Cadence + smoothing quá thưa.** Mặc định mới: `sample_fps=4` (cho
   faces/reframe), `keyframe_fps=4`, `dead_zone=0.006`; cửa sổ median được
   **căn giữa** (bản cũ lệch trái nên crop trễ ở cuối mỗi shard).

Đo A-B trên fixture tổng hợp (1 người di chuyển nhanh + 1 mặt tĩnh to hơn,
30 s, so với crop lý tưởng từ ground truth):

| Cấu hình | tracks | keyframes | mean IoU |
|---|---|---|---|
| cũ (sample 2 / kf 1 / dz 0.02, tracker IoU) | 2 (vỡ) | 44 | **0.344** |
| 0.7, sample 4 / kf 2 / dz 0.006 | 1 | 111 | **0.994** |
| 0.7 mặc định, sample 4 / kf 4 / dz 0.006 | 1 | 120 | **0.994** |

Tuỳ chọn liên quan: `sample_fps`, `keyframe_fps`, `dead_zone`
(alias `deadzone_ratio`), `smooth_window` (alias `smooth_window_ms`),
`sticky_hold_ms`, `track_id`, `min_keyframes`, `asd_url`, `faces_url`.
`reason` của keyframe: `initial | face_move | speaker_switch | cadence |
min_keyframes | fallback_center`.

### op=asd — điều kiện đạt ≤0.05x realtime (v0.5)

Con số ≤0.05x realtime chỉ đúng khi chạy đúng fast path:

1. Nguồn là **proxy 480p** do `cremyx-media-tools op=proxy` tạo (không đưa file gốc 1080p/4K).
2. Truyền `options.faces_url` (kết quả `op=faces` đã có) để ASD **bỏ hẳn bước detect**.
3. Không ép `sample_fps` > 8; mặc định 8 với `detect_fps` 2 là điểm cân bằng.
4. Nếu có `options.words_url`, ASD chỉ decode các khoảng có thoại (speech gating).

Thiếu bước 1 hoặc 2 thì ASD phải decode/detect lại và thời gian tăng nhiều lần — đó là hành vi đúng, không phải hồi quy.

`op=capabilities` trả `memory_mb`, `vcpus`, `host_cpus` và `asd_fast_path.requires` để phía gọi tự kiểm tra cấu hình trước khi đo.

# Actor input Schema

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

faces = detect + track faces. reframe = the same, plus normalized crop keyframes for a target aspect ratio (editplan-ready). asd = active-speaker detection (mouth motion x audio envelope, no torch needed): per-track speech scores + speaking segments; pass target\_aspect to also get crop keyframes that follow whoever is speaking. diarize = visual diarization: the same ASD pass, returned as speaker turns (spk\_<track>) with speaker\_count/coverage/degraded — use it when audio diarization returns 0 speakers. capabilities = return supported ops/features 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 cremyx-media-tools — analysing the proxy is much cheaper than the master).

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

Vision options include sampling/detection controls plus embeddings (default true) and embedding\_samples (default 5, max 8). Face embeddings are 512-d normalized ArcFace vectors computed only from representative aligned samples, without a second video decode.

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

{ signed\_upload\_url } to PUT the result JSON straight into your own storage.

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

on\_success = keep only result artifacts in the run's key-value store. always = also drop them after the signed upload. off = keep everything for debugging.

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

{ url, secret\_header: { name, value } } — POSTed with the result JSON when the run finishes.

## Actor input object example

```json
{
  "op": "faces",
  "cleanup": "on_success"
}
```

# Actor output Schema

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

Full run result JSON: status, op, artifacts \[{name, kv\_key, url, bytes}], meta (tracks, ASD segments, crop keyframes), timings, errors.

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

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

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("andrew_babo/cremyx-vision").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("andrew_babo/cremyx-vision").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 '{}' |
apify call andrew_babo/cremyx-vision --silent --output-dataset

```

## MCP server setup

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

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/dUNxVyhAnukQZICjx/builds/qocPJQA5NOx8hEAJs/openapi.json
