Files
openmaic/OpenMAIC/lib/bundle/serialize.ts
2026-08-16 14:58:47 +08:00

139 lines
4.6 KiB
TypeScript

// Portable classroom serialization for the frozen bundle.
//
// Mirrors the interactive-ZIP export flow (`lib/export/use-export-classroom.ts`
// + `classroom-zip-utils.ts`) but is pure and server-safe: the browser export
// reads bytes from IndexedDB at call time, while the packager receives bytes
// through injected resolvers. The manifest shape is identical, so the L3
// player can reuse the existing import machinery
// (`rewriteAudioRefsToIds` / manifest → document) unchanged.
import type { Action, DiscussionAction, SpeechAction } from '@/lib/types/action';
import type { GeneratedAgentConfig, Scene, Stage } from '@/lib/types/stage';
import type {
ClassroomManifest,
ManifestAction,
ManifestAgent,
ManifestScene,
ManifestStage,
MediaIndexEntry,
} from '@/lib/export/classroom-zip-types';
import {
manifestAgentFromConfig,
CLASSROOM_ZIP_FORMAT_VERSION,
} from '@/lib/export/classroom-zip-types';
export interface ManifestBuildOptions {
appVersion: string;
/** audioId → ZIP path (`audio/<id>.<ext>`); speech actions are rewritten to `audioRef`. */
audioIdToPath: Map<string, string>;
agentConfigs: GeneratedAgentConfig[];
exportedAt?: string;
}
export function serializeActions(
actions: Action[],
audioIdToPath: Map<string, string>,
agentIdToIndex: Map<string, number>,
): ManifestAction[] {
return actions.map((action) => {
if (action.type === 'speech') {
const speech = action as SpeechAction;
const { audioId, audioUrl, ...rest } = speech;
const audioRef = audioId ? audioIdToPath.get(audioId) : undefined;
return {
...rest,
...(audioRef ? { audioRef } : {}),
// A frozen bundle must play its embedded bytes. Keep a remote URL only
// for legacy URL-only speech that could not be mapped to bundle audio.
...(!audioRef && audioUrl ? { audioUrl } : {}),
} as ManifestAction;
}
if (action.type === 'discussion') {
const discussion = action as DiscussionAction;
const { agentId, ...rest } = discussion;
const agentIndex = agentId ? agentIdToIndex.get(agentId) : undefined;
return {
...rest,
...(agentIndex !== undefined ? { agentIndex } : agentId ? { agentId } : {}),
} as ManifestAction;
}
return action as ManifestAction;
});
}
export interface SerializedScene {
scene: ManifestScene;
/** MediaIndexEntry additions for this scene's audio/media, keyed by ZIP path. */
}
/** Serialize one scene into its portable manifest shape (interactive HTML assumed pre-inlined). */
export function serializeScene(
scene: Scene,
audioIdToPath: Map<string, string>,
agentIdToIndex: Map<string, number>,
): ManifestScene {
return {
type: scene.type,
title: scene.title,
order: scene.order,
content: scene.content,
actions: scene.actions
? serializeActions(scene.actions, audioIdToPath, agentIdToIndex)
: undefined,
whiteboards: scene.whiteboards,
...(scene.multiAgent?.enabled
? {
multiAgent: {
enabled: true,
agentIndices: (scene.multiAgent.agentIds ?? [])
.map((id) => agentIdToIndex.get(id))
.filter((index): index is number => index !== undefined),
directorPrompt: scene.multiAgent.directorPrompt,
},
}
: {}),
};
}
export function serializeStage(stage: Stage): ManifestStage {
return {
name: stage.name,
description: stage.description,
language: stage.languageDirective,
style: stage.style,
whiteboard: stage.whiteboard,
videoManifest: stage.videoManifest,
interactiveMode: stage.interactiveMode,
taskEngineMode: stage.taskEngineMode,
createdAt: stage.createdAt,
updatedAt: stage.updatedAt,
};
}
/**
* Build the portable classroom manifest.
*
* `scenes` must already carry inlined interactive HTML and prepared PBL
* scenes (the packager's responsibility); `mediaIndex` is supplied by the
* caller, which owns byte collection.
*/
export function buildManifest(
stage: Stage,
scenes: Scene[],
options: ManifestBuildOptions,
mediaIndex: Record<string, MediaIndexEntry>,
): ClassroomManifest {
const agentIdToIndex = new Map<string, number>();
options.agentConfigs.forEach((agent, index) => agentIdToIndex.set(agent.id, index));
return {
formatVersion: CLASSROOM_ZIP_FORMAT_VERSION,
exportedAt: options.exportedAt ?? new Date().toISOString(),
appVersion: options.appVersion,
stage: serializeStage(stage),
agents: options.agentConfigs.map(manifestAgentFromConfig) satisfies ManifestAgent[],
scenes: scenes.map((scene) => serializeScene(scene, options.audioIdToPath, agentIdToIndex)),
mediaIndex,
};
}