What it is
ARCH‑ANGEL takes a long video — a podcast, a stream VOD, a lecture, a local file — and produces vertical short-form clips from the moments most likely to hold attention: fetching, transcribing, choosing moments, cutting, captioning, reframing, grading, scoring and (optionally) publishing.
There are two ways to get it:
- The hosted workshop at
YOUR-DOMAIN.com— nothing to install. Your video is processed on our servers; see the privacy policy for exactly what that means. Accounts are created by an administrator. - Self-hosted — the same software on your own machine or server, where nothing ever leaves your hardware. See Self-hosting.
And two ways to drive it:
- The Studio — the multi-user web app: accounts, a clip editor, channel monitoring and publishing.
- The CLI — one command, good for batches and scripting. Self-hosted only.
output/<project>/, each with a matching
.json sidecar holding its title, description, score and source
timestamps. Everything else in the app is a way of shaping or re-shaping
that job.
Using the hosted workshop
Nothing to install. Go to YOUR-DOMAIN.com, sign in, and skip
straight to the Studio tab.
- Getting an account — there is no public sign-up. An administrator creates each account and gives you the credentials. Ask for access.
- What is stored — your uploads, transcripts, clips, settings and any channel tokens live on our servers. The privacy policy itemises all of it.
- Fair use — renders are queued one at a time per instance, so be reasonable with batch sizes. Limits may apply to upload size and volume.
- Keep your own copies. Download anything you care about. The service is a workshop, not a backup.
- No CLI — the command line is for self-hosted installs only.
Everything from the Studio onwards applies to you. You can skip Requirements, Install and Deployment entirely.
Self-hosting
The hosted workshop and the self-hosted software are the same program. Running your own gets you three things: your footage never touches anyone else's disk, there are no limits but your hardware, and nobody can take it away from you.
On a laptop, for a look around:
pip install -r requirements.txt
python dashboard.py # -> http://localhost:8000
On a server, on your own domain, with HTTPS:
cp .env.example .env # set PUBLIC_BASE_URL + SUPERADMIN_CODE
docker compose up -d --build
Read Requirements and Install
next, then Deployment when you want it public.
DEPLOY.md in the repository is the full walkthrough.
Requirements self-hosted
| Thing | Notes |
|---|---|
| Python 3.10+ | Required. 3.11+ recommended. |
| ffmpeg | A Windows build ships in .bin/. On macOS/Linux install it and put it on your PATH. |
| Disk | Sources are downloaded before clipping. A 1-hour 1080p video is roughly 1–2 GB. |
| GPU | Optional. CUDA makes transcription several times faster; CPU works with smaller whisper models. |
| Ollama | Optional, only for the local-LLM picker. |
| Google libs | Optional, only for YouTube upload: google-api-python-client, google-auth-oauthlib. |
Install
git clone <your-copy-of-arch-angel>
cd clipper
pip install -r requirements.txt
For one-click YouTube publishing, add the Google client libraries:
pip install google-api-python-client google-auth-oauthlib
Check ffmpeg is reachable:
ffmpeg -version
On Windows the bundled binary in .bin/ is used automatically if
ffmpeg isn't on your PATH.
Quick start
From the command line
# five shorts from a URL
python main.py "https://youtube.com/watch?v=..." --clips 5
# from a local file
python main.py --file lecture.mp4 --clips 3
# local LLM picking, with music and meme SFX
python main.py URL --picker ollama --music --sfx
# just tell me which moments you'd pick — render nothing
python main.py URL --dry-run
Clips land in output/<project>/. Each .mp4 has a
sibling .json with its metadata.
From the Studio
python dashboard.py # -> http://localhost:8000
Open the URL and you'll land on the sign-in page.
First run & the super admin
The very first time you start the server there are no accounts, so the login page shows a Create super admin form instead. It asks for a username, a password, and the super-admin code.
- The code comes from
SUPERADMIN_CODEin your.env. - If you haven't set one, the default is
ARCH-ANGEL-2026. - Change
.envbefore you create the first account — the code is re-read from the environment right up until an account exists. - After setup, rotate it in Users → Super-Admin Code.
Accounts & roles
There is no open sign-up. A super admin creates every account.
| Role | Can do | Cannot do |
|---|---|---|
| Super admin | Everything: API keys and providers, the YouTube app credential, user management, clearing the error log. | — |
| User | Render, edit, use the media library, connect their own YouTube channels, track channels, publish. | Touch the API section, manage users, or see another user's channels. |
Sessions are signed cookies (HttpOnly, SameSite=Lax, and Secure
automatically over HTTPS) valid for 14 days, stored in
config/sessions.json so a restart doesn't sign everyone out.
The Studio tab
Where jobs start. Paste a URL or drop a file, then tune:
- Picker — which backend chooses the moments (see Pickers).
- Ensemble — tick two or more pickers and their agreement ranks moments higher.
- Whisper model —
tiny→large-v3. The Fast/Balanced/Quality/Max preset chips just set this. - Clips — how many to keep.
- Music — on/off, a specific track or auto mood-match, volume, and swells.
- SFX — automatic meme stings.
Captions use whatever you designed in Subtitle Studio, and motion effects come from your Effects selection — both are global choices that apply to Studio and Enhance renders.
Progress streams live underneath, and the log is mirrored into Telemetry.
Enhance
Takes a clip you already have — dropped in or by URL — and re-runs only the finishing stages: captions, effects, music, reframe, trim. Use it when the cut is right but the dressing isn't. For fine control over a single clip, the clip editor is usually the better tool.
Effects
A gallery of motion and overlay effects grouped by category — zoom, motion, transition, glitch, colour, overlay. Selecting effects here stores them in your browser and applies them to Studio renders, Enhance runs and the clip editor. The count shows in the editor so you always know what's armed.
Subtitle studio
Design the caption look once; every render uses it.
| Control | Effect |
|---|---|
| Animation | pop, box, karaoke, bounce, minimal |
| Position | top / middle / bottom |
| Font | Arial, Impact, Montserrat, Bebas Neue, Poppins |
| Uppercase | Force caps on or off |
| Text colour | Main caption colour |
| Highlight | The active/emphasised word |
The preview panel reacts to all six live. Colours are converted to ASS
subtitle format (&H00BBGGRR) when the render runs — you never
have to think about that.
Media library
Upload your own background music (MP3, M4A, WAV, OGG) and sound-effect stings. Uploads appear immediately in every track picker — Studio, Enhance, the clip editor and per-channel profiles. You can play them in place, delete them, and drag the grip handle to reorder; the order is remembered in your browser.
Clips
Every finished short, newest first, with its score and file size. Hover a tile to preview it. From here you can open the editor or publish to YouTube, TikTok or Instagram.
The clip editor
Click any clip to open a non-destructive editor. Adjusting anything and pressing Re-render produces a new file; the original is left alone.
Timeline
- A real audio waveform with a draggable trim region — grab an edge to move one end, or the middle to slide the whole selection.
- Space plays and pauses. I and O set the in and out points to the playhead.
- Loop plays just the trimmed region on repeat.
- The numeric Start/End fields stay in sync both ways — they are what the render actually uses.
Live preview
The picture updates as you drag: brightness, contrast, saturation, gamma, sharpen and all preset looks are approximated with CSS filters; grain and vignette render as overlays; fades dim the frame near the trimmed edges; captions and the watermark show in position.
What you can change
| Group | Controls |
|---|---|
| Trim | Start, end (seconds) |
| Transform | Reframe (crop 9:16 / blur pad), resolution, speed, fade in/out |
| Look | Preset look, brightness, contrast, saturation, gamma, grain, sharpen, vignette |
| Audio | Background music on/off, track, volume, meme SFX |
| Captions | On/off (uses your Subtitle Studio design) plus armed effects |
| Watermark | Text, position, opacity |
Analytics
Computed live from your own output, not from any external service: clips per day for the last fortnight, a breakdown by source project, the score distribution, and cumulative library growth — plus totals for clip count, average score, average length and library size on disk.
Telemetry & the error log
Telemetry shows the live pipeline status and streaming log while a job runs.
Below it, the error log is persistent and always worth checking when something goes quiet. It collects:
- Render failures, with the last lines of pipeline output;
- YouTube connect and upload failures, with the exact provider error;
- Unhandled server errors, with a traceback;
- Browser-side errors and unhandled promise rejections.
Each entry expands to show detail. Super admins can clear the log.
Settings
- Appearance — follow system, light or dark, plus an accent colour. The choice is shared with this website.
- Account — change your password; enter the super-admin code to promote yourself.
- API providers (super admin) — cloud LLM keys and models.
- YouTube app credentials (super admin) — upload the Google OAuth client JSON and read the exact redirect URI to register.
Keyboard shortcuts
| Keys | Does |
|---|---|
| Ctrl/⌘ + K | Command palette — jump to any tab, run a command |
| G then S | Go to Studio |
| G then C | Go to Clips |
| G then A | Go to Analytics |
| ? | Show the shortcut hint |
| Space | Play / pause (clip editor) |
| I / O | Set trim in / out (clip editor) |
| Esc | Close the top-most dialog |
| / | Filter sections (this manual) |
The Auto-Clip monitor
Give it a channel or playlist URL and a poll interval. From then on, every
new upload is downloaded and clipped automatically using that channel's own
settings. Tracked channels are written to
config/monitor_channels.json and re-armed when the server restarts.
Jobs are serialised — a burst of uploads won't launch several ffmpeg pipelines at once.
Per-channel profiles
This is the part people miss. Each tracked channel stores its own render profile, so a podcast and a gaming channel can be treated completely differently without touching your global defaults.
| Setting | Notes |
|---|---|
| Picker | Default, heuristic, energy, Ollama or cloud LLM |
| Clips per video | 1–20 |
| Subtitle style | Any of the five animation modes |
| Look | Vivid, warm, cool, cinematic, dramatic… |
| Min / max clip length | Seconds. Leave blank to inherit the global default. |
| Music & SFX | Per-channel on/off |
| Auto-publish | Post new clips to YouTube automatically |
| Upload as | Short, regular video, or auto |
| Privacy | Public, unlisted or private |
Anything left on Default inherits your global configuration. The
tracked-channel list shows a one-line summary of each profile, e.g.
picker: ollama · len: 15–60s · 3 clips · → YouTube (short, unlisted).
Channel preview
Each tracked channel has a Recent videos button. It opens a grid of that channel's latest uploads with thumbnails, durations and view counts, split into Videos and Shorts tabs.
Clip this on any of them renders it immediately using that channel's saved profile. This is a completely separate, on-demand path — your background auto-tracking keeps running untouched.
Listing uses yt-dlp, so it costs no API quota and works for any channel.
Connecting YouTube
Publishing uses the official YouTube Data API v3 with a browser redirect flow, so it works for remote and hosted users — not just whoever is sitting at the server.
One-time, by a super admin
- In the Google Cloud Console, create a project and enable YouTube Data API v3.
- Configure the OAuth consent screen. While it's in testing, add each person as a test user.
- Create an OAuth client of type Web application — not Desktop.
- Under Authorised redirect URIs, add exactly the URI shown in
Settings → YouTube app credentials, for example
https://YOUR-DOMAIN.com/api/youtube/oauth/callback. - Download the client JSON and upload it in that same panel.
Then, per user
Channels → Connect a channel → sign in with Google. The channel appears in that user's private list. Publish here switches the active target. Connect as many as you run.
http vs https, localhost vs
127.0.0.1, and any trailing slash all count as different URIs.
This is the single most common cause of a failed connect.
Shorts vs. regular video
When you publish a clip to YouTube, a dialog asks how to file it:
- Short — adds the
#Shortshint so YouTube shelves it as a Short. - Regular video — publishes plainly, with no Shorts hint.
- Auto — leaves the metadata as-is and lets YouTube decide.
The same dialog sets privacy (public / unlisted / private) and lets you override the title and description for that upload.
For tracked channels you choose all of this in advance, in the channel's profile, so automatic posts go out exactly how you want without anyone watching.
#Shorts hint is the part we control.
TikTok & Instagram
Both are supported through their official content-posting APIs, which require an approved developer app and an access token. Paste your token in Publish → Connect and the platform lights up.
There is no way around this and no unofficial fallback — it's a platform rule. Everything up to the upload is unaffected.
CLI reference
python main.py [URL] [options]
| Flag | Meaning |
|---|---|
--file PATH | Use a local video instead of a URL |
--clips N | Number of clips to produce |
--picker NAME | heuristic · ollama · energy · api |
--model NAME | Whisper model: tiny, base, small, medium, large-v3 |
--sub-mode MODE | pop · box · karaoke · bounce · minimal |
--sub-pos POS | bottom · middle · top |
--no-subs | Disable captions entirely |
--music / --no-music | Force background music on or off |
--sfx / --no-sfx | Automatic meme sound effects on or off |
--sfx-volume F | SFX volume, 0.0–1.0 |
--watermark TEXT | Burn in a text watermark (your handle) |
--out DIR | Output directory, overriding config |
--keep-source | Keep the source video after clipping (it is deleted by default) |
--delete-source | Delete the source after clipping — the default, listed for scripts that want it explicit |
--config PATH | Use a different config file (default config.json) |
--dry-run | Pick moments and print them; render nothing |
downloads/ or output/_uploads/, plus the working copy under
work/. A file you pointed it at with --file is never
deleted, and neither is an existing clip you re-ran through Enhance. Losing your own
footage is not a recoverable mistake, so the cleanup refuses to touch anything outside
its own scratch directories. Nothing is removed if the render produced no clips.Worked examples
# quick pass, small model, no music
python main.py URL --clips 4 --model base --no-music
# maximum quality, karaoke captions at the top
python main.py --file talk.mp4 --model large-v3 \
--sub-mode karaoke --sub-pos top --clips 6
# branded output
python main.py URL --watermark "@deadangel" --sfx --sfx-volume 0.4
# see the moment list before committing to a render
python main.py URL --picker api --dry-run
Pickers explained
| Picker | How it decides | Cost | Best for |
|---|---|---|---|
heuristic | Transcript structure — hooks, questions, sentence rhythm, keyword density | Free | Fast, dependable default |
energy | Audio dynamics: laughter, emphasis, volume swings | Free | Reaction content, comedy, streams |
ollama | A local LLM reads the transcript and argues for moments | Free, needs Ollama | Nuanced picks with no bill |
api | Cloud LLMs — every provider you've keyed is queried in parallel | Tokens | Best quality when it matters |
Ensemble
In the Studio you can tick more than one. Moments that several pickers independently choose are ranked highest — agreement is a better signal than any single model's confidence. Per-channel profiles support this too.
Caption styles
| Mode | Looks like |
|---|---|
pop | Words appear on a dark rounded pill — the default, reads anywhere |
box | Solid highlight block behind the text, high contrast |
karaoke | Outlined text with the active word highlighted as it's spoken |
bounce | Outlined text with a springy per-word motion |
minimal | Clean text and a soft shadow, nothing else |
All five ride on real word-level whisper timings, in whatever language was spoken.
Look & grading
Preset looks — vivid, warm, cool, cinematic, dramatic, clarity, vintage, B&W — are starting points. On top you get manual brightness, contrast, saturation, gamma, grain, sharpen and vignette, plus speed and fades. The clip editor previews all of it live before you commit.
config.json
Holds pipeline defaults: transcription, picker, subtitles, resize, filters,
music, effects and upload. Every field falls back to a sensible default in
config.py, so the file can be partial or missing entirely. The
Studio edits it visually — you rarely need to open it by hand.
Environment (.env)
Loaded automatically at startup. Real shell environment variables always win over the file.
| Variable | Purpose |
|---|---|
APP_ENV | development (default) or production. See Deployment. |
PUBLIC_BASE_URL | The public origin users reach, e.g. https://YOUR-DOMAIN.com. Required in production. |
SUPERADMIN_CODE | Bootstrap and elevation code. Default ARCH-ANGEL-2026 — change it. |
SECRET_KEY | Signs public clip links. Auto-generated into config/secret.key if unset. |
CLIPPER_HOST | Bind address. 127.0.0.1 in dev, 0.0.0.0 in production. |
CLIPPER_PORT | Port. Default 8000. |
TRUST_PROXY | Believe X-Forwarded-*. On only when a reverse proxy is really in front. |
ENABLE_HSTS | Send Strict-Transport-Security. Leave off until HTTPS is confirmed. |
ALLOW_SHUTDOWN | Permit the in-app "kill server" button. Off in production. |
OPEN_BROWSER | Auto-open a browser at startup. Off in production. |
MAX_BODY_BYTES | Largest accepted upload. Default 2 GiB. |
MEDIA_LINK_TTL | Lifetime of a signed public clip link, in seconds. Default 6 h. |
OAUTH_REDIRECT_BASE | Legacy alias for PUBLIC_BASE_URL; still honoured. |
OPENAI_API_KEY | Optional cloud-LLM picker keys. Set any of them; every keyed provider is queried in parallel. |
ANTHROPIC_API_KEY | |
GEMINI_API_KEY | |
GROQ_API_KEY | |
OPENROUTER_API_KEY | |
DEEPSEEK_API_KEY | |
MISTRAL_API_KEY |
# .env — production
APP_ENV=production
PUBLIC_BASE_URL=https://YOUR-DOMAIN.com
SUPERADMIN_CODE=change-me-before-first-run
SECRET_KEY=<python -c "import secrets;print(secrets.token_hex(32))">
CLIPPER_HOST=127.0.0.1
CLIPPER_PORT=8000
TRUST_PROXY=1
ENABLE_HSTS=1
ALLOW_SHUTDOWN=0
Start from .env.example, which documents every option.
HTTP API
The Studio is a thin client over a plain JSON API. Every route below the public ones needs a session cookie; some need the super-admin role.
| Route | Does |
|---|---|
POST /api/run | Start a render job |
GET /api/status | Current job status and streaming log |
POST /api/cancel | Kill the running job and its children |
GET /api/clips | List finished clips with metadata |
GET /media?path= | Serve a clip (supports HTTP Range) |
POST /api/publish/youtube | Upload a clip — takes kind and privacy |
GET /api/youtube/accounts | The caller's connected channels |
GET /api/monitor/list | Tracked channels and their profiles |
GET /api/monitor/videos | A tracked channel's recent uploads |
GET /api/errors | The error log |
POST /api/errors/clear | Clear it (super admin) |
GET /api/providers | Provider/key status (super admin) |
Cross-origin POSTs are rejected, so drive the API from the same origin or a server-side client.
Project layout
main.py CLI entry (parse args -> run_pipeline)
dashboard.py Web server: routing, auth, job runner, all /api/*
auth.py Accounts, roles, sessions, super-admin code
publish.py YouTube web OAuth + per-user channels
monitor.py Auto-Clip channel monitor
downloader.py transcriber.py ai_picker.py renderer.py pipeline stages
subtitles.py effects.py motion_fx.py filters.py music.py sfx.py
config.py config.json typed config + defaults
web/ landing · docs · privacy · terms · credits · login · studio
web/assets/ site.css · site.js · studio.js
assets/ bundled music + SFX
secrets/ OAuth app + per-user tokens (git-ignored)
config/ auth · sessions · monitor_channels · yt_accounts
output/ your clips
Deployment
There is a full walkthrough in DEPLOY.md, plus ready-made
Dockerfile, docker-compose.yml, a Caddyfile, an nginx
config and a systemd unit under deploy/. The short version:
cp .env.example .env # set APP_ENV, PUBLIC_BASE_URL, SUPERADMIN_CODE
docker compose up -d --build
Production mode
Setting APP_ENV=production flips the app out of laptop mode:
| development | production | |
|---|---|---|
| Browser auto-open | yes | no |
| Remote "kill server" | allowed | disabled, button hidden |
X-Forwarded-* trusted | no | yes, via TRUST_PROXY |
| HSTS | off | on |
| Public links built from | the Host header | PUBLIC_BASE_URL only |
| Misconfiguration | silent | printed and logged at boot |
PUBLIC_BASE_URL is required in production. It is used for
the YouTube OAuth redirect and for the signed clip links handed to
TikTok/Instagram. Host headers are attacker-controlled, so in production
they are ignored in favour of this one configured value.
Essentials
- Terminate TLS at a reverse proxy (Caddy does certificates automatically);
keep the app on
127.0.0.1or a Docker-internal network. - Turn on
TRUST_PROXY=1— but only when a proxy really is in front. - Change
SUPERADMIN_CODEbefore the first boot, then rotate it in-app. - Set
SECRET_KEYexplicitly if you run more than one instance. - Point an uptime monitor at
/healthz— it's public and cheap. - Rendering is CPU/GPU heavy and jobs are serialised, so size the box for one render at a time and run separate instances for more throughput.
- Back up
config/andsecrets/. Losing them means everyone signs in again and reconnects their channels.
How social platforms fetch a clip
TikTok and Instagram download the video from a URL their servers can
reach, so it can't sit behind the session cookie. Rather than exposing
output/, the app mints a short-lived HMAC-signed link for
the single file being posted — bound to that exact path and expiry, verified
server-side, and dead after MEDIA_LINK_TTL (six hours by default).
Everything else stays authenticated.
Signals & restarts
The server handles SIGTERM (and SIGINT/SIGBREAK),
cancelling any running job and draining in-flight requests, so
docker compose restart and systemctl restart are safe.
web/assets/vendor/ and update the tags.
Security
- Passwords and the super-admin code are salted and hashed with PBKDF2-HMAC-SHA256 (200,000 iterations). Neither is ever stored or returned in clear text.
- Sessions are opaque random tokens in HttpOnly cookies.
- Login attempts are rate-limited per address.
- Cross-origin POSTs are blocked.
- API routes are role-gated server-side, not just hidden in the UI.
- Media serving is path-traversal-checked and confined to
output/. - YouTube OAuth uses PKCE, and refresh failures flag the channel for reconnection rather than failing silently.
If you find a security problem, please report it privately before disclosing it.
Your data
On the hosted workshop, the files below live on our servers and the privacy policy governs them. On a self-hosted install they are on your disk and nobody else can reach them. Either way, the layout is the same:
| Path | Holds |
|---|---|
config/auth.json | Users (hashed passwords), hashed super-admin code |
config/sessions.json | Active sessions |
config/yt_accounts.json | Per-user channel manifest |
secrets/youtube/<user>/ | Per-user OAuth tokens |
secrets/client_secret.json | The Google OAuth app credential |
config/monitor_channels.json | Tracked channels and profiles |
output/ | Your clips and their metadata |
See the Privacy Policy for the full picture.
Troubleshooting
"Missing code verifier" when connecting YouTube
A PKCE mismatch: the verifier generated when the consent URL was built has to be replayed at the token exchange. This is handled — if you still see it, you're running an older build. Restart the server and retry.
"invalid_grant" or "redirect_uri_mismatch"
Almost always the redirect URI. Copy it verbatim from Settings → YouTube app credentials into the Google Cloud console. Also confirm the OAuth client type is Web application. Authorisation codes are single-use and expire quickly, so don't reload the callback URL.
The super-admin code isn't accepted
Edit .env before creating the first account, then restart the
server. Once an account exists, the code is fixed in
config/auth.json and must be rotated in-app instead.
Transcription is extremely slow
You're on CPU. Use --model tiny or base, or install a
CUDA-enabled build. The Settings tab reports which engine was detected.
ffmpeg not found
Install it and put it on your PATH. On Windows, confirm
.bin/ffmpeg.exe is present.
A render failed and I don't know why
Telemetry → Error log. Render failures are recorded there with the last lines of pipeline output.
Nothing happens when I track a channel
Monitors poll on an interval — with a 300-second interval, nothing happens for up to five minutes, and only for uploads that appear after you started tracking. To clip something that already exists, use Recent videos → Clip this.
The port is already in use
Set CLIPPER_PORT to something else, or stop the other instance.
Licence & notices
ARCH‑ANGEL is built by dead-angel. It stands on excellent open-source work, each under its own licence:
| Project | Used for |
|---|---|
| yt-dlp | Fetching source video |
| FFmpeg | All decoding, filtering, encoding |
| faster-whisper / CTranslate2 | Transcription |
| Ollama | Optional local LLM picking |
| Google API Python Client | YouTube Data API v3 |
| Chart.js · WaveSurfer.js · Sortable.js · NProgress · tippy.js | Studio interface |
| Fraunces · Newsreader · Courier Prime · Caveat | Typography on this site |
You are responsible for having the right to clip and republish whatever you feed it. See the Terms of Service.
Credit
All credit for ARCH‑ANGEL goes to angel. The credits page says it properly.
thanks for reading the whole thing.