How Asset Studio Was Implemented
Opening
In the previous post, How We Make Games, I explained what kinds of games we are building and in what order. In that post, Asset Studio only appeared briefly. It was mentioned as a tool that manages character and background assets for Story Quiz and Mystery together.
In this post, I want to unpack that sentence based on the actual implementation. Asset Studio is not just a screen for generating images. In Omnilude today, it is the place where we collect assets, refine prompts, run generation jobs, review the results, and then register them into the library. The frontend backoffice acts as the workbench, game-service owns the asset domain, and ai-service owns the generation runtime.
Why We Built Asset Studio Separately
Once you start building games for real, assets scatter faster than expected. Story Quiz has characters, backgrounds, and block images. Mystery has scenario characters, locations, and note images. Even when one person is building a single world, it becomes difficult to stay in control if prompts, style presets, generated results, approval states, and actual usage are managed across different screens.
That is why Asset Studio was designed from the beginning to feel closer to a workshop than a gallery. The important point is that this tool is not tied to a single genre. It handles assets already created in Story Quiz and assets pulled from Mystery drafts in the same way, keeps generated results as a reviewable library, and later tracks whether those assets are being used in other domains. Without that perspective, AI generation may feel convenient, but it does not survive as an operational asset.
How the Backoffice Screen Is Structured
The frontend backoffice has dedicated routes for Asset Studio. Backgrounds and characters open through different entries, but internally they share the same workbench components. The list screen supports both tree and flat views and also shows recently edited items. In other words, you can organize work around directories or return through recent activity.
Once you enter the detail screen, the structure becomes clearer. This screen is effectively a full asset production console. At the top, it manages names and approval states. In the body, it handles import sources, prompts, generation, media, and history in separate tabs. In the implementation, search state for import sources and action state for generated media are kept at the page level. The goal is to avoid losing context when moving between tabs.
There is one important split here. Asset management APIs such as item listing, directory updates, and generated result registration all go to game-service through /api/asset-studio/*. The actual generation request, on the other hand, does not go through the Asset Studio API. It goes to ai-service through /api/media-generation/generate. That split is intentional. Asset management and generation execution live in the same screen, but they do not share the same responsibility.
How the Actual Generation Flow Moves
The user experience is fairly simple. You open an asset, pull in an existing asset or a Story Quiz or Mystery source, adjust the prompt, and press generate. Internally, though, the flow is split like this.
- The backoffice detail screen reads the current
assetItem.generationConfig. - The generation request is sent to
/api/media-generation/generate, and the frontend receives ajobIdimmediately. - The frontend subscribes to the backbone SSE stream with that
jobIdand receives progress events. ai-servicecreates the actual result through the provider and uploads it to storage.- The frontend receives
outputUrlandsourceUrlthrough SSE and shows a preview. - If the result looks acceptable, the user then registers it through
/api/asset-studio/asset-items/{id}/media.
That last step matters. Asset Studio does not save the result into the library the moment generation finishes. The generated result is treated as something to review first. It becomes an operational asset only when the user presses the registration action. That is also clear in the implementation. The generation hook builds a preview state from SSE events, and a separate action calls createGeneratedMediaApi to move the result into the library.
Because of that structure, Asset Studio is not a prompt box. It becomes a production pipeline with review built into it. Failed results, ambiguous results, and pipeline post-processing results can all stay temporary until someone chooses to register them.
What game-service Owns
game-service's Asset Studio is not just a CRUD bundle. In practice, it is much closer to the owner of the asset library. Even at the controller level, directories, items, import, export/import portability, generated media, winner selection, reorder, bulk move, and import source lookup are grouped into one domain.
There are three reasons this service matters in particular.
1. It Pulls Sources from Existing Game Domains
AssetStudioImportService reads DraftLocation and DraftCharacter from Mystery, along with StoryQuizBackgroundAsset and StoryCharacter from Story Quiz, and turns them into import source cards. It does not bring only names. It also ties together prompt, provider, stylePreset, and metadata. That makes Asset Studio a place that continues asset production from existing game data, not a place where every asset starts from zero.
2. It Refines Generated Results into Operational Assets
AssetGeneratedMediaService stores generated results as library records and allows one of them to be marked as the winner. When the winner changes for an image type, the item's thumbnail is updated as well. On the other hand, it blocks deletion of winner media for assets that are already in use. In other words, it separates collecting generated results from adopting an operational asset.
3. It Tracks Whether an Asset Is Actually in Use
AssetReferenceSyncService checks whether a URL is actually being used inside Story Quiz and Mystery and syncs isReferenced accordingly. On the Story Quiz side, it scans covers, cards, default character images, emotion images, backgrounds, and block media. On the Mystery side, it checks scenario thumbnails, character avatars, notes, and object images. That is why Asset Studio is not just a pretty asset repository. It is an asset management tool tied to live content.
What ai-service Owns
The responsibility of ai-service is even clearer. This service does not know the asset library. It owns execution. It interprets the generationConfig sent by the backoffice, decides which provider and workflow to use, runs the work asynchronously, and streams progress through SSE.
In the implementation, MediaGenerationService first checks the per-account concurrency slots. Then GenerationConfigParser parses the settings. The precedence here matters quite a bit. generationConfig.media[mediaType].providers[providerCode] has the highest priority, then default, and finally the root level acts as the fallback. That structure allows a single asset to keep different settings by media type and by provider.
Once parsing is done, ai-service does not generate immediately. It places the work into a DTE job queue. The jobId returned to the frontend is then kept as the same key used for SSE subscription. In other words, the backoffice does not block on a long request. It tracks the asynchronous generation process around a job ID.
Why the ComfyUI Integration Matters
From the current implementation, the interesting part of Asset Studio is the ComfyUI integration. ComfyUIProvider does not hardcode workflows in the application code. Runtime settings are read from provider configuration, and executable workflows are loaded from workflow records in the database. It also supports not only prompt-to-image generation but media-to-media pipelines.
That matters because Asset Studio's generation settings are not just a plain prompt string. Users can send runtime overrides such as workflowId, workflowPipeline, seed, width, height, and an input image URL together. In the actual implementation, selecting a pipeline workflow lets you use existing generated media as input for post-processing or transformation.
The result is that the AI generation screen stops being a generic prompt input box and becomes a data-driven workflow runner. The same character can be regenerated with a different provider, a different workflow, or a different input image, and the result can be reviewed and accumulated into the library afterward.
A separate post that looks at ComfyUI itself in more detail would probably make sense later.
Why We Split game-service and ai-service
This is not just an MSA preference. The asset domain and the generation runtime fail in different ways, have different lifecycles, and are seen from different operational viewpoints.
game-service deals with information that needs to persist for a long time, such as directory structure, approval state, actual usage, portability, and history. ai-service, by contrast, deals with which providers are alive, which workflow should be selected, how many jobs can run right now, and how far generation has progressed. If those two concerns are combined, the implementation may feel convenient for a while, but the boundary quickly becomes blurry.
In the current structure, Asset Studio connects the two. The backoffice works through one screen, but it handles storage and execution separately. That split makes it possible to evolve the asset library and the generation infrastructure independently as more genres are added beyond Story Quiz and Mystery.
Closing
In the previous post, Asset Studio was just a passing sentence. But if you follow the actual implementation, it does not mean a single image-generation feature. In Omnilude, Asset Studio is a production system that gathers game assets, pulls sources from existing domains, runs AI generation jobs, reviews the results, and tracks whether those assets are actually in use.
That is why I do not see Asset Studio as a side screen inside game development. It feels much closer to the intersection where the full production pipeline meets, including Story Quiz and Mystery. In the next post, I want to narrow the scope and explain how this asset system is used inside an actual publishing pipeline or inside the production flow of a specific genre.
Even for me, reading it again as a reviewer, this post ended up feeling dense and a little like a cipher.
I plan to update the blog to support attached images and then revisit existing posts as well so the overall readability improves.
Since this one took real effort to write, I am going to publish it anyway.