import { type CommandRunDeps } from './run-deps.js'; import { type ProjectContext } from '../project/context.js'; import { type GenerationOutcome } from '../generate/stream.js'; import { type SmartObjectSpec } from '@bitmagic/asset-core'; import { type PropVoxelizeResult } from '../generate/prop.js'; import { type VehicleInstallResult } from '../generate/vehicle.js'; import { type GenerateModelResult } from '../generate/model.js'; import { type AnimationFromVideoRun } from '../generate/animation-from-video.js'; import { type VideoRun } from '../generate/video.js'; import type { Environment } from '../config/environments.js'; import type { VoxelizeGlb } from '../assets/voxelize-glb.js'; import type { KeepGlb } from '../assets/keep-glb.js'; export interface SkyboxArgs { description: string; referenceImageUrl?: string; } export declare function buildSkyboxBody(args: SkyboxArgs): Record; export interface BackgroundArgs { description: string; referenceImageUrl?: string; } export declare function buildBackgroundBody(args: BackgroundArgs): Record; export interface BlockTypeArgs { name: string; description: string; displayName?: string; sideDescription?: string; referenceImageUrl?: string; } export declare function buildBlockTypeBody(args: BlockTypeArgs): Record; export interface SoundArgs { prompt: string; duration?: string; promptInfluence?: string; loop?: boolean; } export declare function buildSoundBody(args: SoundArgs): Record; export interface MusicArgs { prompt: string; duration?: string; instrumental?: boolean; /** citty's boolean pair: `--loop` arrives as true, `--no-loop` as false, neither as undefined. */ loop?: boolean; assetId?: string; } /** * The same omission contract as {@link buildSoundBody}: `loop` is only sent when the creator said * so either way, because the generator's default (a music bed loops) is the generator's to own. * The range is the generator schema's, restated so `--duration 5` is refused here, before a * request is made — the plan's verification line 7. */ export declare function buildMusicBody(args: MusicArgs): Record; export interface SpeechArgs { text: string; voice?: string; model?: string; stability?: string; similarityBoost?: string; style?: string; speed?: string; assetId?: string; } /** * The same omission contract as {@link buildSoundBody}: a voice setting left alone is left out * entirely, so Eleven Labs' own default stands rather than the CLI pinning a copy of it. * * The character cap is restated from the generator schema so an over-long line is refused here, * before a request is made — this lane is billed by the length of the audio, and a 20,000 * character paste is an expensive way to learn about a 400. */ export declare function buildSpeechBody(args: SpeechArgs): Record; export interface ImageArgs { name: string; prompt: string; width?: string; height?: string; referenceImageUrl?: string; } export declare function buildImageBody(args: ImageArgs): Record; export { buildCharacterBody, type CharacterArgs } from '../generate/character-body.js'; export interface AnimationArgs { /** Asset-record name. Defaults to the spec's own `name` when omitted. */ description?: string; /** Pre-minted `asset_…` id; becomes what playCustomAnimation resolves. */ assetId?: string; /** Path to a spec JSON file, or `-` for stdin. Mutually exclusive with the flag form. */ spec?: string; family?: string; name?: string; motionId?: string; duration?: string; basedOn?: string; designSpeed?: string; dial?: string | string[]; } /** Reads `--spec`; injected so the mapping stays testable without touching disk. */ export type SpecReader = (source: string) => string; /** * Assemble the request body, validating the spec before it leaves the machine. * * Returns the api-server wire shape `{ description, assetId?, spec }`. The spec is * emitted as the caller WROTE it, defaults unresolved — `safeParse` is used to * check it, not to rewrite it, so the builder stays the one thing that decides * what a default is. */ export declare function buildAnimationBody(args: AnimationArgs, readSpec: SpecReader): Record; export interface AnimationVideoArgs { video: string; name?: string; model?: string; loop?: boolean; slot?: string; autoTrim?: boolean; trimStart?: number; trimEnd?: number; assetId?: string; /** Any of the authored-spec flags, to refuse the two forms being mixed. */ spec?: string; family?: string; /** The prompt lane, to refuse a clip and a description being given at once. */ videoPrompt?: string; } export interface AnimationPromptArgs extends Omit { videoPrompt: string; videoDuration?: number; video?: string; } /** A `--video-prompt` run: everything `--video` carries, plus the clip to generate first. */ export interface AnimationFromPromptRun { /** What the source clip should show, in the creator's words. */ description: string; /** Source clip length in seconds; billed per second by the video leg. */ durationSeconds: number; run: AnimationFromVideoRun; } /** * `--video-prompt` is `--video` with the clip generated instead of filmed, so it * shares every downstream flag and refuses the same combinations. The extra * refusal is the duration floor: `generate video` will happily make a 1 s clip, * and Uthana will then reject it, which would bill the creator for footage that * never had a chance. */ export declare function buildAnimationFromPromptRun(args: AnimationPromptArgs): AnimationFromPromptRun; /** `--video` takes either a local file or an already-public clip URL. */ export declare function isHttpUrl(value: string): boolean; /** * `--video` is a different generator from the authored spec: the clip comes from a person, not * a parameter file, so the two flag sets cannot be combined and the spec flags are refused here * rather than silently ignored. */ export declare function buildAnimationVideoRun(args: AnimationVideoArgs): AnimationFromVideoRun; export interface GenerateRunDeps extends CommandRunDeps { /** Test-only override for the headless bake, so the lane runs without Chrome. */ voxelize?: VoxelizeGlb; /** Whether a person is at the keyboard; injected so the clip-review gate is testable. */ interactive?: () => boolean; /** Reads one line of confirmation. Injected for the same reason. */ ask?: (question: string) => Promise; /** Draws the contact sheet in the terminal when it can. Returns whether it drew. */ renderImage?: (bytes: Buffer) => boolean; /** Injected in tests so the kept-mesh lane (`--keep-glb` / low-poly projects) runs without a network. */ keepGlbUpload?: KeepGlb; } /** * What the three browser-baking runners below (prop, vehicle, model) each need before they can * start, resolved identically by all of them: where the project is, which api-server it belongs * to, and a token to talk to it with. * * `output` and `runDeps` come back too because both depend on `--json` — see commandRunDeps for * why a deps object built before that flag is known routes prose onto stdout. */ export declare function resolveRun(json: boolean, deps?: GenerateRunDeps): Promise<{ output: import("../output.js").Output; runDeps: GenerateRunDeps; context: ProjectContext; environment: Environment; token: string; }>; /** * Runs one `bitmagic generate `: requests the asset, then applies the returned * patches to world.json. Returns the successful outcome, whose `patches` carry the generated * asset's URL — the five `generate` subcommands ignore it and print nothing more, but * `bitmagic cover` needs the URL to save the image locally and hand it to `bitmagic publish`. * * `json` controls only whether the result DOCUMENT is emitted here; prose always goes through * `deps.log`. A caller that emits its own result (cover does) passes `false` and supplies deps * whose `log` is already routed for its own `--json` mode — stdout must carry exactly one JSON * document, and two would be worse than none. * * Every failure path says explicitly whether world.json changed, because a creator (or the agent * driving this CLI) who does not know has to go look for themselves: * * - Anything that fails before a `result`/`error` frame arrives (auth, ownership, spark balance, * a network error, ...) — world.json is untouched; the message says so. * - A terminal `success: false` outcome — asset-core's `fail()` always carries an empty patch * array (see shared/asset-core/src/result.ts), so this is also an unchanged world.json. * - A successful outcome whose patches could not be written (a corrupt world.json, an * unrecognised patch shape) — distinct from the above: the Forger may already have billed for * this generation, so the message says the asset was NOT lost, only not yet applied. */ export declare function runAssetGeneration(type: string, label: string, body: Record, json?: boolean, deps?: GenerateRunDeps): Promise; /** * The request half of {@link runAssetGeneration}: the project, the job record, the network * round-trip and the "world.json is unchanged" failure contract — everything EXCEPT applying * patches. Split out for `bitmagic reference make`, whose `reference` type returns no patches * and must not touch world.json at all (`applyPatchesToWorld` rewrites the file even for an * empty patch list, and would announce "world.json updated (0 patches)"). * * `onSuccess` runs INSIDE the job record with the successful outcome, the world.json path and * the record-updating `log`, so a caller that applies patches does it in the same window the * dev view shows as running — exactly where it always happened. */ export declare function requestAssetWithJobRecord(type: string, label: string, body: Record, runDeps: GenerateRunDeps, onSuccess: (outcome: GenerationOutcome, worldPath: string, log: (message: string) => void) => T): Promise; export interface MusicRunResult { musicUrl: string; originalUrl: string; musicId: string; durationSeconds: number; patchesApplied: number; /** Where the MP3 was saved, when `--out` asked for it. */ file?: string; bytes?: number; } /** * `generate music` is the one `generate` lane with two outputs a creator may want separately: the * world.json asset (the default — what a game plays as its bed) and the MP3 file (`--out`, what a * video editor or `bitmagic trailer make` wants). `--no-world` keeps the asset out of the game * for the file-only case, so a trailer bed is not registered as a track the game never plays. * * Cannot go through {@link runAssetGeneration} because that always applies the patches and never * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the * "world.json is unchanged" failure contract are the same as every other lane's. */ export declare function runMusicGeneration(options: { body: Record; /** Save the MP3 here (relative to the current directory). */ out?: string; /** Apply the asset to world.json. The default; `--no-world` turns it off. */ world: boolean; json: boolean; deps?: GenerateRunDeps; }): Promise; export interface SpeechRunResult { speechUrl: string; originalUrl: string; speechId: string; durationSeconds: number; patchesApplied: number; /** Where the MP3 was saved, when `--out` asked for it. */ file?: string; bytes?: number; } /** * `generate speech` has the same two-outputs shape as `generate music`, and for a sharper reason: * the world.json asset is a line an NPC speaks (`engine.playSound`), while the MP3 is the input * `tools/lipsync/make_line.py --audio` needs to cut mouth cues from. A creator rigging a talking * character wants the file and NOT the asset, so `--no-world` is a first-class path here rather * than an escape hatch. * * Cannot go through {@link runAssetGeneration} because that always applies the patches and never * downloads; it shares {@link requestAssetWithJobRecord} so the request, the job record and the * "world.json is unchanged" failure contract are the same as every other lane's. */ export declare function runSpeechGeneration(options: { body: Record; /** Save the MP3 here (relative to the current directory). */ out?: string; /** Apply the asset to world.json. The default; `--no-world` turns it off. */ world: boolean; json: boolean; deps?: GenerateRunDeps; }): Promise; /** * Refuses `--voxel-grid` alongside `--mesh`, rather than ignoring it. * * The grid only reaches the `prop-voxel` generator, so on the mesh path the flag does nothing at * all — and it is the one flag whose whole point is a decision that cannot be revisited later * (see DEFAULT_MASTER_GRID). Silently dropping it is how a creator ends up believing they asked * for a finer master and finding out months later, when a re-voxelize refuses. */ export declare function assertVoxelGridAllowed(voxelGrid: number | undefined, mesh: boolean): void; /** * Refuses a run that names two sources to bake from. * * Each flag skips the paid generation and points the bake at something the caller already has, and * they point at different things. Picking a winner silently would bake one and leave the creator * believing they had baked the other. */ export declare function assertOneRetrySource(glbUrl: string | undefined, masterUrl: string | undefined): void; /** * The grid flag's value, validated against the generator's own allowlist. * * An allowlist rather than any positive integer because the master packer maps native coordinates * into the target grid by integer division: a grid that does not divide the native resolution * pushes the top coordinate past the grid, and asset-forger reports that as "axis mapping is * wrong". Caught here so a typo costs nothing rather than a GPU minute. */ export declare function parseVoxelGridFlag(raw: string | undefined): number | undefined; /** Load and validate a `--smart-spec` from a URL or a local file. */ export declare function loadSmartSpec(source: string, fetchImpl?: typeof globalThis.fetch): Promise; /** `--smart` / `--smart-hint` as one field: the hint when given, else the boolean. */ export declare function smartField(smart: boolean | undefined, hint: string | undefined): { smart?: boolean | string; }; /** Refuses `--smart` on the mesh path, where there is no master to analyse. */ export declare function assertSmartAllowed(smart: boolean | string | undefined, mesh: boolean): void; /** * `generate prop` cannot go through {@link runAssetGeneration}: the prop generator returns a mesh, * not patches, so there is nothing to apply until the CLI has voxelized it in a browser. This owns * that longer flow — and validates the target asset BEFORE the paid generation, so a mistyped id * or an asset with no `fitBox` costs nothing. */ export declare function runPropGeneration(options: { assetId: string; prompt: string; /** Skip the paid generation and voxelize this mesh instead — how a failed run is retried. */ glbUrl?: string; /** `--master-url`: bake this voxel master instead of generating one. The voxel `--glb-url`. */ masterUrl?: string; /** `--mesh`: ask for a mesh rather than voxels, which are the default. */ mesh?: boolean; /** `--voxel-grid`: the grid the master is forged at. Not valid with `--mesh`. */ voxelGrid?: number; /** `--smart` / `--smart-hint`: find and bake the prop's moving parts and lights. Voxel path only. */ smart?: boolean | string; /** `--smart-spec`: a stored SmartObjectSpec (URL or file) to bake `--master-url` with. */ smartSpec?: SmartObjectSpec; /** `--no-materials`: leave the asset matte rather than classifying it after the bake. */ materials?: boolean; /** `--keep-glb` / `--voxelize`; absent follows game.json artStyle. */ keepGlb?: boolean; voxelize?: boolean; json: boolean; deps?: GenerateRunDeps; }): Promise; /** * `generate vehicle`, like `generate prop`, cannot go through {@link runAssetGeneration}: the * generator returns a GLB rather than patches, so there is nothing to apply until the CLI has * voxelized it in a browser. * * Unlike prop there is nothing to validate up front — a vehicle creates a NEW asset, so there is no * existing one to look up. The one thing that would waste a paid design is asking for neither a * prompt nor a preset, which is refused here before anything is spent. */ export declare function runVehicleGeneration(options: { prompt?: string; presetName?: string; name?: string; /** Skip the paid design and voxelize this GLB instead — how a failed run is retried. */ glbUrl?: string; minVoxelSize?: number; json: boolean; deps?: GenerateRunDeps; }): Promise; /** * `generate model` mints a NEW asset from a prompt. Like prop and vehicle it cannot go through * {@link runAssetGeneration} — the generator returns a mesh, not patches, so nothing can be applied * until the CLI has voxelized it in a browser. * * Unlike prop there is no existing asset to validate against, which is the whole point: prop * refuses without a `fitBox` because every placed instance was positioned against one, while this * has no instances yet and scales to `--height` (or the mesh's own size) instead. */ export declare function runModelGeneration(options: { prompt: string; name?: string; assetId?: string; minVoxelSize: number; maxVoxelSize: number; targetHeight?: number; fillInterior: boolean; /** Skip the paid generation and voxelize this mesh instead — how a failed run is retried. */ glbUrl?: string; /** `--master-url`: bake this voxel master instead of generating one. The voxel `--glb-url`. */ masterUrl?: string; /** `--mesh`: ask for a mesh rather than voxels, which are the default. */ mesh?: boolean; /** `--voxel-grid`: the grid the master is forged at. Not valid with `--mesh`. */ voxelGrid?: number; /** `--no-materials`: leave the asset matte rather than classifying it after the bake. */ materials?: boolean; /** `--keep-glb` / `--voxelize`; absent follows game.json artStyle. */ keepGlb?: boolean; voxelize?: boolean; /** `--smart` / `--smart-hint`: find and bake the model's moving parts and lights. Voxel path only. */ smart?: boolean | string; /** `--smart-spec`: a stored SmartObjectSpec (URL or file) to bake `--master-url` with. */ smartSpec?: SmartObjectSpec; json: boolean; deps?: GenerateRunDeps; }): Promise; /** * The two flags several subcommands share, declared once — the same shape `assets.ts` and * `levels.ts` already use for their own `JSON_FLAG`. * * `--help` is an agent's discovery mechanism, so the wording is part of the interface: restated * once per subcommand it drifts, and two subcommands describing the same flag differently read as * two different flags. `as const` preserves the literal `type`, which is what citty infers * `args.json`'s boolean and `args['reference-image-url']`'s string from. */ export declare const JSON_FLAG: { readonly type: "boolean"; readonly description: "Print the result as JSON on stdout; all progress goes to stderr."; }; /** * The `--video` lane of `bitmagic generate animation`. Job-shaped rather than streamed (see * generate/animation-from-video.ts), so it cannot share `runAssetGeneration`; it does share the * job record, so the dev view shows it beside every other generation. */ export declare function runAnimationFromVideo(run: AnimationFromVideoRun, json: boolean, deps?: GenerateRunDeps): Promise; /** * Look at the generated clip before the capture leg spends on it. * * The clip costs a few sparks and the Uthana leg costs fifteen more, so the expensive half is the * one worth gating. What decides "is this usable" is visible in stills — is it one person, are they * whole and centred, did the model cut to a close-up halfway — so the contact sheet * `generate video` already writes is enough to judge it, and the clip is a real asset by now, so a * creator who wants to watch it move can do that in the dev view while this waits. * * Returns whether to go on. Everything except the question is best-effort: a missing ffmpeg or a * failed download costs a note, never the generation that has already been paid for. */ export declare function confirmSourceClip(options: { root: string; assetId: string; videoUrl: string; durationSeconds: number; /** `--yes`, or any lane with nobody to ask. Skips the question, never the frames. */ skipPrompt: boolean; /** `--no-contact-sheet`: skip the download and the frames, keep the question. */ contactSheet: boolean; deps: GenerateRunDeps; }): Promise; /** * The `--video-prompt` lane: text -> clip -> motion, the two existing job lanes run back to back. * * There is no chained endpoint behind this — asset-forger has one route that makes a video and * another that reads one, and this is the seam between them. Both legs bill under their own * existing key (`generate-video` per second, `generate-animation-from-video` flat), the way * design-vehicle prices its chained image sub-generations rather than minting a composite. * * Both asset rows are kept. The clip is a real `type: 'video'` asset the creator can play back * or reuse as a cutscene, and keeping it is also what makes a leg-2 failure recoverable: the * footage is already paid for, so the error hands back a `--video ` retry instead of asking * for the whole thing again. */ export declare function runAnimationFromPrompt(prompt: AnimationFromPromptRun, json: boolean, deps?: GenerateRunDeps, creatorPrompt?: string, options?: { yes?: boolean; contactSheet?: boolean; }): Promise; /** * Job-shaped rather than streamed (see generate/video.ts), so it cannot share `runAssetGeneration`; * it does share the job record, so the dev view shows it beside every other generation. */ export declare function runVideoGeneration(run: VideoRun, json: boolean, options?: { deps?: GenerateRunDeps; contactSheet?: boolean; }): Promise; export declare const generateCommand: import("citty").CommandDef;