← All topics

Every screen in this app is a window onto one long conveyor belt. A clip arrives from Reddit, Instagram, or Telegram; it gets a caption; the caption produces tags; the clip becomes a generation job; ComfyUI renders a video from it; you sort the results; the keepers go to CivitAI. Six hops, and each one is owned by a different screen.

This page is the map between them. Per-screen detail lives on that screen's own page — the point here is what hands off to what, and where a stall shows up when the belt stops moving at 2am.

How the hops fit together

Source sync → captioning → tagging → generation → review → publishing.

Three of those hops are things you drive by pressing a button. Three of them happen on their own, on a machine that is not Vercel. That split is the single most useful thing to hold in your head, because it decides where you go looking when something stops: see "Where each hop actually runs" at the bottom.

The whole belt can also run unattended overnight — that is a separate story, told on Nightly automation.

Source sync

Owning screen: Settings → Media Source, and Browse.

Two different shapes of ingest, for two different situations.

Bulk, source-wide. Settings → Media Source is where sources are configured and swept. Sync all sources runs Telegram, Reddit, and Instagram together; Sync all Telegram, Sync all Reddit, and Sync AI VIDS Collection run one apiece. The Sync All Media Sources tile on Home fires the same all-sources sync with its Sync Now button, without leaving the dashboard.

Selective, clip by clip. Browse previews what is sitting at Telegram or Instagram before you own it, and downloads only what you tick. There is no Reddit browse — Reddit is synced subreddit-wide from Settings.

Whatever the route, media lands in Media, uncaptioned, and the run shows up under Recent sync jobs on Stats.

Stuck here looks like: an empty or zero-count row under Recent sync jobs on Stats. On Browse, an amber session banner is the loud version — an expired Telegram login, or an Instagram session that needs npm run seed:instagram re-run on the machine. The quiet version is worse: a Telegram ingest that fails clears your selection and refetches the grid exactly as if it had worked, and the only signal is that the cards never flip to Ingested — the reason is in that run's Telegram ingest notification.

One false alarm worth ruling out first. A clip that "did not arrive" is often sitting in Media behind the default filters, which ship narrowed to today, under twenty seconds, and no ComfyUI job yet. Widen Day and Duration before concluding the sync dropped it.

Captioning

Owning screens: Media for one clip, Settings → Captioning for a sweep, Auto-Caption Runs for the audit trail.

A caption is a structured read of the clip — subject, action, position, per-frame descriptions — and it is the input everything downstream is built from. Nothing can be generated from an uncaptioned clip, and nothing gets tagged without one.

Four ways to get one:

  • Caption / Recaption on a card in Media, or in bulk from the selection bar.
  • Start Auto-Captioning on Settings → Captioning, which walks the eligible backlog until you stop it.
  • Queue captioning on the Caption gaps page under Stats, for a specific selection. This one creates no run, so the work never appears on Auto-Caption Runs — look for it under Recent caption activity on Stats instead.
  • The nightly window, which sizes its own batch and dispatches it as a run.

Underneath, all of them converge on the same execution path: the app writes a caption job and puts it on a Cloudflare queue, the caption executor on the home Ubuntu host pulls a batch of messages off it — ten at a time by default, five of them worked in parallel — asks the ffmpeg service for sampled frames on each, and sends those frames plus the prompt to LM Studio. It only pulls again once the whole batch has settled. Both numbers are settings — Queue pull batch size and Queue processing concurrency on Settings → Captioning.

Stuck here looks like: the Caption backlog cards on Stats not going down. Open the run on Auto-Caption Runs, press the Failed counter to filter to just the broken tiles, and read the Failure reason: panel on one — an LM Studio timeout and a media document that could not be fetched at all are different problems with different fixes. A run badged Empty is not a failure; it means the sweep ran and everything was already captioned.

Two traps. A run of jobs wedged in active processing needs Reset Stuck Jobs, not Purge Caption Queue — Purge only clears queued transport state and leaves in-flight jobs exactly where they were. And because the whole execution half runs on the home host, a perfectly healthy web app tells you nothing about whether captions are moving.

If the captions come back wrong rather than not at all, that is a prompt problem, and the LM Studio Lab is the place to iterate on it against a real clip without touching production settings.

Tagging

Owning screen: Tags.

Tagging is the one hop with no button in the normal path. It fires automatically the moment a caption is written, reading the caption's own fields plus the clip's subreddit, and it is best-effort — a tagging failure never fails a caption job that already succeeded.

That coupling is the thing to internalise: tags are downstream of caption text, so a position that never gets tagged almost always means the caption never named it. Confidence is assigned by which caption field matched, and the Video NSFW pass produces the highest-confidence caption input — which makes it the pass to iterate on in the Lab when a position keeps getting missed. A subreddit rule can outrank even that, but only Reddit clips ever get one.

Stuck here looks like: a growing untagged pile rather than an error. Work it on the rapid tagger (Open Rapid Tagger on /tags), which serves one clip at a time in either Untagged or Needs review mode. Re-running tagging over old media is a separate, bounded operation on the same screen.

The expensive mistake here is a careless bulk apply. A hand-applied tag is pinned, and a pinned tag freezes that clip's whole namespace against future automatic tagging, permanently.

Generation

Owning screens: Jobs to drive it, ComfyUI Queue to watch it.

A job is one video you are asking ComfyUI to generate from one source clip. Four things create one:

  • Add to Jobs on a Media card, singly or in bulk.
  • Queue generation on the ComfyUI gaps list under Stats.
  • Add 30 random captioned media in the floating action menu on the Jobs Pending tab.
  • The nightly window's refill, which seeds random captioned clips per platform when the pool is running dry.

A job gets its own caption, separate from the clip's. It enters at pending, moves through captioning to captioned, and only a captioned job can be queued — which is why a freshly created job shows no Queue button. Pressing Queue to ComfyUI submits the workflow to the ComfyUI server configured on Settings → Workflow, and ComfyUI reports progress back to the app by webhook.

While it runs, ComfyUI Queue separates what is rendering right now from what is waiting; the Running tab on Jobs is the same two statuses merged.

Stuck here looks like: a job sitting on Running far longer than the usual duration, or a red Generation failed panel on its detail page. That panel leads with the classification, the stage it died at, and the reference video's resolution and size — the size is repeated there deliberately, because an oversized input is the usual cause of an out-of-memory failure. ComfyUI's own process output is shipped to a separate Sentry project; pivot into it by timestamp when the job document does not say enough.

Nothing anywhere in the app cancels a generation that is already running. Not the Queue page, not Jobs. Once ComfyUI has picked a job up, the only place to stop it is ComfyUI itself. Plan around that rather than hunting for the button.

Review

Owning screen: Review.

Once videos land on completed, the deck is how you sort them without opening anything: one autoplaying card at a time, and ✕ Archive / ★ Favorite / ✓ Keep under it. All three mark the item reviewed, which is what takes it out of the deck for good — there is no skip.

The deck also has two ingest-side modes, and they are the cheap places to catch a problem before you spend a GPU on it: Recently captioned catches a bad caption before it becomes a generation, and Recently ingested culls junk before anything is spent on it at all.

A generated card whose driving clip differs from the result renders as an A/B compare — reference on the left, generated on the right, subject portrait beside them. That layout is the one that makes it obvious when a dud came from the reference clip's motion rather than from the generation.

Review is not a publishing screen. There is no publish button on it. Undo is single-step, and it clears the favorite flag as a side effect, so undoing something that was already favorited before you swiped loses that too.

Publishing to CivitAI

Owning screen: Jobs, Completed tab.

Publish to CivitAI renders on the Completed tab only. The nav's Jobs link points at ?status=all, and on that tab a finished job shows no Publish button at all — switch to Completed first. The Failed tab drives the same publish flow, but the button there reads Retry Publish on a publish_failed job and a disabled Publishing… on one that is mid-flight; the words "Publish to CivitAI" never appear on that tab. The button additionally needs the job to have a final CDN video and a non-empty positive prompt. For a daily batch, Select recent 8 on the Completed tab opens the bulk action menu straight to it. The same button also appears on the Recently Completed row on Home and on the Jobs tab of Favorites.

Before any of it works, the CivitAI session has to be seeded: Settings → Publishing carries a Session Seeded or Not Connected badge and a Test session button. The session is not created on that page — you run the seeding script on the machine and complete the login in the browser it opens.

Then comes the part that is only obvious once you know it. The publish does not run in the cloud. The app puts a publish message on a Cloudflare queue, and services/civitai-publisher-runner on the home Ubuntu host pulls it and drives a real browser through CivitAI's upload flow. It lives there because CivitAI serves a Cloudflare bot-challenge to datacenter IPs — from a datacenter the site never finishes loading, so the flow needs a residential connection. workers/civitai-publisher still exists as a cold fallback; it is not what production runs, and changing it changes nothing.

The consequence: a publish that never moves is a host problem, not a Vercel one. The app can be perfectly healthy while the runner container is down, and no amount of redeploying the app will shift it.

Stuck here looks like: jobs piling up on the Failed tab — which is where publishing lives too, so a publish that is genuinely mid-flight and one that already failed sit side by side. That is deliberate: it is where Retry Publish lives. Stats has a Failed publishing jobs table with the message, and Home has a Recently Failed Published Jobs row that is publish_failed and nothing else. If the session badge reads Not Connected, start there; otherwise the runner on the host is the next place to look.

Where each hop actually runs

Vercel runs the web app and nothing else. It is the control plane: it decides what should happen, writes the records, and puts messages on queues.

The home Ubuntu host runs the work — the caption executor that talks to LM Studio, the ffmpeg service that extracts frames, ComfyUI itself, the CivitAI publisher, and the Instagram collection sync. Everything that needs a GPU or a residential IP is there.

So when a hop stalls, the first question is which side of that line it lives on. Captioning, generation, and publishing all do their real work on the host, and ingest leans on it too for video download and frame extraction. If the screen is fine and the numbers are not moving, the app is usually not where the problem is.