[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fmjcyg97ygyks":3,"$fanuq43nlrv5g":48},{"slug":4,"title":5,"body":6,"summary":7,"tags":8,"author":13,"cover_url":14,"published_at":15,"seo_title":5,"seo_description":7,"reading_minutes":16,"related":17},"an-ai-media-pipeline-that-shows-its-work","An AI media pipeline that shows its work: ComfyUI presets, FlowDSL routing and the misses","\u003Cp>The images and clips on the sites I run — async.shop, ainiac.com, the other brand sites — are generated, not stock. This post is the engineering side of that: how the pipeline is put together, how a blog post gets its media, and the bugs and model misses I ran into shipping it. Every image in this post came through the same pipeline, requested from this post's own media panel in the admin; the prompts and GPU times below are the real ones.\u003C\u002Fp>\n\n\u003Ch2>The shape of it\u003C\u002Fh2>\n\u003Cp>Four pieces, each small on its own:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>ComfyUI on a GPU box on my tailnet.\u003C\u002Fstrong> No public endpoint — only the servers on the private network can reach it.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>A Go module, \u003Ccode>mediagen\u003C\u002Fcode>,\u003C\u002Fstrong> that owns four presets: \u003Cem>image\u003C\u002Fem> (a fast SDXL-class checkpoint), \u003Cem>edit\u003C\u002Fem> (Qwen-Image-Edit 2509), \u003Cem>angle\u003C\u002Fem> (the same edit model with a camera-move LoRA) and \u003Cem>video\u003C\u002Fem> (Wan 2.2 image-to-video from a first and optional last frame, with 4-step \"lightning\" LoRAs). Every preset takes an aspect ratio, a size tier and, for video, a length in frames.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>FlowDSL flows\u003C\u002Fstrong> for everything in between — what a request turns into, and where a finished file goes. They're data, editable per project in the flow studio, not Go code.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Object storage + a proxy.\u003C\u002Fstrong> Outputs land in the brand's own folder in an S3 bucket, uploaded public-read per object; Traefik maps each site's \u003Ccode>\u002Fmedia\u003C\u002Fcode> to its folder with no app in the path, and on the brand domains Cloudflare caches in front (byte ranges included, so video seeking works).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>Presets that survive a new graph\u003C\u002Fh2>\n\u003Cp>ComfyUI graphs are JSON maps of numbered nodes. The naive way to \"set the prompt\" is to write into node \u003Ccode>6\u003C\u002Fcode> because that's where it was in the export. That breaks the first time someone re-exports the workflow. Instead the patcher finds roles by walking the graph backwards from the node that matters: the video node's \u003Ccode>start_image\u003C\u002Fcode> input is traced through any scalers to its \u003Ccode>LoadImage\u003C\u002Fcode>, its \u003Ccode>positive\u003C\u002Fcode> input to the text encoder, and so on. Sizes go onto every scaler on the path, not just the first. The upshot is that a project can store its own graph under a preset's key and it just works — as long as it has the same kind of nodes, not the same ids.\u003C\u002Fp>\n\n\u003Ch2>Job rows are the state\u003C\u002Fh2>\n\u003Cp>A generation is a row in Mongo: preset, parameters, the ComfyUI prompt id, status, outputs. A scheduler tick submits queued rows (a configurable number in flight — the GPU box is shared), polls ComfyUI's history, uploads outputs and emits \u003Ccode>mediagen.job.succeeded\u003C\u002Fcode> or \u003Ccode>…failed\u003C\u002Fcode>. Nothing lives only in memory, so the API can be redeployed mid-render and the result is still collected.\u003C\u002Fp>\n\n\u003Ch2>Flows do the routing\u003C\u002Fh2>\n\u003Cp>A blog post asking for media doesn't call the generator. It saves a row for the request, then emits an event; a flow turns the event into a job and records the job id back on the row. When the job finishes, another flow routes the file by what the job was for:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\"from\": \"src\", \"to\": \"blogmedia\", \"condition\": \"packet.ref_kind == \\\"post_media\\\"\"}\n{\"from\": \"src\", \"to\": \"cover\",     \"condition\": \"packet.ref_kind == \\\"editorial_story\\\"\"}\n{\"from\": \"src\", \"to\": \"ig\",        \"condition\": \"packet.ref_kind == \\\"instagram_post\\\"\"}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Same pattern for Instagram drafts, X\u002FFacebook posts and editorial covers. Adding a new consumer is a new branch in a flow, not a new code path. One more flow listens to \u003Ccode>content.page.changed\u003C\u002Fcode>: when a \u003Cem>live\u003C\u002Fem> blog post has no cover, the AI writes a one-subject prompt and a cover is generated and applied — only if the post still has none when it's ready.\u003C\u002Fp>\n\n\u003Ch2>Per-post media, and this post\u003C\u002Fh2>\n\u003Cp>In the admin, every blog post has a media panel. A request is just this:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>POST \u002Fadmin\u002Feditorial\u002Fposts\u002Fifcodes\u002F{slug}\u002Fmedia\n{\"purpose\": \"inline\", \"preset\": \"video\", \"source_media_id\": \"…\",\n \"aspect\": \"16:9\", \"frames\": 49, \"prompt\": \"…\"}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Finished media goes into the post's list; from there it's set as the cover or inserted as a \u003Ccode>&lt;figure&gt;\u003C\u002Fcode> after a chosen heading. Nothing lands in a live post by itself — except an auto-cover on a post with no cover. There's also an \"illustrate\" button: the model reads the post and proposes pictures, but they're suggestions — nothing renders until someone approves one. Here's the chain this post went through.\u003C\u002Fp>\n\n\u003Ch3>1. A plain photo\u003C\u002Fh3>\n\u003Cfigure data-post-media=\"6aba81a317ceba3543925a7d\">\n  \u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba81a317ceba3543925a82-0-8be175a8.png\" alt=\"A small succulent in a concrete pot on a cluttered desk\" width=\"1280\" height=\"832\" loading=\"lazy\">\n  \u003Cfigcaption>1280×832, 21 s on the GPU — a stand-in for a phone photo. The first three takes came back pixel-identical (the unseeded-request bug below). After the fix, the three new takes differed; one had two plants, one a laptop screen full of text, and the one kept still has a laptop edge the prompt never asked for.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp class=\"prompt\">\u003Cstrong>image · 3:2 · medium · 21 s on the GPU\u003C\u002Fstrong>\u003Cbr>Unedited smartphone photo of one small succulent plant in a grey concrete pot standing on a cluttered office desk next to a tangled charging cable, a spiral notebook and an empty coffee mug, flat harsh overhead office light, slightly tilted framing, ordinary everyday snapshot\u003C\u002Fp>\n\n\u003Ch3>2. Edit: new set, same object\u003C\u002Fh3>\n\u003Cfigure data-post-media=\"6aba827c17ceba3543925ab2\">\n  \u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba827d17ceba3543925ab8-0-dadd967b.png\" alt=\"The same succulent on dark slate against a navy background\" width=\"1248\" height=\"832\" loading=\"lazy\">\n  \u003Cfigcaption>1248×832, 2 min 11 s on the GPU. Source: the photo above, passed by media id.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp class=\"prompt\">\u003Cstrong>edit · 3:2 · medium\u003C\u002Fstrong>\u003Cbr>Remove the cable, the notebook, the mug and all clutter. Place the same succulent in its concrete pot on a dark slate surface in front of a deep navy background, one soft light from the left and a gentle rim light. Keep the plant, its leaves and the pot exactly as they are. Quiet, minimal product photo.\u003C\u002Fp>\n\n\u003Ch3>3. Camera move\u003C\u002Fh3>\n\u003Cfigure data-post-media=\"6aba854617ceba3543925b06\">\n  \u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba854717ceba3543925b0c-0-5be7abb1.png\" alt=\"The succulent seen after a 90 degree camera turn\" width=\"1248\" height=\"832\" loading=\"lazy\">\n  \u003Cfigcaption>1248×832. The turn is clear here — the slate now runs away from the camera — but both takes warmed the grey concrete to beige. A generative camera move re-draws the object; check colours and details before using one as a product view.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp class=\"prompt\">\u003Cstrong>angle · rotate 90° left · 2 min 19 s on the GPU\u003C\u002Fstrong>\u003Cbr>Rotate the camera 90 degrees to the left.\u003C\u002Fp>\n\n\u003Ch3>4. A seamless loop\u003C\u002Fh3>\n\u003Cfigure data-post-media=\"6aba892f17ceba3543925b6a\">\n  \u003Cvideo src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba893017ceba3543925b70-0-e2e74e77.mp4\" autoplay muted loop playsinline preload=\"metadata\" width=\"848\" height=\"480\">\u003C\u002Fvideo>\n  \u003Cfigcaption>848×480, 49 frames at 16 fps, 4 min 22 s on the GPU. With no last frame given, the first frame doubles as the last, so the clip loops without a jump.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp class=\"prompt\">\u003Cstrong>video · 16:9 · medium · 49 frames\u003C\u002Fstrong>\u003Cbr>Still life: the succulent rests on the dark slate while soft light slowly drifts across its leaves and the pot. Nothing else moves and nothing enters the frame. Locked-off static camera.\u003C\u002Fp>\n\n\u003Ch3>5. The cover\u003C\u002Fh3>\n\u003Cfigure data-post-media=\"6aba8ba317ceba3543925bdf\">\n  \u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba8ba317ceba3543925be4-0-2f6b8a5c.png\" alt=\"A succulent on a dark desk in a single beam of light\" width=\"1344\" height=\"768\" loading=\"lazy\">\n  \u003Cfigcaption>1344×768, 30 s on the GPU. All three takes matched the prompt this time — one subject plus light and mood is what this model does well. The one kept leaves room for the title.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\u003Cp class=\"prompt\">\u003Cstrong>image · 16:9 · medium · 30 s on the GPU\u003C\u002Fstrong>\u003Cbr>One small succulent in a grey concrete pot on a dark slate desk, lit by a single soft beam of light, deep navy background, lots of empty space, quiet minimal mood\u003C\u002Fp>\n\n\u003Ch2>What broke\u003C\u002Fh2>\n\u003Cp>None of these were exotic. Most were the same few mistakes in new places.\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>An explicit field list, three times.\u003C\u002Fstrong> The storage module merges admin settings over env config by building a new struct field by field; the new \"upload public-read\" flag wasn't in the list, so every upload stayed private and every page showed a 403. The content editor did the same in reverse: it didn't send the blog fields, and the upsert replaced them with blanks — any save from that editor would have erased a post's cover and date. The workflow store had dropped a new \u003Ccode>kind\u003C\u002Fcode> field the same way. The fix for the editor was structural: the save now keeps any field the request doesn't mention, and a field sent empty still clears it.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\"Generate again\" returned the same picture.\u003C\u002Fstrong> A request with no seed kept the preset graph's own fixed seed. I only noticed while making this post: three tries of the first photo came back pixel-identical. Unseeded requests now draw a random seed and store it on the job, so any take can be reproduced.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>A relative URL is not a source.\u003C\u002Fstrong> Locally, media URLs are same-origin (\u003Ccode>\u002Fmedia\u002F&lt;key&gt;\u003C\u002Fcode>), and the generator only fetches \u003Ccode>http(s)\u003C\u002Fcode> sources — so \"animate this image\" failed. Our own files now go in as bucket keys, which need no fetch at all.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Saving after emitting.\u003C\u002Fstrong> The first version emitted the \"cover requested\" event and then saved the row. The flow runs in milliseconds and wrote the job id first; the save then overwrote it. Save, then emit.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Queue time is not GPU time.\u003C\u002Fstrong> \u003Ccode>submitted_at → updated_at\u003C\u002Fcode> on a job includes waiting in ComfyUI's queue behind other jobs; a 31-second image showed up as six minutes. The times in this post come from ComfyUI's own history (\u003Ccode>execution_start → execution_success\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>What the models do\u003C\u002Fh2>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Negative prompts are a no-op on the fast video model.\u003C\u002Fstrong> The lightning LoRAs run at CFG 1, where the negative conditioning contributes nothing. A clip kept adding a hand lighting a candle; listing \"hands, lighter\" changed nothing. Removing the rising smoke from the prompt (which read as \"someone just lit this\") did most of the work — and one of the two takes after that still had fingertips creeping in at the edge.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Objects right, relationships wrong.\u003C\u002Fstrong> \"A phone on a tripod pointed at a candle\" never came out: across seven tries the tripods turned into candle stands, one candle became three, a phone screen showed a stranger's face. One subject plus light and mood is reliable; a scene with things doing things to each other is not.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Famous products leak in.\u003C\u002Fstrong> A plain white sneaker came back with a swoosh. If it looks like somebody's brand, change the subject.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The practical rule that came out of all this: render two or three takes of anything that matters, look at them, and say in the caption when a picture needed tries. It's cheap, and it's the difference between a pipeline you trust and one you have to apologise for.\u003C\u002Fp>\n","How the images on my sites are generated: four ComfyUI presets behind one Go module, job rows as state, FlowDSL flows for routing, per-post media in the admin — and the bugs and model misses I hit shipping it. This post's own images were made the same way.",[9,10,11,12],"ai","flowdsl","comfyui","image-generation","if.codes","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba8ba317ceba3543925be4-0-2f6b8a5c.png","2026-09-28T15:57:50Z",7,[18,30,40],{"slug":19,"title":20,"type":21,"summary":22,"tags":23,"author":13,"cover_url":27,"published_at":28,"updated_at":29},"openai-embeddings-python-mongodb","Transforming Text into Vectors: OpenAI Embeddings in Python","blog","Learn how to generate text embeddings with the OpenAI API in Python to power semantic search, recommendations, and more. Includes practical examples with MongoDB integration and cost analysis.",[24,9,25,26],"openai","python","mongodb","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fblog\u002Fge\u002Fgetty-2-69fd9cb7.jpg","2024-11-23T00:00:00Z","2026-09-26T22:29:53.912Z",{"slug":31,"title":32,"type":21,"summary":33,"tags":34,"author":13,"cover_url":37,"published_at":38,"updated_at":39},"check-pricing-availability-ing-domains","Last Chance to Grab Short .ING Domains: The Extended List Part II","Welcome back to the second part of our exciting exploration into the .ING domain zone! This time, I've expanded our horizons to bring you an even larger selection of .ING domain names. List of over 24,000 domain names inside.",[35,36],"domains","business","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fblog\u002Fdo\u002Fdosmthgreat-60b31838.png","2023-12-14T00:00:00Z","2026-09-26T22:29:53.739Z",{"slug":41,"title":42,"type":21,"summary":43,"tags":44,"author":13,"cover_url":45,"published_at":46,"updated_at":47},"impressive-ing-domains","Unveiling the Impressive .ING Domains","Discover the vast potential of the new .ING domain zone in my latest blog post! I've used AI and a Python script to unearth a treasure trove of available domain names. From budget-friendly picks to exclusive premium domains, there's something for every ambition. Plus, a special list of unique, lesser-known domains awaits.",[35,36],"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fblog\u002Fin\u002Fing-zone-092f42bc.jpg","2023-12-11T00:00:00Z","2026-09-26T22:29:53.788Z",[49,52,55,58,61,72],{"slug":4,"title":5,"type":21,"summary":7,"tags":50,"author":13,"cover_url":14,"published_at":15,"updated_at":51,"reading_minutes":16},[9,10,11,12],"2026-09-28T15:57:50.784Z",{"slug":19,"title":20,"type":21,"summary":22,"tags":53,"author":13,"cover_url":27,"published_at":28,"updated_at":29,"reading_minutes":54},[24,9,25,26],3,{"slug":31,"title":32,"type":21,"summary":33,"tags":56,"author":13,"cover_url":37,"published_at":38,"updated_at":39,"reading_minutes":57},[35,36],1,{"slug":41,"title":42,"type":21,"summary":43,"tags":59,"author":13,"cover_url":45,"published_at":46,"updated_at":47,"reading_minutes":60},[35,36],2,{"slug":62,"title":63,"type":21,"summary":64,"tags":65,"author":13,"cover_url":69,"published_at":70,"updated_at":71,"reading_minutes":54},"secured-web-server-in-5-minutes","Fortify Web Server Security in 5 Minutes with Tailscale","Tailscale revolutionizes secure networking with its user-friendly approach, effortlessly connecting devices across diverse networks.",[66,67,68],"firewall","tailscale","webserver","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fblog\u002Fta\u002Ftailscale-logo-black-4bc15a01.png","2023-11-03T00:00:00Z","2026-09-26T22:29:53.977Z",{"slug":73,"title":74,"type":21,"summary":75,"tags":76,"author":13,"cover_url":79,"published_at":80,"updated_at":81,"reading_minutes":82},"lets-encrypt-free-ssl","How to Secure Your Website with Free SSL Certificates for a Lifetime","Let’s Encrypt certificates have revolutionized internet security by providing free, automated, and widely trusted SSL\u002FTLS certificates. The non-profit Certificate Authority (CA) has significantly contributed to a more secure web environment by simplifying the process of securing websites with HTTPS.",[77,78,68],"ssl","https","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fblog\u002Fce\u002Fcertbot-logo-7d6d4311.png","2023-11-01T00:00:00Z","2026-09-26T22:29:53.839Z",6]