how it runs · stages · stack
Six stages. Eighteen seconds. Thirteen feeds.
Between a prompt and a published post there's a pipeline. Six stages, a handful of models, and a queue that fans out to thirteen platforms in parallel. Here's exactly what happens — and on what.
the cycle
What runs, and on what.
- 01
prompt
+0.0s
—
You type. PrintAIr accepts a one-line idea, a paragraph, or a topic pulled from your idea bank. Persona + locale + workflow context are stamped automatically.
- 02
brief
+0.2s
xai/grok-4-1-fast
The LLM derives a structured creative brief — topic, key message, mood, hashtags, image prompt. Persona context prepends so voice carries through every downstream call.
- 03
master
+11.4s
google/nano-banana-2 · xai/grok-imagine-video
One master asset gets generated — image for image posts, faceless video for story posts. Multi-ref-capable image models receive the persona's photos so the look stays consistent post over post.
- 04
cuts
+13.1s
ffmpeg
Native cuts per platform: 1:1 for IG feed, 9:16 for Reels / Shorts / TikTok, 16:9 for X / YouTube long. The cuts happen locally — no extra Wiro spend.
- 05
variants
+17.8s
xai/grok-4-1-fast (×N)
Per-platform text rewriters fire in parallel. Each variant respects the platform's character limit, voice conventions, and hashtag norms. Live SSE token streaming pushes drafts into the UI as they generate.
- 06
publish
+18.6s
platform OAuth / paste-creds adapters
A human approves on the web detail page or the Telegram bot. The publish fan-out sends one variant per account in parallel; partial failures are isolated, the rest still ship.
architecture
Five rows, two stores, no glue code.
persistence
Postgres 17
33 migrations · Drizzle ORM
MinIO · S3
all generated assets
primitives
Six concepts. Everything maps to one.
primitive
Persona
Who the post is written and rendered as. Photos seed multi-ref image models; mood/tone seed the LLM.
personas ( id uuid, user_id text, name text, description text, characteristics text, mood text, speaking_tone text, master_image_url text, status persona_status — active|retired|candidate )
primitive
Scene
A single beat of a story video. Carries the visual prompt + narration + planned duration.
scenes_json: [{
index: number,
act: 'intro'|'development'|'conclusion',
visualPrompt: string,
narration: string,
durationSec: number,
imageAssetId, videoAssetId, narrationAssetId,
}]primitive
Variant
One platform-shaped take of a post. Per-account binding so two LinkedIn handles each get their own.
post_variants ( id uuid, post_id uuid, platform real_platform, social_account_id uuid, text text, media_asset_ids uuid[], status variant_status — pending|ready|publishing|published|failed )
primitive
Workflow
A scheduled prompt template. Pulls from an idea bank, fires on cron, optionally pins a persona pool.
workflows ( id uuid, user_id text, template_key text, prompt_template text, topic_pool jsonb, idea_tag_filter jsonb, account_ids jsonb, persona_ids — via workflow_personas )
primitive
Approval
The gating moment. Web detail page or Telegram bot — both write the same row and trigger the same publish.
posts.status: 'awaiting_approval' → POST /api/posts/:id/approve → publish fan-out (one job per variant) → variants flip to 'published' or 'failed'
primitive
Cost ceiling
Per-account monthly cap. Workflow ticks skip when the cap is exhausted; surface progress everywhere.
social_accounts ( monthly_budget_usd numeric(10,2), budget_mode 'warn'|'enforce', spend_this_month_usd numeric(10,2), spend_month_key text — YYYY-MM )
stack
Boring infrastructure. Sharp surface.
Next.js
15.5
app router · RSC · SSE
React
19
UI runtime
Tailwind
4
CSS · @theme tokens
Drizzle ORM
0.45
schema · migrations · types
Postgres
17
primary store · 33 migrations
RabbitMQ
—
9 queues · DLX · DLQs
Redis
—
pub/sub → SSE bridge
MinIO · S3
—
asset storage · CDN-fronted