How to Achieve Parallel Scene Content Generation in OpenMAIC: A Complete Technical Guide

OpenMAIC enables parallel scene content generation by dispatching independent async generation jobs for each scene through a three-layer architecture—UI triggers, per-scene Zustand state tracking, and server-side concurrent job queues—allowing multiple LLM/image model calls to execute simultaneously while streaming results back to the client.

OpenMAIC is an open-source framework for multi-modal AI content creation that handles resource-intensive generation tasks across multiple scenes concurrently. This article explains how to implement parallel scene content generation by leveraging the repository's fire-and-forget pipeline, which decouples UI requests from server-side execution to maximize throughput.

Understanding the Three-Layer Architecture

OpenMAIC separates concerns into three distinct layers to handle concurrency safely and efficiently:

Dispatching Fire-and-Forget Requests from the UI

To initiate parallel generation, the UI maps over a list of scene IDs and invokes the generation function for each without awaiting individual results. This pattern prevents serialization bottlenecks.

When a user clicks the generate button, the component fires off all requests immediately:

// components/generation/interactive-mode-button.tsx
import { generateScene } from '@/lib/server/agent-runtime/generation-tools';

const startParallel = async (sceneIds: string[]) => {
  // Create an array of promises, one per scene
  const promises = sceneIds.map(id => generateScene(id));
  // Optionally await all completions for a final "complete" signal
  await Promise.all(promises);
};

Each call to generateScene returns immediately after registering the task, allowing the loop to proceed to the next scene without waiting for the model to finish.

Tracking Per-Scene State with Zustand

The client-side store isolates each generation task by indexing state with sceneId. This prevents collisions when multiple scenes update concurrently.

In lib/store/media-generation.ts, the store creates a MediaGenerationTask for every request:

// lib/store/media-generation.ts
import { create } from 'zustand';

type MediaGenerationTask = {
  token: string;
  status: 'open' | 'complete' | 'error';
  prompt: string;
};

type MediaGenState = {
  tasks: Record<string, MediaGenerationTask>;
  startGeneration: (opts: { sceneId: string; prompt: string }) => void;
};

export const useMediaGenerationStore = create<MediaGenState>(set => ({
  tasks: {},
  startGeneration: ({ sceneId, prompt }) => {
    const token = crypto.randomUUID();
    set(state => ({
      tasks: { 
        ...state.tasks, 
        [sceneId]: { token, status: 'open', prompt } 
      },
    }));
    // Dispatch to server without awaiting
    api.runGeneration({ sceneId, token, prompt });
  },
}));

Because the tasks object uses sceneId as the key, updates to one scene never overwrite another, enabling safe parallel state mutations.

Executing Concurrent Jobs on the Server

The server processes generation requests through an async job queue that respects concurrency limits. Each runGeneration call runs in its own execution context, allowing the event loop to handle multiple model inferences simultaneously.

According to the OpenMAIC source code in lib/server/agent-runtime/generation-tools.ts, the server implementation looks like this:

// lib/server/agent-runtime/generation-tools.ts
import { MAX_CONCURRENT_JOBS } from '@/configs/concurrency';

export async function runGeneration({
  sceneId,
  token,
  prompt,
}: { sceneId: string; token: string; prompt: string }) {
  // The job queue manages concurrency automatically
  await jobQueue.add(async () => {
    const result = await model.generate(prompt);
    // Stream partial or final results back to client
    pushUpdate(sceneId, { token, content: result });
  });
}

The MAX_CONCURRENT_JOBS configuration variable controls how many of these async jobs run at once. Once a job completes, the queue automatically pulls the next waiting task, maintaining optimal throughput without overwhelming the GPU or API rate limits.

Streaming Results to the Stage Store

As the server generates content, it pushes updates via WebSocket or Server-Sent Events (SSE). The client listens for these updates and patches the corresponding scene in the stage store.

The media generation store listens for generation:update events and routes them to the correct scene:

// lib/store/media-generation.ts
socket.on('generation:update', ({ sceneId, delta }) => {
  useStageStore.getState().updateSceneContent(sceneId, delta);
});

The stage store (lib/store/stage.ts) holds the canonical scene data. Because updates are keyed by sceneId, the UI components—such as those in components/stage/scene-renderer.tsx—re-render only when their specific scene content changes, ensuring efficient DOM updates across many parallel generations.

Complete Implementation Example

Here is a minimal end-to-end implementation demonstrating parallel generation for a batch of scenes:

// components/generation/batch-generator.tsx
import { generateScene } from '@/lib/server/agent-runtime/generation-tools';
import { useMediaGenerationStore } from '@/lib/store/media-generation';
import { Button } from '@/components/ui/button';

export const BatchGenerator = ({ sceneIds }: { sceneIds: string[] }) => {
  const startBatch = async () => {
    // Fire all generation requests simultaneously
    const jobs = sceneIds.map(id => 
      generateScene(id)
    );
    
    // Wait for all to settle (optional, for "Export" or "Next" gating)
    await Promise.all(jobs);
  };

  return <Button onClick={startBatch}>Generate All Scenes</Button>;
};

This button triggers parallel execution, while the underlying stores and server queue handle the complexity of concurrency management and state isolation.

Summary

Frequently Asked Questions

How does OpenMAIC prevent race conditions when updating scene content?

OpenMAIC prevents race conditions by indexing all generation state and scene content by sceneId. The Zustand store in lib/store/media-generation.ts uses a dictionary structure where each scene has its own MediaGenerationTask entry, and the stage store applies updates only to the matching sceneId. This isolation ensures that concurrent updates for different scenes never corrupt each other's data.

What configuration controls the maximum number of concurrent generations?

The MAX_CONCURRENT_JOBS variable in the concurrency configuration file controls server-side parallelism. This setting defines how many runGeneration jobs can execute simultaneously in the job queue defined in lib/server/agent-runtime/generation-tools.ts. Adjusting this value allows you to optimize for your hardware capacity or API rate limits.

How does the UI know when all parallel generations are complete?

The UI can track completion by awaiting Promise.all on the array of generation promises returned by generateScene, as shown in components/generation/interactive-mode-button.tsx. Alternatively, components can subscribe to the useMediaGenerationStore and check that all relevant sceneId entries have a status of complete or error before enabling downstream actions like navigation or export.

Can different scene types be generated in parallel?

Yes, the architecture supports heterogeneous scene types because each generation job is context-independent. Whether generating slides, images, or video segments, the runGeneration function in lib/server/agent-runtime/generation-tools.ts executes each request in its own async context with a unique token. The components/stage/scene-renderer.tsx component then dispatches to the appropriate renderer based on the scene type once content arrives.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →