An open source, agent-first Rust CLI for importing short-form source material, managing a local asset library, and composing vertical clips without touching a timeline.
The honest minimum viable truth is fresh input in, finished clip out.
Given a trending sound URL and one or more source clip URLs, the CLI imports, normalizes, trims, scales, center-crops, concatenates, and muxes them into a final MP4 — reliably, locally, and with real bytes end-to-end.
Discovery of trending material exists in the codebase but is scoped down in the docs: every official path is gated by an external API (TikTok Research, X/Twitter v2 search) that is either hard to obtain or paywalled, and the unauthenticated fallbacks are brittle by design. Treat discovery as an optional convenience on top of the manual-URL spine, not the spine itself.
What's solid today:
- importing sounds and clips from supported URLs into a local library
- composing one final vertical MP4 from one sound and one or more clips
- loudness normalization presets for social, viral, podcast, broadcast
- structured JSON output on stdout; progress logs on stderr
- committed demo library assets so
composeworks immediately after clone - end-to-end integration test that exercises import → compose with real media
Build and verify dependencies:
cargo build --release
./target/release/capcut-cli deps check
# If yt-dlp is missing, install the standalone binary into ~/.capcut-cli/bin
./target/release/capcut-cli deps installRun the primary flow — import one sound URL plus one or more clip URLs, then compose:
# 1. Import a trending audio source (TikTok music, YouTube, Instagram, X)
./target/release/capcut-cli library import \
"https://www.tiktok.com/music/<slug>-<id>" --type sound --tags trending
# 2. Import one or more source clips
./target/release/capcut-cli library import \
"https://x.com/<user>/status/<id>" --type clip --tags source
# 3. Compose a finished vertical MP4
./target/release/capcut-cli compose \
--sound <sound_id> --clip <clip_id> \
--duration 15 --resolution 1080x1920 --loudness viralThe CLI writes to library/output/comp_<job_id>/final.mp4 unless
--output is supplied. Asset IDs are returned in each import's JSON envelope
(.data.id).
A batch script that wraps the above for three clips at once is documented below under Batch: three finished clips.
- Rust toolchain to build the crate
ffmpegonPATH(or at~/.capcut-cli/bin/ffmpeg)yt-dlpat~/.capcut-cli/bin/yt-dlp(the CLI installs this itself viadeps install, no other runtime needed)
On macOS, brew install ffmpeg is the simplest way to satisfy ffmpeg.
scripts/build-clips-from-urls.sh takes one supplied sound URL plus three
supplied clip URLs and produces a self-contained clips/ folder:
clip_1.mp4,clip_2.mp4,clip_3.mp4— finished vertical MP4ssource_sound.<ext>— the imported audio used by all threesource_1.<ext>,source_2.<ext>,source_3.<ext>— the imported clipsmanifest.json— provenance (the supplied URLs and compose settings)
Local invocation:
SOUND_URL="https://..." \
CLIP_URLS="https://url1 https://url2 https://url3" \
./scripts/build-clips-from-urls.sh- Open Actions → build-clips → Run workflow in the GitHub mobile app.
- Leave
modeaturls(the default). - Paste
sound_url,clip_url_1,clip_url_2,clip_url_3. Tweakdurationandresolutionif desired. - When the run finishes, download the
clipsartifact.
- Open a Codespace on this repo (the devcontainer builds the CLI and installs ffmpeg + yt-dlp).
- Run:
SOUND_URL="..." CLIP_URLS="... ... ..." make clips
- The
clips/folder is in the workspace; grab it from the file browser.
Manage runtime dependencies.
cargo run --release -- deps check
cargo run --release -- deps installdeps check returns structured JSON describing whether ffmpeg and yt-dlp
are installed. deps install downloads the standalone yt-dlp binary from
the upstream GitHub release for macOS and Linux.
Manage local media assets stored under library/.
# Import from a supported URL
./target/release/capcut-cli library import \
"https://www.tiktok.com/music/..." --type sound --tags trending,tiktok
./target/release/capcut-cli library import \
"https://x.com/user/status/123" --type clip --tags source
# Inspect the library
./target/release/capcut-cli library list
./target/release/capcut-cli library list --type sound
./target/release/capcut-cli library show snd_demo001
# Remove an asset
./target/release/capcut-cli library delete snd_demo001Import behavior:
--typeis optional; TikTok/music/URLs are auto-detected as sounds, everything else defaults to clip- sounds are downloaded with
yt-dlp, converted to MP3, and stored underlibrary/sounds/assets/<asset_id>/ - clips are downloaded with
yt-dlpand stored underlibrary/clips/<asset_id>/ - imported assets are indexed in
library/manifest.json - X/Twitter imports use authenticated browser cookies via
yt-dlp --cookies-from-browserand emit distinct structured error codes for missing auth, suspended tweets, missing video media, unavailable video, and rate limiting
Supported source platforms detected by the downloader:
- TikTok
- X/Twitter
- YouTube
Render one final MP4 from one sound plus one or more clips.
./target/release/capcut-cli compose \
--sound snd_demo001 \
--clip clp_demo001 \
--duration 20 \
--resolution 1080x1920 \
--loudness viralOptions:
--sound <ID>: required sound asset ID--clip <ID>: required, repeatable clip asset ID--duration <SECONDS>: output duration, default30--output <PATH>: optional explicit output path--resolution <WxH>: default1080x1920--loudness <PRESET|LUFS>: preset or numeric LUFS value
Built-in loudness presets:
viral:-8 LUFSsocial:-10 LUFSpodcast:-14 LUFSbroadcast:-23 LUFS
Compose pipeline:
- normalize the chosen sound with
ffmpegloudness normalization - trim audio to the requested duration
- trim each clip to its segment duration
- scale and center-crop clips to the requested resolution
- concatenate clips and mux AAC audio into the final MP4
If --output is omitted, the CLI writes to
library/output/comp_<job_id>/final.mp4.
⚠️ These commands depend on external APIs that are hard to obtain or paywalled, and public fallbacks are brittle. Use them as a convenience on top of the manual-URL spine, not as the primary path.
- TikTok Research API (
TIKTOK_RESEARCH_ACCESS_TOKEN): restricted to academic researchers at non-profit institutions; commercial applicants are routinely rejected and approval takes weeks. Unauthenticated Creative Center scraping exists as a fallback but is frequently degraded upstream. - X/Twitter API (
TWITTER_BEARER_TOKEN): the recent-search endpoint this CLI uses is not on the Free tier. Minimum is Basic at $200/month.
If you have the tokens:
export TIKTOK_RESEARCH_ACCESS_TOKEN=...
export TWITTER_BEARER_TOKEN=...
./target/release/capcut-cli discover tiktok-sounds --limit 5 --region US --window-days 7
./target/release/capcut-cli discover x-clips --query "ai agents" --limit 5 --min-likes 1000
# Or end-to-end:
./target/release/capcut-cli autopilot --query "ai agents" --duration 15The discovery-mode batch path is also available in the Actions workflow by
setting mode: discovery and adding both tokens as repo secrets. Downloads
from Actions may be rate-limited or blocked on data-center IPs even when
discovery succeeds — this is why the manual-URL path is the recommended one.
Every successful command prints a structured JSON envelope to stdout. Progress logs go to stderr.
Example:
{
"status": "ok",
"command": "library list",
"data": {
"count": 2,
"assets": []
},
"errors": [],
"meta": {
"version": "0.1.0",
"duration_ms": 2
}
}Behavior guarantees:
- stdout is machine-readable JSON
- stderr is for human-readable progress messages
- success exits with code
0 deps checkexits with code2when dependencies are missing- all imported asset paths and compose output paths are emitted as absolute paths
- structured error codes distinguish setup failures from media/data failures on X/Twitter
TWITTER_BEARER_TOKENandTIKTOK_RESEARCH_ACCESS_TOKENare only read from the environment at runtime; the CLI does not persist them in repo files or library manifests.- X media import uses
yt-dlp --cookies-from-browser, which reads your local browser session instead of asking you to paste cookie values into the repo. - command logs redact token-like query parameters and signed URL fragments before printing to stderr.
- imported asset metadata strips token-like query parameters before saving
source_urlintolibrary/manifest.json. .env,.env.*, and*.localare ignored by git so local credential files are less likely to be committed accidentally.- copy
.env.exampleto.envif you want a local template for the supported variables. - prefer a dedicated low-scope X API token for this tool and avoid sharing terminals or log captures from authenticated runs.
- see SECURITY.md for the operational checklist we recommend before using real API tokens.
src/
cli.rs # clap command tree and dispatch
config.rs # paths, version, loudness presets
deps.rs # ffmpeg checks and yt-dlp installation
discover/
tiktok.rs # TikTok discovery (API-gated, optional)
twitter.rs # X/Twitter discovery (API-gated, optional)
library.rs # import/list/show/delete asset workflow
media/
compose.rs # end-to-end composition pipeline
downloader.rs # yt-dlp integration
ffmpeg.rs # ffmpeg wrappers
models.rs # asset and compose result models
output.rs # JSON envelope helpers
library/
manifest.json # imported asset index used by the CLI
sounds/ # sound assets and committed seed media
clips/ # imported clip assets
output/ # composed videos
scripts/
build-clips-from-urls.sh # primary: compose 3 clips from supplied URLs
build-clips.sh # optional: discovery-driven batch
tests/
e2e_url_to_clip.rs # end-to-end import → compose smoke test
library/manifest.json references two small committed fixtures so compose
works immediately on a freshly cloned repo:
snd_demo001— 2-second 440 Hz sine tone atlibrary/sounds/assets/snd_demo001/audio.mp3clp_demo001— 3-second solid-color vertical MP4 atlibrary/clips/clp_demo001/video.mp4
These are synthetic, not "trending" — they exist so the compose pipeline is inspectable without network access. For real trending material, use the manual-URL import flow.
Run the full Rust test suite with:
cargo test --all-targetsCoverage currently includes:
- X clip scoring and guided-fallback labeling
- TikTok
import_urlnormalization - downloader error classification for X auth/media failures
- import metadata enrichment for TikTok embeds
- loudness preset resolution
- numeric loudness parsing
- duration parsing in the ffmpeg helpers
- a compose smoke test over the committed demo assets
- an end-to-end integration test (
tests/e2e_url_to_clip.rs) that exercises the full import-from-URL → compose spine via a yt-dlp shim, so the honest minimum viable truth is verifiable in CI
The test GitHub Actions workflow runs cargo test --all-targets on every
push.