From 07f4c2f4989a4f22fe9c9a7e1f8e61adbb8c4a8e Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Mon, 13 Jul 2026 13:08:05 +1000 Subject: [PATCH] fix: update @shotstack/schemas to 1.14.0 and handle srcless generated assets --- package.json | 2 +- src/components/canvas/players/audio-player.ts | 7 ++++ src/components/canvas/players/image-player.ts | 4 +++ src/components/canvas/players/video-player.ts | 4 +++ .../timeline/media-thumbnail-renderer.ts | 6 ++++ tests/schema.test.ts | 36 +++++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bce606b4..ea8bb5e6 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "vite-plugin-dts": "^4.5.4" }, "dependencies": { - "@shotstack/schemas": "1.11.0", + "@shotstack/schemas": "1.14.0", "@shotstack/shotstack-canvas": "^2.10.0", "howler": "^2.2.4", "mediabunny": "^1.11.2", diff --git a/src/components/canvas/players/audio-player.ts b/src/components/canvas/players/audio-player.ts index 1ff7672a..0d055750 100644 --- a/src/components/canvas/players/audio-player.ts +++ b/src/components/canvas/players/audio-player.ts @@ -30,6 +30,10 @@ export class AudioPlayer extends Player { const audioClipConfiguration = this.clipConfiguration.asset as AudioAsset; const identifier = audioClipConfiguration.src; + if (!identifier) { + // Prompt-driven audio has no src until it's generated — fail this clip's load, not the edit + throw new Error("Audio asset has no src to load."); + } const loadOptions: pixi.UnresolvedAsset = { src: identifier, parser: AudioLoadParser.Name }; const audioResource = await this.edit.assetLoader.load(identifier, loadOptions); @@ -123,6 +127,9 @@ export class AudioPlayer extends Player { this.syncTimer = 0; const audioAsset = this.clipConfiguration.asset as AudioAsset; + if (!audioAsset.src) { + throw new Error("Audio asset has no src to load."); + } const loadOptions: pixi.UnresolvedAsset = { src: audioAsset.src, parser: AudioLoadParser.Name }; const audioResource = await this.edit.assetLoader.load(audioAsset.src, loadOptions); diff --git a/src/components/canvas/players/image-player.ts b/src/components/canvas/players/image-player.ts index c937ee53..548fa67d 100644 --- a/src/components/canvas/players/image-player.ts +++ b/src/components/canvas/players/image-player.ts @@ -86,6 +86,10 @@ export class ImagePlayer extends Player { private async loadTexture(): Promise { const imageAsset = this.clipConfiguration.asset as ImageAsset; const { src } = imageAsset; + if (!src) { + // Prompt-driven images have no src until they're generated — fail this clip's load, not the edit + throw new Error("Image asset has no src to load."); + } const corsUrl = `${src}${src.includes("?") ? "&" : "?"}x-cors=1`; const loadOptions: pixi.UnresolvedAsset = { src: corsUrl, crossorigin: "anonymous", data: {} }; diff --git a/src/components/canvas/players/video-player.ts b/src/components/canvas/players/video-player.ts index 6c64f8a2..6e88846f 100644 --- a/src/components/canvas/players/video-player.ts +++ b/src/components/canvas/players/video-player.ts @@ -168,6 +168,10 @@ export class VideoPlayer extends Player { private async loadVideo(): Promise { const videoAsset = this.clipConfiguration.asset as VideoAsset; const { src } = videoAsset; + if (!src) { + // Generated video assets have no src until they're rendered — fail this clip's load, not the edit + throw new Error("Video asset has no src to load."); + } if (src.endsWith(".mov")) { throw new Error(`Video source '${src}' is not supported. .mov files cannot be played in the browser. Please convert to .webm or .mp4 first.`); diff --git a/src/components/timeline/media-thumbnail-renderer.ts b/src/components/timeline/media-thumbnail-renderer.ts index 3a8487d7..14910a1d 100644 --- a/src/components/timeline/media-thumbnail-renderer.ts +++ b/src/components/timeline/media-thumbnail-renderer.ts @@ -127,6 +127,9 @@ export class MediaThumbnailRenderer implements ClipRenderer { this.clipStates.set(clipKey, state); try { + if (!asset.src) { + throw new Error("Video asset has no src for thumbnail generation."); + } const result = await this.generator.generateThumbnail(asset.src, asset.trim ?? 0); // Check if element is still in DOM (might have been disposed) @@ -156,6 +159,9 @@ export class MediaThumbnailRenderer implements ClipRenderer { this.clipStates.set(clipKey, state); try { + if (!asset.src) { + throw new Error("Image asset has no src for thumbnail generation."); + } const result = await this.loadImageThumbnail(asset.src); // Check if element is still in DOM (might have been disposed) diff --git a/tests/schema.test.ts b/tests/schema.test.ts index e8193c38..f0a1075b 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -387,3 +387,39 @@ describe("Real-world Use Cases", () => { expect(result.success).toBe(true); }); }); + +describe("API-accepted templates parse at load", () => { + // Templates created via the render API (e.g. by the MCP server) must open in the + // editor. These shapes were rejected by the previously pinned schemas version. + it("accepts text-to-speech assets with the newscaster option", () => { + const result = ClipSchema.safeParse({ + asset: { type: "text-to-speech", text: "Tonight's headlines", voice: "Matthew", newscaster: true }, + start: 0, + length: 5 + }); + expect(result.success).toBe(true); + }); + + it("accepts prompt-driven audio assets with voice and newscaster", () => { + const result = ClipSchema.safeParse({ + asset: { type: "audio", prompt: "Read the news intro", voice: "Joanna", newscaster: true }, + start: 0, + length: 5 + }); + expect(result.success).toBe(true); + }); + + it("accepts a destination without an options object", () => { + const result = EditSchema.safeParse({ + timeline: { + tracks: [{ clips: [{ asset: { type: "image", src: "https://example.com/a.jpg" }, start: 0, length: 3 }] }] + }, + output: { + size: { width: 1280, height: 720 }, + format: "mp4", + destinations: [{ provider: "shotstack" }] + } + }); + expect(result.success).toBe(true); + }); +});