@hraness/slopcamera 3.3.2 → 3.3.4
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 +49 -25
- package/apps/desktop/application/directing-contract.ts +15 -3
- package/apps/desktop/application/directing-plan.ts +2 -1
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +1 -1
- package/apps/desktop/cli/cinema-renderer.ts +9 -6
- package/apps/desktop/cli/commands.ts +8 -1
- package/apps/desktop/cli/directing-blob.ts +17 -12
- package/apps/desktop/cli/directing-media.ts +14 -4
- package/apps/desktop/cli/directing-quote.ts +29 -11
- package/apps/desktop/cli/directing-service.ts +6 -2
- package/apps/desktop/cli/gateway-media-catalog.ts +57 -0
- package/apps/desktop/cli/gateway-media-service.ts +12 -2
- package/apps/desktop/cli/help.ts +4 -4
- package/apps/desktop/cli/main.ts +1 -1
- package/apps/desktop/cli/project-renderer.ts +23 -9
- package/apps/desktop/cli/spatial-scene-service.ts +1 -1
- package/apps/desktop/dist/cli/main.js +254 -248
- package/apps/desktop/html-overlay/spatial-splat-runtime.ts +6 -0
- package/dist/cli.js +1 -1
- package/dist/code/index.js +4 -4
- package/dist/{index-gs48afpt.js → index-c8jpst4g.js} +2 -2
- package/dist/index-z1kd1x8j.js +3 -0
- package/dist/index.js +1 -1
- package/docs/README.md +6 -3
- package/docs/studio.md +63 -3
- package/package.json +13 -7
- package/skills/slopcamera/references/directing-video.md +3 -3
- package/skills/slopcamera/references/educational-video.md +1 -1
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/parametric-design.md +5 -1
- package/skills/slopcamera/references/scene-building.md +2 -0
- package/skills/slopcamera/references/workflows-sdk.md +1 -1
- package/src/spatial-scene/geometry.ts +88 -35
- package/src/spatial-scene/parametric.ts +42 -15
- package/src/version.ts +1 -1
- package/dist/index-zhg36tf9.js +0 -3
|
@@ -920,3 +920,60 @@ export function createFileGatewayMediaCatalogSnapshotStore(
|
|
|
920
920
|
},
|
|
921
921
|
};
|
|
922
922
|
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Alibaba Wan video models advertise normalized catalog resolutions such as
|
|
926
|
+
* "720p", but the provider API accepts only explicit pixel sizes bound to
|
|
927
|
+
* the requested aspect ratio ("1280x720"). This adapter translates the
|
|
928
|
+
* catalog value through Wan's documented size table so local qualification
|
|
929
|
+
* and duration pricing stay anchored to the catalog row while paid dispatch
|
|
930
|
+
* sends the provider representation. The `1280x720` (16:9) cell and the
|
|
931
|
+
* `480x832` enumeration hint are verified against the live provider; the
|
|
932
|
+
* remaining cells mirror Wan's published size enum.
|
|
933
|
+
*/
|
|
934
|
+
const WAN_RESOLUTION_PIXEL_SIZES: Readonly<
|
|
935
|
+
Record<string, Readonly<Record<string, string>>>
|
|
936
|
+
> = {
|
|
937
|
+
"1080p": {
|
|
938
|
+
"16:9": "1920x1080",
|
|
939
|
+
"9:16": "1080x1920",
|
|
940
|
+
"1:1": "1440x1440",
|
|
941
|
+
"3:4": "1248x1632",
|
|
942
|
+
"4:3": "1632x1248",
|
|
943
|
+
},
|
|
944
|
+
"480p": {
|
|
945
|
+
"16:9": "832x480",
|
|
946
|
+
"9:16": "480x832",
|
|
947
|
+
"1:1": "624x624",
|
|
948
|
+
},
|
|
949
|
+
"720p": {
|
|
950
|
+
"16:9": "1280x720",
|
|
951
|
+
"9:16": "720x1280",
|
|
952
|
+
"1:1": "960x960",
|
|
953
|
+
"3:4": "832x1088",
|
|
954
|
+
"4:3": "1088x832",
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Resolve the resolution value a provider expects on the wire.
|
|
960
|
+
* Returns `undefined` when the request carries no resolution, `null` when a
|
|
961
|
+
* Wan catalog resolution cannot be mapped for the requested aspect ratio,
|
|
962
|
+
* and the wire-ready value otherwise.
|
|
963
|
+
*/
|
|
964
|
+
export function providerVideoResolution(
|
|
965
|
+
modelId: string,
|
|
966
|
+
resolution: string | undefined,
|
|
967
|
+
aspectRatio: string | undefined,
|
|
968
|
+
): string | null | undefined {
|
|
969
|
+
if (
|
|
970
|
+
resolution === undefined
|
|
971
|
+
|| !modelId.startsWith("alibaba/wan-")
|
|
972
|
+
|| !/^[1-9]\d{2,4}p$/iu.test(resolution)
|
|
973
|
+
) {
|
|
974
|
+
return resolution;
|
|
975
|
+
}
|
|
976
|
+
// Wan produces 16:9 landscape output when a request omits the aspect ratio.
|
|
977
|
+
return WAN_RESOLUTION_PIXEL_SIZES[resolution.toLocaleLowerCase("en-US")]
|
|
978
|
+
?.[aspectRatio ?? "16:9"] ?? null;
|
|
979
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./gateway-media-artifacts";
|
|
17
17
|
import {
|
|
18
18
|
GATEWAY_MEDIA_CATALOG_URL,
|
|
19
|
+
providerVideoResolution,
|
|
19
20
|
type GatewayJsonValue,
|
|
20
21
|
type GatewayMediaCatalogView,
|
|
21
22
|
type GatewayMediaKind,
|
|
@@ -2930,6 +2931,12 @@ export function createGatewayMediaService(options: Readonly<{
|
|
|
2930
2931
|
execution.signal,
|
|
2931
2932
|
);
|
|
2932
2933
|
assertVideoCapabilities(catalog.model, request);
|
|
2934
|
+
const wireResolution = providerVideoResolution(
|
|
2935
|
+
catalog.model.id,
|
|
2936
|
+
request.resolution,
|
|
2937
|
+
request.aspectRatio,
|
|
2938
|
+
);
|
|
2939
|
+
if (wireResolution === null) unsupportedModelSetting();
|
|
2933
2940
|
const dispatchAt = now();
|
|
2934
2941
|
const credential = await options.loadCredential(dispatchAt);
|
|
2935
2942
|
const timeout = combineAbortSignals(
|
|
@@ -2989,9 +2996,9 @@ export function createGatewayMediaService(options: Readonly<{
|
|
|
2989
2996
|
...(request.providerOptions === undefined
|
|
2990
2997
|
? {}
|
|
2991
2998
|
: { providerOptions: request.providerOptions }),
|
|
2992
|
-
...(
|
|
2999
|
+
...(wireResolution === undefined
|
|
2993
3000
|
? {}
|
|
2994
|
-
: { resolution:
|
|
3001
|
+
: { resolution: wireResolution }),
|
|
2995
3002
|
...(request.seed === undefined ? {} : { seed: request.seed }),
|
|
2996
3003
|
});
|
|
2997
3004
|
return parseGeneratedFiles(sdkResult, "videos", ["video/"]);
|
|
@@ -3025,6 +3032,9 @@ export function createGatewayMediaService(options: Readonly<{
|
|
|
3025
3032
|
generateAudio: optionalSummaryValue(request.generateAudio),
|
|
3026
3033
|
maxVideosPerCall: optionalSummaryValue(request.maxVideosPerCall),
|
|
3027
3034
|
n: optionalSummaryValue(request.n),
|
|
3035
|
+
providerResolution: optionalSummaryValue(
|
|
3036
|
+
wireResolution === request.resolution ? undefined : wireResolution,
|
|
3037
|
+
),
|
|
3028
3038
|
resolution: optionalSummaryValue(request.resolution),
|
|
3029
3039
|
seed: optionalSummaryValue(request.seed),
|
|
3030
3040
|
}), request.prompt, request.providerOptions),
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -91,7 +91,7 @@ For local SDK workflows, code run, workflows run and runs resume accept
|
|
|
91
91
|
The fixed local operation is slopcamera.studio.run; its SDK surface is studio.run().`,
|
|
92
92
|
direct: `Usage:
|
|
93
93
|
slopcamera direct init <recipe.json> [--json]
|
|
94
|
-
slopcamera direct anchor --input <
|
|
94
|
+
slopcamera direct anchor --input <media> [--json]
|
|
95
95
|
slopcamera direct plan <recipe.json> [--json]
|
|
96
96
|
slopcamera direct start <recipe.json> --budget-usd <USD> [--json]
|
|
97
97
|
slopcamera direct inspect <direct-id> [--json]
|
|
@@ -105,13 +105,13 @@ The fixed local operation is slopcamera.studio.run; its SDK surface is studio.ru
|
|
|
105
105
|
slopcamera direct assemble <direct-id> [--json]
|
|
106
106
|
|
|
107
107
|
Init writes a new recipe without overwriting. Edit its ID, shots, prompts and settings.
|
|
108
|
-
Anchor imports one explicit local image and returns a hash-bound source reference.
|
|
108
|
+
Anchor imports one explicit local image or video and returns a hash-bound source reference.
|
|
109
109
|
Plan checks live Gateway video capabilities and duration pricing without a paid call.
|
|
110
110
|
Start retains one budget for the direct ID; starting again cannot reset it.
|
|
111
|
-
Generate dispatches at most one new take and requires a unique take_<id>. Local
|
|
111
|
+
Generate dispatches at most one new take and requires a unique take_<id>. Local media,
|
|
112
112
|
including a predecessor's endpoint, require --allow-cloud-upload on that invocation.
|
|
113
113
|
URL-only models require --allow-reference-hosting and a private Vercel Blob store.
|
|
114
|
-
Only exact
|
|
114
|
+
Only exact shot references receive short-lived signed read URLs. Signed URLs and
|
|
115
115
|
credentials are not saved. Cleanup deletes those exact objects; inspect cleanup
|
|
116
116
|
receipts and use direct cleanup after interruptions. Blob charges are separate
|
|
117
117
|
from the model catalog estimate and require room in your overall spending limit.
|
package/apps/desktop/cli/main.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function main(
|
|
|
55
55
|
}
|
|
56
56
|
const paths = await resolveRepositoryPaths(processIo.cwd(), processIo.env);
|
|
57
57
|
if (earlyCommand.kind === "html-render" || earlyCommand.kind === "spatial-world" || earlyCommand.kind === "spatial-asset" || earlyCommand.kind === "spatial-design" || earlyCommand.kind === "directing" || earlyCommand.kind === "studio"
|
|
58
|
-
|| earlyCommand.kind === "spatial-scene" && (earlyCommand.action === "camera-track" || earlyCommand.action === "render-audit" || earlyCommand.action === "review")) {
|
|
58
|
+
|| earlyCommand.kind === "spatial-scene" && (earlyCommand.action === "render" || earlyCommand.action === "camera-track" || earlyCommand.action === "render-audit" || earlyCommand.action === "review")) {
|
|
59
59
|
const controller = new AbortController();
|
|
60
60
|
const cancel = () => controller.abort(new CliError("cancelled", "Command interrupted."));
|
|
61
61
|
process.on("SIGINT", cancel);
|
|
@@ -959,6 +959,7 @@ function overlayVideoChain(
|
|
|
959
959
|
} else {
|
|
960
960
|
filters.push(
|
|
961
961
|
`trim=duration=${seconds(sliceDurationUs)}`,
|
|
962
|
+
"settb=AVTB",
|
|
962
963
|
`setpts=PTS-STARTPTS+${seconds(resolved.outputRange.startUs)}/TB`,
|
|
963
964
|
);
|
|
964
965
|
}
|
|
@@ -1246,7 +1247,7 @@ export async function buildProjectFfmpegInvocation(
|
|
|
1246
1247
|
if (overlayInputIndex.has(overlay.overlayId)) continue;
|
|
1247
1248
|
overlayInputIndex.set(overlay.overlayId, mediaInputIndex.size + overlayInputIndex.size);
|
|
1248
1249
|
if (overlay.source.kind === "image" || overlay.source.kind === "svg" || overlay.source.kind === "emoji") {
|
|
1249
|
-
inputArguments.push("-loop", "1");
|
|
1250
|
+
inputArguments.push("-loop", "1", "-framerate", emittedRate);
|
|
1250
1251
|
}
|
|
1251
1252
|
const prepared = await prepareProjectOverlayMedia(overlay, options);
|
|
1252
1253
|
preparedOverlayMedia.set(overlay.overlayId, prepared);
|
|
@@ -1309,6 +1310,7 @@ export async function buildProjectFfmpegInvocation(
|
|
|
1309
1310
|
} else {
|
|
1310
1311
|
const transparent = `video_blend_canvas_${serial++}`;
|
|
1311
1312
|
const positioned = `video_blend_positioned_${serial++}`;
|
|
1313
|
+
const layerColorSource = `video_blend_color_source_${serial++}`;
|
|
1312
1314
|
const layerColor = `video_blend_color_${serial++}`;
|
|
1313
1315
|
const layerAlphaSource = `video_blend_alpha_source_${serial++}`;
|
|
1314
1316
|
const layerMask = `video_blend_mask_${serial++}`;
|
|
@@ -1317,10 +1319,13 @@ export async function buildProjectFfmpegInvocation(
|
|
|
1317
1319
|
const blended = `video_blend_result_${serial++}`;
|
|
1318
1320
|
filters.push(
|
|
1319
1321
|
`color=c=black@0:s=${plan.output.pixelWidth}x${plan.output.pixelHeight}:r=${emittedRate}:d=${seconds(plan.output.durationUs)},format=rgba[${transparent}]`,
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
+
// Preserve the positioned layer's alpha and blend color channels in RGB.
|
|
1323
|
+
// YUV blend arithmetic changes even uncovered pixels (neutral chroma is 128).
|
|
1324
|
+
`[${transparent}][${label}]overlay=x=${transform.x}:y=${transform.y}:eof_action=repeat:repeatlast=1:enable='${enable}':format=auto[${positioned}]`,
|
|
1325
|
+
`[${positioned}]format=rgba,split=2[${layerColorSource}][${layerAlphaSource}]`,
|
|
1326
|
+
`[${layerColorSource}]format=gbrp[${layerColor}]`,
|
|
1322
1327
|
`[${layerAlphaSource}]alphaextract[${layerMask}]`,
|
|
1323
|
-
`[${currentVideo}]split=2[${baseBlend}][${baseMerge}]`,
|
|
1328
|
+
`[${currentVideo}]format=gbrp,split=2[${baseBlend}][${baseMerge}]`,
|
|
1324
1329
|
`[${baseBlend}][${layerColor}]blend=all_mode=${slice.presentation.blendMode}[${blended}]`,
|
|
1325
1330
|
`[${baseMerge}][${blended}][${layerMask}]maskedmerge[${next}]`,
|
|
1326
1331
|
);
|
|
@@ -1365,15 +1370,23 @@ export async function buildProjectFfmpegInvocation(
|
|
|
1365
1370
|
const x = overlayPositionExpression(overlay, "x", bounds.startUs, visibleFullEndUs, animations.entranceUs, animations.exitUs);
|
|
1366
1371
|
const y = overlayPositionExpression(overlay, "y", bounds.startUs, visibleFullEndUs, animations.entranceUs, animations.exitUs);
|
|
1367
1372
|
const next = `canvas_${serial++}`;
|
|
1368
|
-
const
|
|
1373
|
+
const staticSource = source.kind === "image" || source.kind === "svg" || source.kind === "emoji";
|
|
1374
|
+
// A still's last decoded PTS can precede the final output frame, especially
|
|
1375
|
+
// after cuts or speed edits. Hold it only inside its half-open interval;
|
|
1376
|
+
// animated sources retain their explicit hide/freeze/loop behavior.
|
|
1377
|
+
const enable = staticSource
|
|
1378
|
+
? `gte(t,${seconds(resolvedOverlay.outputRange.startUs)})*lt(t,${seconds(chain.visibleEndUs)})`
|
|
1379
|
+
: `between(t,${seconds(resolvedOverlay.outputRange.startUs)},${seconds(chain.visibleEndUs)})`;
|
|
1380
|
+
const terminal = staticSource ? "eof_action=repeat:repeatlast=1" : "eof_action=pass:repeatlast=0";
|
|
1369
1381
|
const blendMode = overlay.blendMode ?? "normal";
|
|
1370
1382
|
if (blendMode === "normal") {
|
|
1371
1383
|
filters.push(
|
|
1372
|
-
`[${currentVideo}][${label}]overlay=x='${x}':y='${y}':
|
|
1384
|
+
`[${currentVideo}][${label}]overlay=x='${x}':y='${y}':${terminal}:enable='${enable}'[${next}]`,
|
|
1373
1385
|
);
|
|
1374
1386
|
} else {
|
|
1375
1387
|
const transparent = `graphic_blend_canvas_${serial++}`;
|
|
1376
1388
|
const positioned = `graphic_blend_positioned_${serial++}`;
|
|
1389
|
+
const layerColorSource = `graphic_blend_color_source_${serial++}`;
|
|
1377
1390
|
const layerColor = `graphic_blend_color_${serial++}`;
|
|
1378
1391
|
const layerAlphaSource = `graphic_blend_alpha_source_${serial++}`;
|
|
1379
1392
|
const layerMask = `graphic_blend_mask_${serial++}`;
|
|
@@ -1382,10 +1395,11 @@ export async function buildProjectFfmpegInvocation(
|
|
|
1382
1395
|
const blended = `graphic_blend_result_${serial++}`;
|
|
1383
1396
|
filters.push(
|
|
1384
1397
|
`color=c=black@0:s=${plan.output.pixelWidth}x${plan.output.pixelHeight}:r=${emittedRate}:d=${seconds(plan.output.durationUs)},format=rgba[${transparent}]`,
|
|
1385
|
-
`[${transparent}][${label}]overlay=x='${x}':y='${y}':
|
|
1386
|
-
`[${positioned}]format=rgba,split=2[${
|
|
1398
|
+
`[${transparent}][${label}]overlay=x='${x}':y='${y}':${terminal}:enable='${enable}':format=auto[${positioned}]`,
|
|
1399
|
+
`[${positioned}]format=rgba,split=2[${layerColorSource}][${layerAlphaSource}]`,
|
|
1400
|
+
`[${layerColorSource}]format=gbrp[${layerColor}]`,
|
|
1387
1401
|
`[${layerAlphaSource}]alphaextract[${layerMask}]`,
|
|
1388
|
-
`[${currentVideo}]split=2[${baseBlend}][${baseMerge}]`,
|
|
1402
|
+
`[${currentVideo}]format=gbrp,split=2[${baseBlend}][${baseMerge}]`,
|
|
1389
1403
|
`[${baseBlend}][${layerColor}]blend=all_mode=${blendMode}[${blended}]`,
|
|
1390
1404
|
`[${baseMerge}][${blended}][${layerMask}]maskedmerge[${next}]`,
|
|
1391
1405
|
);
|
|
@@ -337,7 +337,7 @@ export async function executeSpatialSceneCommand(application: ApplicationContext
|
|
|
337
337
|
if (command.action === "plan" || command.action === "render") {
|
|
338
338
|
const request = bindSpatialCliExecutionProfile(await readSpatialJson(resolve(application.paths.repositoryRoot, command.request)), command.executionProfile);
|
|
339
339
|
if (command.action === "plan") return planSpatialRender(scene, request);
|
|
340
|
-
const result = await createApplicationOperationRegistry().execute({ application, abortSignal: new AbortController().signal }, {
|
|
340
|
+
const result = await createApplicationOperationRegistry().execute({ application, abortSignal: signal ?? new AbortController().signal }, {
|
|
341
341
|
kind: "scene.render", version: 1, input: {
|
|
342
342
|
source: { path: relative(application.paths.repositoryRoot, sourcePath) }, request,
|
|
343
343
|
...(command.assets === undefined ? {} : { assets: await readSpatialJson(resolve(application.paths.repositoryRoot, command.assets)) }),
|