@docyrus/animaguru 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ CLI 0.7.1 can select a cloud workspace for project listing, project creation, an
|
|
|
80
80
|
animaguru workspaces list
|
|
81
81
|
animaguru workspaces use team_0123456789abcdef0123456789abcdef
|
|
82
82
|
animaguru projects list
|
|
83
|
-
animaguru projects create --name "Shared cast"
|
|
83
|
+
animaguru projects create --name "Shared cast"
|
|
84
84
|
animaguru credits
|
|
85
85
|
animaguru workspaces use personal
|
|
86
86
|
```
|
|
@@ -89,10 +89,12 @@ Use a team ID returned by `workspaces list`. The choice is saved per server in t
|
|
|
89
89
|
|
|
90
90
|
## Agent workflow
|
|
91
91
|
|
|
92
|
+
Remote projects are neutral containers. `projects create` has no `--style` flag. Choose a style when generating a sprite; local film setup keeps its separate style interview.
|
|
93
|
+
|
|
92
94
|
```sh
|
|
93
|
-
animaguru projects create --name "Forest cast"
|
|
95
|
+
animaguru projects create --name "Forest cast"
|
|
94
96
|
# Read the returned project.id and use it below.
|
|
95
|
-
animaguru generate PROJECT_ID --name "Knight" --description "A blue knight with a red scarf" --actions idle,walk --yes
|
|
97
|
+
animaguru generate PROJECT_ID --name "Knight" --description "A blue knight with a red scarf" --style pixel --actions idle,walk --yes
|
|
96
98
|
animaguru jobs wait PROJECT_ID JOB_ID --timeout 600
|
|
97
99
|
animaguru jobs list PROJECT_ID
|
|
98
100
|
animaguru projects get PROJECT_ID
|
|
@@ -123,7 +125,7 @@ animaguru models PROJECT_ID --image-model gpt-image-2.5 --video-model pruna/p-vi
|
|
|
123
125
|
# Either selection can be changed independently:
|
|
124
126
|
animaguru models PROJECT_ID --video-model heygen/heygen-video-1
|
|
125
127
|
# Or choose them when creating a project:
|
|
126
|
-
animaguru projects create --name "Forest cast"
|
|
128
|
+
animaguru projects create --name "Forest cast" \
|
|
127
129
|
--image-model gpt-image-2.5 --video-model heygen/heygen-video-1
|
|
128
130
|
```
|
|
129
131
|
|
|
@@ -136,7 +138,7 @@ These flags are available in CLI 0.2.0 and later. Rerun the macOS installer to u
|
|
|
136
138
|
CLI 0.4.0 adds standalone assets alongside the existing sprite commands. Use any project, or create an empty one first. `assets list` returns saved asset ids, image blob hashes for video references, and the models configured on that server.
|
|
137
139
|
|
|
138
140
|
```sh
|
|
139
|
-
animaguru projects create --name "Adventure assets"
|
|
141
|
+
animaguru projects create --name "Adventure assets"
|
|
140
142
|
animaguru assets list PROJECT_ID
|
|
141
143
|
|
|
142
144
|
animaguru assets generate PROJECT_ID --type image --name "Forest" \
|
|
@@ -177,6 +179,27 @@ animaguru assets generate PROJECT_ID --type sfx --name "Magic spell" \
|
|
|
177
179
|
|
|
178
180
|
Replace `--dry-run` with `--yes` to generate within your authorized credit budget. ElevenLabs accepts a voice ID in `--voice` and defaults to George when omitted. Use audio tags in the transcript; `--delivery` is only for Gemini and is rejected for ElevenLabs. SFX has a single model, `eleven_text_to_sound_v2`; `--seconds` accepts 0.5–30 (default 5), `--loop` requests a seamless loop, and `--prompt-influence` accepts 0–1 (default 0.3). Both return MP3. `catalog` includes `sfxModels` and `elevenLabsVoices`. The server needs `AI_PROVIDER_KEY_ELEVENLABS`; provider keys never belong in CLI credentials.
|
|
179
181
|
|
|
182
|
+
CLI 0.8.0 adds MiniMax H3 Max Turbo and the H3 workflows. MiniMax H3 Max and H3 Max Turbo on fal.ai (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also write videos from text and extend saved videos. Choose with `--workflow`:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# End on a pose, pin saved audio as the soundtrack, and keep a repeatable seed.
|
|
186
|
+
animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max-turbo \
|
|
187
|
+
--name "Hero lands" --prompt "The hero leaps and lands in a crouch" \
|
|
188
|
+
--reference START_IMAGE_SHA --end-reference END_IMAGE_SHA --soundtrack AUDIO_SHA \
|
|
189
|
+
--prompt-expansion balanced --seed 42 --resolution 1080p --dry-run
|
|
190
|
+
# Write a scene from text alone.
|
|
191
|
+
animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
|
|
192
|
+
--workflow text-to-video --name "Harbor" --prompt "A foggy harbor at dawn" \
|
|
193
|
+
--aspect-ratio 21:9 --seconds 8 --resolution 768p --dry-run
|
|
194
|
+
# Add six seconds to a saved video at 2K, guided by saved audio.
|
|
195
|
+
animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
|
|
196
|
+
--workflow extend-video --source VIDEO_SHA --name "Harbor, continued" \
|
|
197
|
+
--prompt "Audio 1 for the gulls. The fog lifts and a ship sails out" \
|
|
198
|
+
--audio-references AUDIO_SHA --seconds 6 --resolution 2k --dry-run
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`--end-reference` can be used without `--reference`; the video then ends on that image. `--soundtrack` takes a music, speech or SFX asset's `file` SHA and replaces the generated sound; the video must be at least 2 seconds. Text-to-video takes `--aspect-ratio` 21:9, 16:9 (default), 4:3, 1:1, 3:4 or 9:16. Extensions take a video asset's SHA as `--source` (1.6–60 seconds, up to 50 MB), bill `--seconds` of new footage, offer 2K, and keep the source's shape unless `--aspect-ratio` crops it. `--extend-output continuation` returns only a 1.625-second overlap and the new footage. Extensions take up to three comma-separated `--audio-references` (2–15 seconds each, 15 seconds in total); refer to them in the prompt as Audio 1, Audio 2 and so on. `--prompt-expansion` is `disabled` (default), `balanced` or `quality`; for extensions, any value other than `disabled` turns expansion on. Each model's `videoProfiles` entry in `catalog` lists its workflows, resolutions and options. The server needs `AI_PROVIDER_KEY_FAL`.
|
|
202
|
+
|
|
180
203
|
Images have a full background by default; `--transparent` requests a cutout. Gemini speech's `--prompt` is the verbatim transcript, and `--delivery` controls how it sounds without being spoken. Gemini speech supports both `gemini-3.8-flash-tts` and `gemini-3.8-flash-lite-tts`. Music uses Lyria. Google audio requires the server's `AI_PROVIDER_KEY_GOOGLE` secret; no provider keys are stored in the CLI.
|
|
181
204
|
|
|
182
205
|
Preview a validated request and its estimated credits offline with `--dry-run`, without signing in or spending credits:
|
package/dist/animaguru.js
CHANGED
|
@@ -542,7 +542,7 @@ var init_dist = __esm({
|
|
|
542
542
|
// cli/package.json
|
|
543
543
|
var package_default = {
|
|
544
544
|
name: "@docyrus/animaguru",
|
|
545
|
-
version: "0.
|
|
545
|
+
version: "0.8.1",
|
|
546
546
|
description: "Agent-friendly AnimaGuru CLI built with CrustJS",
|
|
547
547
|
type: "module",
|
|
548
548
|
bin: {
|
|
@@ -6111,11 +6111,20 @@ var SPRITE_IMAGE_MODEL_IDS = SPRITE_IMAGE_MODELS.map((m) => m.id);
|
|
|
6111
6111
|
function spriteImageModel(id2) {
|
|
6112
6112
|
return SPRITE_IMAGE_MODELS.find((m) => m.id === id2) ?? SPRITE_IMAGE_MODELS[0];
|
|
6113
6113
|
}
|
|
6114
|
+
var VIDEO_WORKFLOWS = ["image-to-video", "text-to-video", "extend-video"];
|
|
6115
|
+
var VIDEO_RESOLUTIONS = ["480p", "768p", "1080p", "2k"];
|
|
6116
|
+
var VIDEO_ASPECT_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
|
|
6117
|
+
var PROMPT_EXPANSION_MODES = ["disabled", "balanced", "quality"];
|
|
6118
|
+
var EXTEND_OUTPUTS = ["extended", "continuation"];
|
|
6119
|
+
var SOUNDTRACK_MAX_BYTES = 15 * 1024 * 1024;
|
|
6120
|
+
var EXTEND_SOURCE_MAX_BYTES = 50 * 1024 * 1024;
|
|
6121
|
+
var MAX_AUDIO_REFERENCES = 3;
|
|
6114
6122
|
var SPRITE_VIDEO_MODELS = [
|
|
6115
6123
|
{
|
|
6116
6124
|
id: "pruna/p-video-2-pro",
|
|
6117
6125
|
label: "Pruna P-Video 2 Pro",
|
|
6118
6126
|
provider: "pruna",
|
|
6127
|
+
workflows: ["image-to-video"],
|
|
6119
6128
|
resolutions: ["480p", "768p"],
|
|
6120
6129
|
modes: ["cost", "speed", "quality"],
|
|
6121
6130
|
lastFrame: true,
|
|
@@ -6126,6 +6135,7 @@ var SPRITE_VIDEO_MODELS = [
|
|
|
6126
6135
|
id: "heygen/heygen-video-1",
|
|
6127
6136
|
label: "HeyGen Video 1",
|
|
6128
6137
|
provider: "heygen",
|
|
6138
|
+
workflows: ["image-to-video"],
|
|
6129
6139
|
resolutions: ["480p", "768p"],
|
|
6130
6140
|
modes: ["cost"],
|
|
6131
6141
|
lastFrame: false,
|
|
@@ -6136,13 +6146,40 @@ var SPRITE_VIDEO_MODELS = [
|
|
|
6136
6146
|
id: "fal/minimax-h3-max",
|
|
6137
6147
|
label: "MiniMax H3 Max (fal.ai)",
|
|
6138
6148
|
provider: "fal",
|
|
6139
|
-
|
|
6149
|
+
workflows: VIDEO_WORKFLOWS,
|
|
6150
|
+
endpoints: { "image-to-video": "minimax/h3-max/image-to-video", "text-to-video": "minimax/h3-max/text-to-video", "extend-video": "minimax/h3-max/extend-video" },
|
|
6140
6151
|
resolutions: ["480p", "768p", "1080p"],
|
|
6152
|
+
extendResolutions: VIDEO_RESOLUTIONS,
|
|
6141
6153
|
modes: ["quality"],
|
|
6142
6154
|
lastFrame: true,
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6155
|
+
endFrameOnly: true,
|
|
6156
|
+
seed: true,
|
|
6157
|
+
promptExpansion: PROMPT_EXPANSION_MODES,
|
|
6158
|
+
audio: true,
|
|
6159
|
+
// Regular rates, not the temporary promotion ending 2026-10-15; the same for every workflow,
|
|
6160
|
+
// 2K extensions only. Extensions bill reference input beyond 4,096 tokens a request too
|
|
6161
|
+
// ($0.02 per 1,000), which fal doesn't report back, so it isn't charged.
|
|
6162
|
+
// https://fal.ai/models/minimax/h3-max/image-to-video, …/text-to-video, …/extend-video
|
|
6163
|
+
usdPerSecond: { "480p": { quality: 0.05 }, "768p": { quality: 0.08 }, "1080p": { quality: 0.16 }, "2k": { quality: 0.32 } },
|
|
6164
|
+
seconds: { min: 1, max: 15 }
|
|
6165
|
+
},
|
|
6166
|
+
{
|
|
6167
|
+
id: "fal/minimax-h3-max-turbo",
|
|
6168
|
+
label: "MiniMax H3 Max Turbo (fal.ai)",
|
|
6169
|
+
provider: "fal",
|
|
6170
|
+
workflows: VIDEO_WORKFLOWS,
|
|
6171
|
+
endpoints: { "image-to-video": "minimax/h3-max-turbo/image-to-video", "text-to-video": "minimax/h3-max-turbo/text-to-video", "extend-video": "minimax/h3-max-turbo/extend-video" },
|
|
6172
|
+
resolutions: ["480p", "768p", "1080p"],
|
|
6173
|
+
extendResolutions: VIDEO_RESOLUTIONS,
|
|
6174
|
+
modes: ["speed"],
|
|
6175
|
+
lastFrame: true,
|
|
6176
|
+
endFrameOnly: true,
|
|
6177
|
+
seed: true,
|
|
6178
|
+
promptExpansion: PROMPT_EXPANSION_MODES,
|
|
6179
|
+
audio: true,
|
|
6180
|
+
// Regular rates, not the temporary promotion ending 2026-10-15; extensions bill only the new footage.
|
|
6181
|
+
// https://fal.ai/models/minimax/h3-max-turbo/image-to-video, …/text-to-video, …/extend-video
|
|
6182
|
+
usdPerSecond: { "480p": { speed: 0.025 }, "768p": { speed: 0.04 }, "1080p": { speed: 0.08 }, "2k": { speed: 0.16 } },
|
|
6146
6183
|
seconds: { min: 1, max: 15 }
|
|
6147
6184
|
}
|
|
6148
6185
|
];
|
|
@@ -6151,6 +6188,9 @@ var DEFAULT_SPRITE_VIDEO_MODEL = "pruna/p-video-2-pro";
|
|
|
6151
6188
|
function spriteVideoModel(id2) {
|
|
6152
6189
|
return SPRITE_VIDEO_MODELS.find((m) => m.id === id2) ?? SPRITE_VIDEO_MODELS[0];
|
|
6153
6190
|
}
|
|
6191
|
+
function videoResolutions(model, workflow = "image-to-video") {
|
|
6192
|
+
return workflow === "extend-video" ? model.extendResolutions ?? model.resolutions : model.resolutions;
|
|
6193
|
+
}
|
|
6154
6194
|
function spriteVideoCostUsd(model, p) {
|
|
6155
6195
|
const byMode = model.usdPerSecond[p.resolution] ?? model.usdPerSecond[model.resolutions[0]];
|
|
6156
6196
|
const perSecond = byMode[p.mode] ?? byMode[model.modes[0]] ?? 0;
|
|
@@ -26811,7 +26851,7 @@ var VIDEO_WORKFLOW_IDS = [
|
|
|
26811
26851
|
"ambient-loop",
|
|
26812
26852
|
"visual-essay"
|
|
26813
26853
|
];
|
|
26814
|
-
var
|
|
26854
|
+
var VIDEO_WORKFLOWS2 = VIDEO_WORKFLOW_IDS.map((id2, index) => ({
|
|
26815
26855
|
id: id2,
|
|
26816
26856
|
category: index < 8 ? "advertisement" : "general-video",
|
|
26817
26857
|
name: animation_styles_default.workflows[id2].name,
|
|
@@ -26840,10 +26880,10 @@ function videoTemplateCatalog() {
|
|
|
26840
26880
|
name: animation_styles_default.categories[id2].name,
|
|
26841
26881
|
description: animation_styles_default.categories[id2].description,
|
|
26842
26882
|
templates: VIDEO_TEMPLATES.filter((template) => template.categories.some((category) => category === id2)),
|
|
26843
|
-
workflows:
|
|
26883
|
+
workflows: VIDEO_WORKFLOWS2.filter((workflow) => workflow.category === id2)
|
|
26844
26884
|
})),
|
|
26845
26885
|
templates: VIDEO_TEMPLATES,
|
|
26846
|
-
workflows:
|
|
26886
|
+
workflows: VIDEO_WORKFLOWS2
|
|
26847
26887
|
};
|
|
26848
26888
|
}
|
|
26849
26889
|
|
|
@@ -27076,14 +27116,59 @@ var isElevenLabsSpeech = (model) => ELEVENLABS_SPEECH_MODEL_IDS.includes(model);
|
|
|
27076
27116
|
var MUSIC_MODELS = [{ id: "lyria-3.5", label: "Lyria 3.5" }];
|
|
27077
27117
|
var SPEECH_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede", "Leda", "Orus", "Zephyr"];
|
|
27078
27118
|
var base = { name: external_exports.string().trim().min(1).max(80), prompt: external_exports.string().trim().min(3).max(4e3) };
|
|
27119
|
+
var blob = external_exports.string().regex(/^[a-f0-9]{64}$/);
|
|
27120
|
+
var videoAsset = external_exports.object({
|
|
27121
|
+
...base,
|
|
27122
|
+
type: external_exports.literal("video"),
|
|
27123
|
+
model: external_exports.enum(SPRITE_VIDEO_MODEL_IDS).default(DEFAULT_SPRITE_VIDEO_MODEL),
|
|
27124
|
+
workflow: external_exports.enum(VIDEO_WORKFLOWS).default("image-to-video"),
|
|
27125
|
+
/** First frame. */
|
|
27126
|
+
reference: blob.optional(),
|
|
27127
|
+
/** Last frame. */
|
|
27128
|
+
endReference: blob.optional(),
|
|
27129
|
+
/** The saved clip to extend. */
|
|
27130
|
+
source: blob.optional(),
|
|
27131
|
+
/** Saved audio pinned as the soundtrack (at least two seconds of video). */
|
|
27132
|
+
soundtrack: blob.optional(),
|
|
27133
|
+
/** Saved audio guiding an extension's new sound; name each clip's role in the prompt. */
|
|
27134
|
+
audioReferences: external_exports.array(blob).max(MAX_AUDIO_REFERENCES).default([]),
|
|
27135
|
+
seconds: external_exports.number().int().min(1).max(15).default(5),
|
|
27136
|
+
resolution: external_exports.enum(VIDEO_RESOLUTIONS).default("480p"),
|
|
27137
|
+
mode: external_exports.enum(["cost", "speed", "quality"]).default("cost"),
|
|
27138
|
+
/** A clip written from text (default 16:9), or an extension cropped to a new canvas ("auto" keeps the source's). */
|
|
27139
|
+
aspectRatio: external_exports.enum(["auto", ...VIDEO_ASPECT_RATIOS]).optional(),
|
|
27140
|
+
promptExpansion: external_exports.enum(PROMPT_EXPANSION_MODES).default("disabled"),
|
|
27141
|
+
seed: external_exports.number().int().min(0).max(2 ** 32 - 1).optional(),
|
|
27142
|
+
/** An extension returns the whole clip, or only the new footage after a short overlap. */
|
|
27143
|
+
extendOutput: external_exports.enum(EXTEND_OUTPUTS).default("extended")
|
|
27144
|
+
}).superRefine((p, ctx) => {
|
|
27145
|
+
const model = spriteVideoModel(p.model);
|
|
27146
|
+
const issue2 = (path, message) => ctx.addIssue({ code: "custom", message, path: [path] });
|
|
27147
|
+
if (!model.workflows.includes(p.workflow)) return issue2("workflow", "Choose a way of making video supported by the video model.");
|
|
27148
|
+
const extend2 = p.workflow === "extend-video";
|
|
27149
|
+
if (p.seconds < model.seconds.min || p.seconds > model.seconds.max) issue2("seconds", "Choose a duration supported by the video model.");
|
|
27150
|
+
if (!videoResolutions(model, p.workflow).includes(p.resolution)) issue2("resolution", "Choose a resolution supported by the video model.");
|
|
27151
|
+
if (!model.modes.includes(p.mode)) issue2("mode", "Choose a mode supported by the video model.");
|
|
27152
|
+
if (p.workflow === "image-to-video") {
|
|
27153
|
+
if (!p.reference && !(p.endReference && model.endFrameOnly)) issue2("reference", "Choose a starting image.");
|
|
27154
|
+
if (p.endReference && !model.lastFrame) issue2("endReference", "This video model can't end on a chosen image.");
|
|
27155
|
+
} else {
|
|
27156
|
+
if (p.reference) issue2("reference", "Only image-to-video starts from an image.");
|
|
27157
|
+
if (p.endReference) issue2("endReference", "Only image-to-video ends on an image.");
|
|
27158
|
+
}
|
|
27159
|
+
if (extend2 !== Boolean(p.source)) issue2("source", extend2 ? "Choose a saved video to extend." : "Only an extension starts from a saved video.");
|
|
27160
|
+
if (p.aspectRatio && p.workflow === "image-to-video") issue2("aspectRatio", "A video made from an image keeps the image's shape.");
|
|
27161
|
+
if (p.aspectRatio === "auto" && p.workflow === "text-to-video") issue2("aspectRatio", "Choose an aspect ratio for a video written from text.");
|
|
27162
|
+
if (p.soundtrack && (!model.audio || extend2)) issue2("soundtrack", extend2 ? "An extension keeps its source's soundtrack; add audio references instead." : "This video model can't use a soundtrack.");
|
|
27163
|
+
if (p.soundtrack && p.seconds < 2) issue2("seconds", "A video with a soundtrack must be at least 2 seconds long.");
|
|
27164
|
+
if (p.audioReferences.length && (!model.audio || !extend2)) issue2("audioReferences", "Only an extension takes audio references.");
|
|
27165
|
+
if (p.promptExpansion !== "disabled" && !model.promptExpansion?.includes(p.promptExpansion)) issue2("promptExpansion", "This video model doesn't rewrite prompts.");
|
|
27166
|
+
if (p.seed !== void 0 && !model.seed) issue2("seed", "This video model doesn't take a seed.");
|
|
27167
|
+
if (p.extendOutput !== "extended" && !extend2) issue2("extendOutput", "Only an extension can return the new footage alone.");
|
|
27168
|
+
});
|
|
27079
27169
|
var assetRequest = external_exports.discriminatedUnion("type", [
|
|
27080
27170
|
external_exports.object({ ...base, type: external_exports.literal("image"), model: external_exports.enum(SPRITE_IMAGE_MODEL_IDS).default(DEFAULT_SPRITE_IMAGE_MODEL), aspectRatio: external_exports.enum(["1:1", "3:2", "2:3"]).default("1:1"), transparent: external_exports.boolean().default(false) }),
|
|
27081
|
-
|
|
27082
|
-
const model = spriteVideoModel(p.model);
|
|
27083
|
-
if (p.seconds < model.seconds.min || p.seconds > model.seconds.max) ctx.addIssue({ code: "custom", message: "Choose a duration supported by the video model.", path: ["seconds"] });
|
|
27084
|
-
if (!model.resolutions.includes(p.resolution)) ctx.addIssue({ code: "custom", message: "Choose a resolution supported by the video model.", path: ["resolution"] });
|
|
27085
|
-
if (!model.modes.includes(p.mode)) ctx.addIssue({ code: "custom", message: "Choose a mode supported by the video model.", path: ["mode"] });
|
|
27086
|
-
}),
|
|
27171
|
+
videoAsset,
|
|
27087
27172
|
external_exports.object({ ...base, type: external_exports.literal("music"), model: external_exports.literal("lyria-3.5").default("lyria-3.5") }),
|
|
27088
27173
|
external_exports.object({ ...base, type: external_exports.literal("speech"), model: external_exports.enum(["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts", ...ELEVENLABS_SPEECH_MODEL_IDS]).default("gemini-3.8-flash-lite-tts"), voice: external_exports.string().trim().max(80).default(""), delivery: external_exports.string().trim().max(600).default("") }).superRefine((p, ctx) => {
|
|
27089
27174
|
if (p.voice && (isElevenLabsSpeech(p.model) ? !/^[a-zA-Z0-9]{20}$/.test(p.voice) : !SPEECH_VOICES.includes(p.voice))) ctx.addIssue({ code: "custom", message: "Choose a voice supported by the speech provider.", path: ["voice"] });
|
|
@@ -27310,25 +27395,46 @@ async function submitVideo(client, options) {
|
|
|
27310
27395
|
// cli/src/assets.ts
|
|
27311
27396
|
import { readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
|
|
27312
27397
|
import { resolve as resolve8 } from "node:path";
|
|
27313
|
-
var extras = { image: ["aspect-ratio", "transparent"], video: ["reference", "seconds", "resolution", "mode"], music: [], speech: ["voice", "delivery"], sfx: ["seconds", "loop", "prompt-influence"] };
|
|
27398
|
+
var extras = { image: ["aspect-ratio", "transparent"], video: ["workflow", "reference", "end-reference", "source", "soundtrack", "audio-references", "seconds", "resolution", "mode", "aspect-ratio", "prompt-expansion", "seed", "extend-output"], music: [], speech: ["voice", "delivery"], sfx: ["seconds", "loop", "prompt-influence"] };
|
|
27399
|
+
function parseAsset(input2) {
|
|
27400
|
+
const parsed = assetRequest.safeParse(input2);
|
|
27401
|
+
if (parsed.success) return parsed.data;
|
|
27402
|
+
throw new Error(parsed.error.issues.map((issue2) => `${issue2.path.length ? `${issue2.path.join(".")}: ` : ""}${issue2.message}`).join("\n"));
|
|
27403
|
+
}
|
|
27314
27404
|
function assetFromFlags(flags) {
|
|
27315
27405
|
for (const name of Object.values(extras).flat()) {
|
|
27316
27406
|
const value = flags[name];
|
|
27317
27407
|
if (value !== void 0 && value !== false && !extras[flags.type]?.includes(name)) throw new Error(`--${name} is not an option for ${flags.type} assets.`);
|
|
27318
27408
|
}
|
|
27319
27409
|
const base2 = { type: flags.type, name: flags.name, prompt: flags.prompt, model: flags.model };
|
|
27320
|
-
if (flags.type === "image") return
|
|
27410
|
+
if (flags.type === "image") return parseAsset({ ...base2, aspectRatio: flags["aspect-ratio"], transparent: flags.transparent });
|
|
27321
27411
|
if (flags.type === "video") {
|
|
27322
27412
|
const model = spriteVideoModel(flags.model);
|
|
27323
|
-
|
|
27413
|
+
const workflow = flags.workflow ?? "image-to-video";
|
|
27414
|
+
return parseAsset({
|
|
27415
|
+
...base2,
|
|
27416
|
+
workflow: flags.workflow,
|
|
27417
|
+
reference: flags.reference,
|
|
27418
|
+
endReference: flags["end-reference"],
|
|
27419
|
+
source: flags.source,
|
|
27420
|
+
soundtrack: flags.soundtrack,
|
|
27421
|
+
audioReferences: flags["audio-references"]?.split(",").map((sha) => sha.trim()).filter(Boolean),
|
|
27422
|
+
seconds: flags.seconds ?? Math.max(5, model.seconds.min),
|
|
27423
|
+
resolution: flags.resolution ?? videoResolutions(model, workflow)[0],
|
|
27424
|
+
mode: flags.mode ?? model.modes[0],
|
|
27425
|
+
aspectRatio: flags["aspect-ratio"],
|
|
27426
|
+
promptExpansion: flags["prompt-expansion"],
|
|
27427
|
+
seed: flags.seed,
|
|
27428
|
+
extendOutput: flags["extend-output"]
|
|
27429
|
+
});
|
|
27324
27430
|
}
|
|
27325
|
-
if (flags.type === "sfx") return
|
|
27326
|
-
return
|
|
27431
|
+
if (flags.type === "sfx") return parseAsset({ ...base2, seconds: flags.seconds, loop: flags.loop, promptInfluence: flags["prompt-influence"] });
|
|
27432
|
+
return parseAsset({ ...base2, ...flags.type === "speech" ? { voice: flags.voice, delivery: flags.delivery } : {} });
|
|
27327
27433
|
}
|
|
27328
27434
|
async function assetFromFile(file2, model) {
|
|
27329
27435
|
const input2 = JSON.parse(await readFile7(file2, "utf8"));
|
|
27330
27436
|
if (!input2 || typeof input2 !== "object" || Array.isArray(input2)) throw new Error("The asset file must contain a JSON object.");
|
|
27331
|
-
return
|
|
27437
|
+
return parseAsset({ ...input2, ...model ? { model } : {} });
|
|
27332
27438
|
}
|
|
27333
27439
|
async function submitAsset(client, project, body, options) {
|
|
27334
27440
|
if (options["dry-run"]) return { dryRun: true, request: body, estimatedCredits: assetEstimate(body) };
|
|
@@ -27457,10 +27563,10 @@ Confirm code: ${device.user_code}`);
|
|
|
27457
27563
|
}))).add(defineCommand("catalog", { description: "List styles, kinds, actions, asset types, models, video settings and speech voices" }, (command) => command.action(({ stdout }) => out(stdout, { styles: SPRITE_STYLES, kinds: SPRITE_KINDS, actions: ALL_SPRITE_ACTIONS, imageModels: SPRITE_IMAGE_MODEL_IDS, videoModels: SPRITE_VIDEO_MODEL_IDS, assetTypes: ASSET_TYPES, musicModels: MUSIC_MODELS.map((m) => m.id), speechModels: SPEECH_MODELS.map((m) => m.id), sfxModels: SFX_MODELS.map((m) => m.id), elevenLabsVoices: ELEVENLABS_VOICES, voices: SPEECH_VOICES, videoProfiles: SPRITE_VIDEO_MODELS })))).command("projects", (projects) => projects.add(defineCommand("list", { description: "List projects in the selected workspace" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).action(async ({ flags, stdout, signal }) => {
|
|
27458
27564
|
const client = new Client(flags.url, signal);
|
|
27459
27565
|
out(stdout, await client.request(`/api/v1/projects${await workspaceQuery(client)}`));
|
|
27460
|
-
}))).add(defineCommand("create", { description: "Create an empty sprite project; generate artwork with jobs submit" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).flags(...modelFlags).flags({ name: "name", type: "string", required: true }
|
|
27566
|
+
}))).add(defineCommand("create", { description: "Create an empty sprite project; generate artwork with jobs submit" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).flags(...modelFlags).flags({ name: "name", type: "string", required: true }).action(async ({ flags, stdout, signal }) => {
|
|
27461
27567
|
const client = new Client(flags.url, signal);
|
|
27462
27568
|
const selected = await selectedWorkspace(client.origin);
|
|
27463
|
-
out(stdout, await client.request("/api/v1/sprites", { method: "POST", body: { owner: selected, name: flags.name,
|
|
27569
|
+
out(stdout, await client.request("/api/v1/sprites", { method: "POST", body: { owner: selected, name: flags.name, ...selectedModels(flags) } }));
|
|
27464
27570
|
}))).add(defineCommand("get", { description: "Read project, subjects, animations, estimates and job state" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(projectPath(args.id)))))).add(defineCommand("rename", { description: "Rename a project" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags({ name: "name", type: "string", required: true }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/projects/${encodeURIComponent(args.id)}`, { method: "PATCH", body: { name: flags.name } }))))).add(defineCommand("delete", { description: "Delete a project; requires --yes" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags({ name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
|
|
27465
27571
|
if (!flags.yes) throw new Error("Pass --yes to delete this project.");
|
|
27466
27572
|
out(stdout, await new Client(flags.url, signal).request(`/api/v1/projects/${encodeURIComponent(args.id)}`, { method: "DELETE" }));
|
|
@@ -27469,12 +27575,20 @@ Confirm code: ${device.user_code}`);
|
|
|
27469
27575
|
{ name: "name", type: "string", required: true },
|
|
27470
27576
|
{ name: "prompt", type: "string", required: true, description: "Description to generate, or verbatim transcript for speech" },
|
|
27471
27577
|
{ name: "model", type: "string", choices: assetModelIds, description: "Model for the chosen asset type" },
|
|
27472
|
-
{ name: "aspect-ratio", type: "string", choices: ["1:1", "3:2", "2:3"], description: "Image
|
|
27578
|
+
{ name: "aspect-ratio", type: "string", choices: [.../* @__PURE__ */ new Set(["1:1", "3:2", "2:3", "auto", ...VIDEO_ASPECT_RATIOS])], description: "Image: 1:1 (default), 3:2 or 2:3. Video from text: 21:9\u20139:16 (default 16:9). Extension: auto (default, the source's) or a ratio to crop to" },
|
|
27473
27579
|
{ name: "transparent", type: "boolean", description: "Image: transparent background" },
|
|
27580
|
+
{ name: "workflow", type: "string", choices: VIDEO_WORKFLOWS, description: "Video: image-to-video (default), text-to-video or extend-video, as the model's videoProfiles allow" },
|
|
27474
27581
|
{ name: "reference", type: "string", description: "Video: starting image blob SHA from this project's assets or sprites" },
|
|
27475
|
-
{ name: "
|
|
27476
|
-
{ name: "
|
|
27582
|
+
{ name: "end-reference", type: "string", description: "Video: ending image blob SHA; models with endFrameOnly accept it without --reference" },
|
|
27583
|
+
{ name: "source", type: "string", description: "Extension: blob SHA of a video asset in this project to extend" },
|
|
27584
|
+
{ name: "soundtrack", type: "string", description: "Video from an image or text: blob SHA of a music, speech or SFX asset pinned as the soundtrack (2 s or longer)" },
|
|
27585
|
+
{ name: "audio-references", type: "string", description: "Extension: up to 3 comma-separated audio asset blob SHAs (2\u201315 s each, 15 s total) guiding the new sound; refer to them as Audio 1, Audio 2\u2026 in the prompt" },
|
|
27586
|
+
{ name: "seconds", type: "number", description: "Video or SFX duration in seconds (default 5); extensions add this many seconds; SFX supports 0.5\u201330" },
|
|
27587
|
+
{ name: "resolution", type: "string", choices: VIDEO_RESOLUTIONS, description: "Video resolution supported by the selected model and workflow (2k for extensions)" },
|
|
27477
27588
|
{ name: "mode", type: "string", choices: ["cost", "speed", "quality"], description: "Video generation mode supported by the selected model" },
|
|
27589
|
+
{ name: "prompt-expansion", type: "string", choices: PROMPT_EXPANSION_MODES, description: "Video: how much the model may rewrite the prompt (default disabled); extensions treat any other value as on" },
|
|
27590
|
+
{ name: "seed", type: "number", description: "Video: seed for repeatable results, where the model takes one" },
|
|
27591
|
+
{ name: "extend-output", type: "string", choices: EXTEND_OUTPUTS, description: "Extension: extended (default, source plus new footage) or continuation (a 1.625 s overlap, then the new footage)" },
|
|
27478
27592
|
{ name: "voice", type: "string", description: "Gemini voice name or ElevenLabs voice ID (defaults: Kore / George)" },
|
|
27479
27593
|
{ name: "delivery", type: "string", description: "Gemini speech delivery instructions; ElevenLabs uses audio tags in the transcript" },
|
|
27480
27594
|
{ name: "loop", type: "boolean", description: "SFX: create a seamless loop" },
|
|
@@ -27502,9 +27616,9 @@ Confirm code: ${device.user_code}`);
|
|
|
27502
27616
|
const body = selectedModels(flags);
|
|
27503
27617
|
if (!Object.keys(body).length) throw new Error("Pass --image-model or --video-model.");
|
|
27504
27618
|
out(stdout, await new Client(flags.url, signal).request(`${projectPath(args.id)}/models`, { method: "PUT", body }));
|
|
27505
|
-
}))).add(defineCommand("generate", { description: "Draw a subject; --yes authorizes generation costs" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags(...modelFlags).flags({ name: "name", type: "string", required: true }, { name: "description", type: "string", required: true }, { name: "kind", type: "string", choices: SPRITE_KINDS, default: "character" }, { name: "actions", type: "string", description: "Comma-separated built-in actions to animate after drawing" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
|
|
27619
|
+
}))).add(defineCommand("generate", { description: "Draw a subject; --yes authorizes generation costs" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags(...modelFlags).flags({ name: "name", type: "string", required: true }, { name: "description", type: "string", required: true }, { name: "style", type: "string", choices: SPRITE_STYLES, description: "Artwork style for this sprite" }, { name: "kind", type: "string", choices: SPRITE_KINDS, default: "character" }, { name: "actions", type: "string", description: "Comma-separated built-in actions to animate after drawing" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
|
|
27506
27620
|
if (!flags.yes) throw new Error("Pass --yes to authorize spending credits.");
|
|
27507
|
-
const body = spriteJobBody.parse({ kind: "character", name: flags.name, description: flags.description, subjectKind: flags.kind, actions: actions(flags.actions), ...selectedModels(flags) });
|
|
27621
|
+
const body = spriteJobBody.parse({ kind: "character", name: flags.name, description: flags.description, ...flags.style ? { style: flags.style } : {}, subjectKind: flags.kind, actions: actions(flags.actions), ...selectedModels(flags) });
|
|
27508
27622
|
out(stdout, await new Client(flags.url, signal).request(`${projectPath(args.id)}/jobs`, { method: "POST", body }));
|
|
27509
27623
|
}))).add(defineCommand("schema", { description: "Print the Crust command schema for agent discovery" }, (command) => command.action(async ({ stdout }) => out(stdout, await createCli().snapshot())))).command("files", (files) => files.add(defineCommand("import", { description: "Import a PNG, JPEG or WebP reference; --yes authorizes redraw costs when configured" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).args(id).flags(...modelFlags).flags({ name: "file", type: "string", required: true }, { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
|
|
27510
27624
|
if (!flags.yes) throw new Error("Pass --yes to authorize reference redraw costs.");
|
package/package.json
CHANGED
|
@@ -24,9 +24,9 @@ Share the [animation-style gallery](https://animaguru.com/animation-styles), or
|
|
|
24
24
|
Reuse the user's chosen project. Otherwise inspect `animaguru projects list` before creating one. Read `animaguru projects get PROJECT_ID` for subjects, character identifiers, generation estimates, and current job state. Use identifiers returned by the service rather than guessing slugs.
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
animaguru projects create --name "Forest cast"
|
|
27
|
+
animaguru projects create --name "Forest cast"
|
|
28
28
|
animaguru generate PROJECT_ID --name "Knight" \
|
|
29
|
-
--description "A blue knight with a red scarf" --actions idle,walk --yes
|
|
29
|
+
--description "A blue knight with a red scarf" --style pixel --actions idle,walk --yes
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Read the project ID from `projects create` and job IDs from submission responses. Supported actions depend on the subject kind. Use `--kind` for a different subject kind, based on the catalog.
|
|
@@ -63,7 +63,7 @@ Import reference artwork with `animaguru files import PROJECT_ID --file knight.p
|
|
|
63
63
|
|
|
64
64
|
## Standalone images, video, music and speech
|
|
65
65
|
|
|
66
|
-
CLI 0.4.0 adds `assets generate`, `assets submit`, `assets list`, and `assets download`. Use `assets list PROJECT_ID` to discover saved assets and configured provider models.
|
|
66
|
+
CLI 0.4.0 adds `assets generate`, `assets submit`, `assets list`, and `assets download`. Use `assets list PROJECT_ID` to discover saved assets and configured provider models. Create neutral asset projects with `projects create --name NAME`. Projects have no art style; choose style when generating an individual sprite.
|
|
67
67
|
|
|
68
68
|
```sh
|
|
69
69
|
animaguru assets generate PROJECT_ID --type image --name "Forest" \
|
|
@@ -77,7 +77,7 @@ animaguru assets generate PROJECT_ID --type speech --name "Welcome" \
|
|
|
77
77
|
--prompt "Welcome, traveler." --voice Kore --delivery "Warm and welcoming" --yes
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Use `--model` to select a model for any asset type; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
|
|
80
|
+
Use `--model` to select a model for any asset type; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. MiniMax H3 Max and H3 Max Turbo (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also take `--workflow text-to-video` (with `--aspect-ratio`) and `--workflow extend-video` (with `--source VIDEO_SHA`, 2K resolution, `--extend-output`, and up to three comma-separated `--audio-references`). From images, they accept `--end-reference` with or without `--reference` and a `--soundtrack AUDIO_SHA`. All workflows take `--prompt-expansion` and `--seed`. These need CLI 0.8.0 or newer. For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
|
|
81
81
|
|
|
82
82
|
For ElevenLabs speech, choose `eleven_v4` or `eleven_v4_turbo`, pass an ElevenLabs voice ID with `--voice` (George is the default), and put audio tags in `--prompt` rather than using `--delivery`. For sound effects, use `--type sfx`; the only model is `eleven_text_to_sound_v2`. SFX supports `--seconds` from 0.5 to 30 (default 5), `--loop`, and `--prompt-influence` from 0 to 1 (default 0.3). These additions require CLI 0.5.0 or newer; check `catalog` before using them with an installed CLI.
|
|
83
83
|
|
|
@@ -24,7 +24,7 @@ Read [visual and motion guidance](references/visual-style.md) when designing sce
|
|
|
24
24
|
|
|
25
25
|
Use image tools for bitmap artwork, cutouts and textures, and PixiJS for composition, typography, masks and motion. Treat people, products, objects, landscapes and abstract forms as equally valid cutout subjects. Choose coherent edge treatment, material, lighting and movement so the film feels intentionally assembled.
|
|
26
26
|
|
|
27
|
-
Use the user's selected generation models and budget. Otherwise inspect the current AnimaGuru catalog and configured providers to select models suited to the assets and motion. MiniMax H3 Max (`fal/minimax-h3-max`) is one option for animated photographic assets when available; cutout motion can also be built directly in PixiJS from still layers. Validate supported settings before submission.
|
|
27
|
+
Use the user's selected generation models and budget. Otherwise inspect the current AnimaGuru catalog and configured providers to select models suited to the assets and motion. MiniMax H3 Max (`fal/minimax-h3-max`) or its faster, cheaper Turbo (`fal/minimax-h3-max-turbo`) is one option for animated photographic assets when available; cutout motion can also be built directly in PixiJS from still layers. Validate supported settings before submission.
|
|
28
28
|
|
|
29
29
|
Deliver the film described in the brief with consistent cutout visuals and intentional animation. Keep story development responsive to the project's purpose rather than borrowing the narrative of an example film.
|
|
30
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, cp, mkdir } from "node:fs/promises";
|
|
1
|
+
import { readFile, cp, mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { listAnimations, projectContext, publicFile, readBranding } from "./project-context.mjs";
|
|
@@ -50,9 +50,19 @@ void mountPlayer({config,Film,chapters,branding:${JSON.stringify(context.brandin
|
|
|
50
50
|
}
|
|
51
51
|
const file = await publicFile(context, match[2]);
|
|
52
52
|
const type = ({svg:"image/svg+xml",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",mp3:"audio/mpeg",wav:"audio/wav",m4a:"audio/mp4",woff2:"font/woff2",woff:"font/woff",ttf:"font/ttf",mp4:"video/mp4",webm:"video/webm"})[file.split(".").at(-1).toLowerCase()] ?? "application/octet-stream";
|
|
53
|
-
|
|
53
|
+
// Media elements seek with byte ranges; without them Chrome cannot load or seek video.
|
|
54
|
+
const { size } = await stat(file);
|
|
55
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.range ?? "");
|
|
56
|
+
let start = 0, end = size - 1;
|
|
57
|
+
if (range && (range[1] || range[2])) {
|
|
58
|
+
start = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
|
|
59
|
+
if (range[1] && range[2]) end = Math.min(Number(range[2]), size - 1);
|
|
60
|
+
if (start > end) { response.writeHead(416, { "Content-Range": `bytes */${size}` }).end(); return; }
|
|
61
|
+
}
|
|
62
|
+
const partial = Boolean(range && (range[1] || range[2]));
|
|
63
|
+
response.writeHead(partial ? 206 : 200, { "Content-Type": type, "Cache-Control": "no-store", "Accept-Ranges": "bytes", "Content-Length": String(end - start + 1), ...(partial ? { "Content-Range": `bytes ${start}-${end}/${size}` } : {}) });
|
|
54
64
|
if (request.method === "HEAD") { response.end(); return; }
|
|
55
|
-
const stream = createReadStream(file); response.once("close", () => stream.destroy()); stream.pipe(response);
|
|
65
|
+
const stream = createReadStream(file, { start, end }); response.once("close", () => stream.destroy()); stream.pipe(response);
|
|
56
66
|
})().catch((error) => { if (!response.headersSent) response.writeHead(404).end(error.message); });
|
|
57
67
|
});
|
|
58
68
|
},
|
|
@@ -110,10 +110,11 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
110
110
|
if (request !== playbackRequest) { audio?.pause(); return; }
|
|
111
111
|
last = performance.now(); playing = true; update();
|
|
112
112
|
}
|
|
113
|
-
function
|
|
113
|
+
function setMuted(value: boolean) {
|
|
114
114
|
if (!audio) return;
|
|
115
|
-
muted =
|
|
115
|
+
muted = value; audio.muted = muted; update();
|
|
116
116
|
}
|
|
117
|
+
function toggleSound() { setMuted(!muted); }
|
|
117
118
|
function setCaptions(value: boolean) { film.captions = value; void render(time); update(); }
|
|
118
119
|
async function fullscreen() {
|
|
119
120
|
try { if (document.fullscreenElement) await document.exitFullscreen(); else await theater.requestFullscreen(); }
|
|
@@ -206,12 +207,12 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
206
207
|
}
|
|
207
208
|
await render(time); update();
|
|
208
209
|
const api = {
|
|
209
|
-
ready: true, width: config.width, height: config.height, duration: config.duration, fps: config.fps, language: config.language, canvas: app.canvas,
|
|
210
|
+
ready: true, width: config.width, height: config.height, duration: config.duration, fps: config.fps, language: config.language, languages: [{ code: config.language, name: config.language }], canvas: app.canvas,
|
|
210
211
|
get state() { return { time, playing, muted, captions: film.captions, audio: { configured: Boolean(audio), readyState: audio?.readyState ?? 0, error: audio?.error?.code ?? null } }; },
|
|
211
212
|
resources: film.resources,
|
|
212
213
|
get texts() { return film.texts; },
|
|
213
214
|
get cues() { return film.cues; },
|
|
214
|
-
seek, play, pause, setCaptions, saveFrame,
|
|
215
|
+
seek, play, pause, setMuted, setCaptions, saveFrame,
|
|
215
216
|
async renderFrame(frame: number, fps = config.fps) { await seek(frame / fps); },
|
|
216
217
|
};
|
|
217
218
|
Object.assign(window, { animaguru: api });
|