Files
infiplot-web/packages/engine/src/orchestrator.ts
T
Zonghao Yuan d1f13d51a3 feat: scene/beat architecture — decouple dialogue from image generation (#2)
Replace the one-image-per-interaction model with scenes that hold multiple
dialogue beats. The image regenerates only on scene-change actions; tapping
through beats and in-scene choices are instant and zero-network.

Squashed from #2:
- feat: scene/beat architecture — decouple dialogue from image generation
- fix: harden LLM-output parsing, prefetch lifecycle, and typewriter (PR review)
- fix: dedupe beat ids; fallback narration on empty insert-beat (PR review #2)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-28 15:20:12 +08:00

91 lines
3.9 KiB
TypeScript

import type {
EngineConfig,
InsertBeatRequest,
InsertBeatResponse,
SceneRequest,
SceneResponse,
Session,
StartRequest,
StartResponse,
VisionRequest,
VisionResponse,
} from "@yume/types";
import { annotateClick } from "./annotate";
import { directInsertBeat, directScene } from "./director";
import { render } from "./renderer";
import { interpret } from "./vision";
function newSessionId(): string {
return `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
// ──────────────────────────────────────────────────────────────────────
// startSession — first scene + image
// ──────────────────────────────────────────────────────────────────────
export async function startSession(
config: EngineConfig,
req: StartRequest,
): Promise<StartResponse> {
const session: Session = {
id: newSessionId(),
createdAt: Date.now(),
worldSetting: req.worldSetting.trim(),
styleGuide: req.styleGuide.trim(),
history: [],
};
const scene = await directScene(config.text, session);
const imageBase64 = await render(config.image, scene, session.styleGuide);
return {
sessionId: session.id,
scene,
imageBase64,
};
}
// ──────────────────────────────────────────────────────────────────────
// requestScene — generate the NEXT scene + image.
// Frontend passes a session whose latest history entry has `exit` set.
// Also used for prefetch speculation (frontend synthesizes the exit).
// ──────────────────────────────────────────────────────────────────────
export async function requestScene(
config: EngineConfig,
req: SceneRequest,
): Promise<SceneResponse> {
const scene = await directScene(config.text, req.session);
const imageBase64 = await render(config.image, scene, req.session.styleGuide);
return { scene, imageBase64 };
}
// ──────────────────────────────────────────────────────────────────────
// visionDecide — interprets a background click into intent + classify.
// ──────────────────────────────────────────────────────────────────────
export async function visionDecide(
config: EngineConfig,
req: VisionRequest,
): Promise<VisionResponse> {
const annotated = await annotateClick(req.prevImageBase64, req.click);
const current = req.session.history.at(-1)?.scene ?? null;
return interpret(config.vision, annotated, current);
}
// ──────────────────────────────────────────────────────────────────────
// requestInsertBeat — generates a transient in-scene beat (no image regen)
// ──────────────────────────────────────────────────────────────────────
export async function requestInsertBeat(
config: EngineConfig,
req: InsertBeatRequest,
): Promise<InsertBeatResponse> {
const partial = await directInsertBeat(
config.text,
req.session,
req.freeformAction,
);
return { partial };
}