refactor: flatten monorepo to single web package (#12)
Flatten the pnpm monorepo (apps/web + packages/*) into a single web package at the repo root. - Move app/lib/components/scripts/public to root; drop apps/web and packages/* wrappers - Rewrite tsconfig paths (@infiplot/*) to ./lib/*; turbopack.root = __dirname - Update Vercel (no root-directory) and Cloudflare (pnpm build:cf at root) deploy paths - Regenerate pnpm-lock.yaml to drop stale workspace importers - Bump engines.node to >=22 to match wrangler Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { chat } from "@infiplot/ai-client";
|
||||
import type { BeatActiveCharacter, ProviderConfig } from "@infiplot/types";
|
||||
import { parseJsonLoose } from "../jsonParser";
|
||||
import {
|
||||
CINEMATOGRAPHER_SYSTEM,
|
||||
buildCinematographerUserMessage,
|
||||
} from "../prompts";
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Cinematographer agent — translates the Writer's narrative scene
|
||||
// summary into an English compositional prompt for FLUX.
|
||||
//
|
||||
// Reads: sceneSummary + entry beat's activeCharacters (poses)
|
||||
// + prior sceneKey (for continuity hints)
|
||||
// Writes: { shotType, integratedPrompt }
|
||||
//
|
||||
// Does NOT describe character APPEARANCE — that's appended at the
|
||||
// Painter stage from session.characters[].visualDescription. The
|
||||
// Cinematographer only positions named characters in the frame and
|
||||
// describes the environment + lighting + camera framing.
|
||||
//
|
||||
// This separation lets the Cinematographer run IN PARALLEL with the
|
||||
// CharacterDesigner — neither needs the other's output. They both
|
||||
// feed independently into the Painter prompt.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CinematographerOutput = {
|
||||
shotType: string;
|
||||
integratedPrompt: string;
|
||||
};
|
||||
|
||||
type RawCinematographerOutput = {
|
||||
shotType?: string;
|
||||
integratedPrompt?: string;
|
||||
};
|
||||
|
||||
export type CinematographerInput = {
|
||||
sceneSummary: string;
|
||||
styleGuide: string;
|
||||
entryBeatActive: BeatActiveCharacter[];
|
||||
/** Entry beat's speaker — drives the dynamic camera policy:
|
||||
* NPC name → NPC looks toward camera (close-up)
|
||||
* "你" → medium shot, NPC listens
|
||||
* undefined → wide establishing shot */
|
||||
entryBeatSpeaker?: string;
|
||||
priorSceneKey?: string;
|
||||
currentSceneKey?: string;
|
||||
};
|
||||
|
||||
export async function runCinematographer(
|
||||
config: ProviderConfig,
|
||||
input: CinematographerInput,
|
||||
): Promise<CinematographerOutput> {
|
||||
const raw = await chat(
|
||||
config,
|
||||
[
|
||||
{ role: "system", content: CINEMATOGRAPHER_SYSTEM },
|
||||
{
|
||||
role: "user",
|
||||
content: buildCinematographerUserMessage(
|
||||
input.sceneSummary,
|
||||
input.styleGuide,
|
||||
input.entryBeatActive,
|
||||
input.entryBeatSpeaker,
|
||||
input.priorSceneKey,
|
||||
input.currentSceneKey,
|
||||
),
|
||||
},
|
||||
],
|
||||
{ temperature: 0.6, responseFormat: "json_object" },
|
||||
);
|
||||
|
||||
const parsed = parseJsonLoose<RawCinematographerOutput>(raw);
|
||||
|
||||
// Fallback: if the LLM produced nothing usable, synthesize a minimal
|
||||
// integratedPrompt from the Writer's sceneSummary so the Painter has
|
||||
// SOMETHING to work with rather than blowing up the whole pipeline.
|
||||
const integratedPrompt =
|
||||
parsed.integratedPrompt?.trim() ||
|
||||
`A cinematic illustration depicting: ${input.sceneSummary}. Wide establishing shot, natural lighting, atmospheric mood.`;
|
||||
|
||||
return {
|
||||
shotType: parsed.shotType?.trim() || "medium shot",
|
||||
integratedPrompt,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user