From 94846b580b648bc7344cd6b463682cc00016e69a Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Thu, 30 Apr 2026 17:54:51 -0700 Subject: [PATCH] artcraft: migrate frontend image/video calls to unified generate_* tauri commands Add GenerateImage / GenerateVideo wrappers in @storyteller/tauri-api that mirror the unified TauriGenerateImageRequest / TauriGenerateVideoRequest shapes and invoke generate_image_command / generate_video_command. Migrate PromptBoxImage (text-to-image), PromptBoxVideo (image-to-video), PageDraw (edit + inpaint), and Angles off the per-flow Enqueue* wrappers onto the unified ones. Old wrappers stay; Rust proxy handles routing. --- .../app/src/pages/PageAngles/Angles.tsx | 12 +- .../app/src/pages/PageDraw/PageDraw.tsx | 50 ++--- .../promptbox/src/lib/PromptBoxImage.tsx | 48 +---- .../promptbox/src/lib/PromptBoxVideo.tsx | 12 +- frontend/libs/tauri-api/src/index.ts | 2 + .../src/lib/generate/GenerateImage.ts | 185 ++++++++++++++++++ .../src/lib/generate/GenerateVideo.ts | 167 ++++++++++++++++ 7 files changed, 398 insertions(+), 78 deletions(-) create mode 100644 frontend/libs/tauri-api/src/lib/generate/GenerateImage.ts create mode 100644 frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx index 6d0179ab0d..3adeb72403 100644 --- a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx @@ -30,7 +30,7 @@ import { twMerge } from "tailwind-merge"; import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; import { SliderV2 } from "@storyteller/ui-sliderv2"; // import { Switch } from "@headlessui/react"; -import { EnqueueEditImage } from "@storyteller/tauri-api"; +import { GenerateImage } from "@storyteller/tauri-api"; import { ClassyModelSelector, ANGLES_PAGE_MODEL_LIST, @@ -278,14 +278,14 @@ export const Angles = () => { }); try { - await EnqueueEditImage({ - model: selectedModel ?? ("flux_2_lora_angles" as any), + await GenerateImage({ + model: selectedModel ?? "flux_2_lora_angles", provider: selectedProvider ?? undefined, image_media_tokens: [state.sourceMediaToken], prompt: "", - horizontal_angle: state.angleConfig.rotation, - vertical_angle: state.angleConfig.tilt, - zoom: state.angleConfig.zoom, + adjust_horizontal_angle: state.angleConfig.rotation, + adjust_vertical_angle: state.angleConfig.tilt, + adjust_zoom: state.angleConfig.zoom, frontend_subscriber_id: subscriberId, }); diff --git a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx index 27bd1767eb..a1db32c8f7 100644 --- a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx @@ -7,35 +7,32 @@ import { type ImageBundle, } from "@storyteller/ui-pagedraw"; import { - EnqueueEditImage, - EnqueueEditImageRequest, - EnqueueEditImageSize, - EnqueueEditImageResolution, - EnqueueImageInpaint, - EnqueueImageInpaintRequest, + GenerateImage, + GenerateImageRequest, EnqueueImageBgRemoval, useCanvasBgRemovedEvent, } from "@storyteller/tauri-api"; +import { CommonAspectRatio, CommonResolution } from "@storyteller/model-list"; import { useImageEditCompleteEvent } from "@storyteller/tauri-events"; import { UploadImageMedia } from "@storyteller/api"; import { BaseImageSelector } from "./BaseImageSelector"; // ─── Aspect ratio / resolution mappers ──────────────────────────────────────── -const mapAspectRatio = (ratio?: string): EnqueueEditImageSize | undefined => { +const mapAspectRatio = (ratio?: string): CommonAspectRatio | undefined => { switch (ratio) { - case "auto": return EnqueueEditImageSize.Auto; - case "wide": return EnqueueEditImageSize.Wide; - case "tall": return EnqueueEditImageSize.Tall; - case "square": return EnqueueEditImageSize.Square; + case "auto": return CommonAspectRatio.Auto; + case "wide": return CommonAspectRatio.Wide; + case "tall": return CommonAspectRatio.Tall; + case "square": return CommonAspectRatio.Square; default: return undefined; } }; -const mapResolution = (res?: string): EnqueueEditImageResolution | undefined => { +const mapResolution = (res?: string): CommonResolution | undefined => { switch (res) { - case "1k": return EnqueueEditImageResolution.OneK; - case "2k": return EnqueueEditImageResolution.TwoK; - case "4k": return EnqueueEditImageResolution.FourK; + case "1k": return CommonResolution.OneK; + case "2k": return CommonResolution.TwoK; + case "4k": return CommonResolution.FourK; default: return undefined; } }; @@ -84,34 +81,37 @@ const useTauriAdapter = (): PageDrawAdapter => { return useMemo( () => ({ enqueueEditImage: async (req) => { - const request: EnqueueEditImageRequest = { + const request: GenerateImageRequest = { model: req.model, scene_image_media_token: req.sceneImageMediaToken, image_media_tokens: req.imageMediaTokens, prompt: req.prompt, - disable_system_prompt: req.disableSystemPrompt, - image_count: req.imageCount, + enable_system_prompt: + typeof req.disableSystemPrompt === "boolean" + ? !req.disableSystemPrompt + : undefined, + batch_size: req.imageCount, aspect_ratio: mapAspectRatio(req.aspectRatio), - image_resolution: mapResolution(req.imageResolution), + resolution: mapResolution(req.imageResolution), frontend_caller: req.frontendCaller, frontend_subscriber_id: req.frontendSubscriberId, }; if (req.provider) request.provider = req.provider; - return EnqueueEditImage(request); + return GenerateImage(request); }, enqueueInpaint: async (req) => { - const request: EnqueueImageInpaintRequest = { + const request: GenerateImageRequest = { model: req.model, - image_media_token: req.imageMediaToken, - mask_image_raw_bytes: req.maskImageRawBytes, + image_media_tokens: req.imageMediaToken ? [req.imageMediaToken] : undefined, + inpainting_mask_image_raw_bytes: req.maskImageRawBytes, prompt: req.prompt, - image_count: req.imageCount, + batch_size: req.imageCount, frontend_caller: req.frontendCaller, frontend_subscriber_id: req.frontendSubscriberId, }; if (req.provider) request.provider = req.provider; - return EnqueueImageInpaint(request); + return GenerateImage(request); }, enqueueBgRemoval: async (base64Image, nodeId) => { diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx index 5f5daa4f72..9fb6ddd084 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx @@ -7,10 +7,8 @@ import { Tooltip } from "@storyteller/ui-tooltip"; import { Button, ToggleButton, GenerateButton } from "@storyteller/ui-button"; import { Modal } from "@storyteller/ui-modal"; import { - EnqueueTextToImage, - EnqueueTextToImageRequest, - EnqueueTextToImageSize, - EnqueueTextToImageResolution, + GenerateImage, + GenerateImageRequest, } from "@storyteller/tauri-api"; import { faMessageXmark, @@ -296,15 +294,10 @@ export const PromptBoxImage = ({ }, 10000); try { - const aspectRatio = getCurrentLegacyAspectRatio(); - const resolution = getCurrentResolution(); - - const request: EnqueueTextToImageRequest = { + const request: GenerateImageRequest = { prompt: prompt, model: selectedModel, - aspect_ratio: aspectRatio, - image_resolution: resolution, - number_images: generationCount, + batch_size: generationCount, frontend_caller: "text_to_image", frontend_subscriber_id: subscriberId, }; @@ -314,11 +307,11 @@ export const PromptBoxImage = ({ } if (selectedModel?.supportsNewAspectRatio()) { - request.common_aspect_ratio = commonAspectRatio; + request.aspect_ratio = commonAspectRatio; } if (selectedModel?.supportsNewResolution()) { - request.common_resolution = commonResolution; + request.resolution = commonResolution; } if (selectedModel?.supportsQuality()) { @@ -342,7 +335,7 @@ export const PromptBoxImage = ({ timestamp: Date.now(), }); - const generateResponse = await EnqueueTextToImage(request); + const generateResponse = await GenerateImage(request); console.debug("PromptBoxImage - generateResponse", generateResponse); await onEnqueuePressed?.(prompt, generationCount, subscriberId); @@ -368,33 +361,6 @@ export const PromptBoxImage = ({ } }; - const getCurrentLegacyAspectRatio = (): EnqueueTextToImageSize => { - const selected = aspectRatioList.find((item) => item.selected); - switch (selected?.label.toLowerCase()) { - case "wide": - return EnqueueTextToImageSize.Wide; - case "tall": - return EnqueueTextToImageSize.Tall; - case "square": - default: - return EnqueueTextToImageSize.Square; - } - }; - - const getCurrentResolution = (): EnqueueTextToImageResolution | undefined => { - const selected = resolutionList.find((item) => item.selected); - switch (selected?.label) { - case "1k": - return EnqueueTextToImageResolution.OneK; - case "2k": - return EnqueueTextToImageResolution.TwoK; - case "4k": - return EnqueueTextToImageResolution.FourK; - default: - return undefined; - } - }; - const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key !== "Enter") return; const isSubmitCombo = enterToGenerate && !e.shiftKey; diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index 9e0b42b3df..c275a3f7a9 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -8,8 +8,8 @@ import { Tooltip } from "@storyteller/ui-tooltip"; import { ToggleButton, GenerateButton } from "@storyteller/ui-button"; import { Modal } from "@storyteller/ui-modal"; import { - EnqueueImageToVideo, - EnqueueImageToVideoRequest, + GenerateVideo, + GenerateVideoRequest, } from "@storyteller/tauri-api"; import { faWaveformLines, @@ -703,10 +703,10 @@ export const PromptBoxVideo = ({ setIsEnqueueing(false); }, 10000); - const buildRequest = (subscriberId: string): EnqueueImageToVideoRequest => { - let request: EnqueueImageToVideoRequest = { + const buildRequest = (subscriberId: string): GenerateVideoRequest => { + let request: GenerateVideoRequest = { model: selectedModel, - image_media_token: imageMediaToken, + start_frame_image_media_token: imageMediaToken, prompt: prompt, end_frame_image_media_token: isRefMode ? undefined @@ -816,7 +816,7 @@ export const PromptBoxVideo = ({ ? crypto.randomUUID() : Math.random().toString(36).slice(2); subscriberIds.push(subscriberId); - enqueuePromises.push(EnqueueImageToVideo(buildRequest(subscriberId))); + enqueuePromises.push(GenerateVideo(buildRequest(subscriberId))); } try { diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts index 29b5ef0f6b..73fbaae175 100644 --- a/frontend/libs/tauri-api/src/index.ts +++ b/frontend/libs/tauri-api/src/index.ts @@ -14,6 +14,8 @@ export * from "./lib/enqueue/EnqueueImageTo3dWorld.js"; export * from "./lib/enqueue/EnqueueImageToGaussian.js"; export * from "./lib/enqueue/EnqueueImageToVideo.js"; export * from "./lib/enqueue/EnqueueTextToImage.js"; +export * from "./lib/generate/GenerateImage.js"; +export * from "./lib/generate/GenerateVideo.js"; export * from "./lib/events/BasicEventWrapper.js"; export * from "./lib/events/CanvasBgRemovedEvent.js"; export * from "./lib/fal/FalBackgroundRemoval.js"; diff --git a/frontend/libs/tauri-api/src/lib/generate/GenerateImage.ts b/frontend/libs/tauri-api/src/lib/generate/GenerateImage.ts new file mode 100644 index 0000000000..5ce3dbeeb7 --- /dev/null +++ b/frontend/libs/tauri-api/src/lib/generate/GenerateImage.ts @@ -0,0 +1,185 @@ +import { invoke } from "@tauri-apps/api/core"; +import { CommandResult } from "../common/CommandStatus"; +import { + CommonAspectRatio, + CommonResolution, + CommonQuality, + ImageModel, +} from "@storyteller/model-list"; +import { GenerationProvider } from "@storyteller/api-enums"; + +export interface GenerateImageRequest { + // The provider to use (defaults to Artcraft/Storyteller). + provider?: GenerationProvider; + + // The model to use. + model?: ImageModel | string; + + // Text prompt for the image generation. + prompt?: string; + + // Aspect ratio. + aspect_ratio?: CommonAspectRatio; + + // Resolution. + resolution?: CommonResolution; + + // Quality (used by OpenAI models). + quality?: CommonQuality; + + // Number of images to generate. + batch_size?: number; + + // Reference images (without semantics — purpose varies per model). + image_media_tokens?: string[]; + + // Canvas image — supply this XOR canvas_image_raw_bytes. + // Becomes the first image reference (pushing back image_media_tokens by one). + canvas_image_media_token?: string; + canvas_image_raw_bytes?: Uint8Array; + + // Scene image — supply this XOR scene_image_raw_bytes. + scene_image_media_token?: string; + scene_image_raw_bytes?: Uint8Array; + + // Inpainting mask — supply this XOR inpainting_mask_image_raw_bytes. + inpainting_mask_image_media_token?: string; + inpainting_mask_image_raw_bytes?: Uint8Array; + + // Angle adjustments (for edit models like QwenEdit, Flux2LoraAngles). + adjust_horizontal_angle?: number; + adjust_vertical_angle?: number; + adjust_zoom?: number; + + // Turn on the system prompt. + enable_system_prompt?: boolean; + + // Frontend metadata. + frontend_caller?: string; + frontend_subscriber_id?: string; + frontend_subscriber_payload?: string; +} + +interface RawGenerateImageRequest { + provider?: GenerationProvider; + model?: string; + prompt?: string; + aspect_ratio?: CommonAspectRatio; + resolution?: CommonResolution; + quality?: CommonQuality; + batch_size?: number; + image_media_tokens?: string[]; + canvas_image_media_token?: string; + canvas_image_raw_bytes?: Uint8Array; + scene_image_media_token?: string; + scene_image_raw_bytes?: Uint8Array; + inpainting_mask_image_media_token?: string; + inpainting_mask_image_raw_bytes?: Uint8Array; + adjust_horizontal_angle?: number; + adjust_vertical_angle?: number; + adjust_zoom?: number; + enable_system_prompt?: boolean; + frontend_caller?: string; + frontend_subscriber_id?: string; + frontend_subscriber_payload?: string; +} + +export enum GenerateImageErrorType { + ModelNotSpecified = "model_not_specified", + BadInput = "bad_input", + NoProviderAvailable = "no_provider_available", + ServerError = "server_error", + NeedsStorytellerCredentials = "needs_storyteller_credentials", + NeedsGrokCredentials = "needs_grok_credentials", + BillingIssue = "billing_issue", +} + +export interface GenerateImageError extends CommandResult { + error_type: GenerateImageErrorType; + error_message?: string; +} + +export type GenerateImagePayload = Record; + +export interface GenerateImageSuccess extends CommandResult { + payload: GenerateImagePayload; +} + +export type GenerateImageResult = GenerateImageSuccess | GenerateImageError; + +export const GenerateImage = async ( + request: GenerateImageRequest, +): Promise => { + let modelName: string | undefined; + + if (!!request.model) { + if (typeof request.model === "string") { + modelName = request.model; + } else if (typeof request.model.tauriId === "string") { + modelName = request.model.tauriId; + } + } + + if (!modelName) { + throw new Error( + "No model specified in request: " + JSON.stringify(request), + ); + } + + const mutableRequest: RawGenerateImageRequest = { + model: modelName, + }; + + if (!!request.provider) mutableRequest.provider = request.provider; + if (!!request.prompt) mutableRequest.prompt = request.prompt; + if (!!request.aspect_ratio) mutableRequest.aspect_ratio = request.aspect_ratio; + if (!!request.resolution) mutableRequest.resolution = request.resolution; + if (!!request.quality) mutableRequest.quality = request.quality; + if (typeof request.batch_size === "number") mutableRequest.batch_size = request.batch_size; + if (!!request.image_media_tokens && request.image_media_tokens.length > 0) { + mutableRequest.image_media_tokens = request.image_media_tokens; + } + if (!!request.canvas_image_media_token) { + mutableRequest.canvas_image_media_token = request.canvas_image_media_token; + } + if (!!request.canvas_image_raw_bytes) { + mutableRequest.canvas_image_raw_bytes = request.canvas_image_raw_bytes; + } + if (!!request.scene_image_media_token) { + mutableRequest.scene_image_media_token = request.scene_image_media_token; + } + if (!!request.scene_image_raw_bytes) { + mutableRequest.scene_image_raw_bytes = request.scene_image_raw_bytes; + } + if (!!request.inpainting_mask_image_media_token) { + mutableRequest.inpainting_mask_image_media_token = request.inpainting_mask_image_media_token; + } + if (!!request.inpainting_mask_image_raw_bytes) { + mutableRequest.inpainting_mask_image_raw_bytes = request.inpainting_mask_image_raw_bytes; + } + if (typeof request.adjust_horizontal_angle === "number") { + mutableRequest.adjust_horizontal_angle = request.adjust_horizontal_angle; + } + if (typeof request.adjust_vertical_angle === "number") { + mutableRequest.adjust_vertical_angle = request.adjust_vertical_angle; + } + if (typeof request.adjust_zoom === "number") { + mutableRequest.adjust_zoom = request.adjust_zoom; + } + if (typeof request.enable_system_prompt === "boolean") { + mutableRequest.enable_system_prompt = request.enable_system_prompt; + } + if (!!request.frontend_caller) mutableRequest.frontend_caller = request.frontend_caller; + if (!!request.frontend_subscriber_id) { + mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id; + } + if (!!request.frontend_subscriber_payload) { + mutableRequest.frontend_subscriber_payload = request.frontend_subscriber_payload; + } + + const result = await invoke("generate_image_command", { + request: mutableRequest, + }); + + return result as GenerateImageResult; +}; diff --git a/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts b/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts new file mode 100644 index 0000000000..0269208395 --- /dev/null +++ b/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts @@ -0,0 +1,167 @@ +import { invoke } from "@tauri-apps/api/core"; +import { CommandResult } from "../common/CommandStatus"; +import { + CommonAspectRatio, + CommonResolution, + VideoModel, +} from "@storyteller/model-list"; +import { GenerationProvider } from "@storyteller/api-enums"; + +export interface GenerateVideoRequest { + // The provider to use (defaults to Artcraft/Storyteller). + provider?: GenerationProvider; + + // The model to use. + model?: VideoModel | string; + + // Text prompt. + prompt?: string; + + // Negative prompt. + negative_prompt?: string; + + // Starting frame. + start_frame_image_media_token?: string; + + // Ending frame. + end_frame_image_media_token?: string; + + // Reference media tokens. + reference_image_media_tokens?: string[]; + reference_video_media_tokens?: string[]; + reference_audio_media_tokens?: string[]; + reference_character_tokens?: string[]; + + aspect_ratio?: CommonAspectRatio; + resolution?: CommonResolution; + + duration_seconds?: number; + generate_audio?: boolean; + video_batch_count?: number; + + // Deprecated on the Rust side (still read by some legacy handlers). + sora_orientation?: "portrait" | "landscape"; + grok_aspect_ratio?: "portrait" | "landscape" | "square"; + + // Frontend metadata. + frontend_caller?: string; + frontend_subscriber_id?: string; + frontend_subscriber_payload?: string; +} + +interface RawGenerateVideoRequest { + provider?: GenerationProvider; + model?: string; + prompt?: string; + negative_prompt?: string; + start_frame_image_media_token?: string; + end_frame_image_media_token?: string; + reference_image_media_tokens?: string[]; + reference_video_media_tokens?: string[]; + reference_audio_media_tokens?: string[]; + reference_character_tokens?: string[]; + aspect_ratio?: CommonAspectRatio; + resolution?: CommonResolution; + duration_seconds?: number; + generate_audio?: boolean; + video_batch_count?: number; + sora_orientation?: "portrait" | "landscape"; + grok_aspect_ratio?: "portrait" | "landscape" | "square"; + frontend_caller?: string; + frontend_subscriber_id?: string; + frontend_subscriber_payload?: string; +} + +export enum GenerateVideoErrorType { + ModelNotSpecified = "model_not_specified", + NoProviderAvailable = "no_provider_available", + ServerError = "server_error", + NeedsFalApiKey = "needs_fal_api_key", + FalError = "fal_error", + NeedsStorytellerCredentials = "needs_storyteller_credentials", +} + +export interface GenerateVideoError extends CommandResult { + error_type: GenerateVideoErrorType; + error_message?: string; +} + +export type GenerateVideoPayload = Record; + +export interface GenerateVideoSuccess extends CommandResult { + payload: GenerateVideoPayload; +} + +export type GenerateVideoResult = GenerateVideoSuccess | GenerateVideoError; + +export const GenerateVideo = async ( + request: GenerateVideoRequest, +): Promise => { + let modelName: string | undefined; + + if (!!request.model) { + if (typeof request.model === "string") { + modelName = request.model; + } else if (typeof request.model.tauriId === "string") { + modelName = request.model.tauriId; + } + } + + if (!modelName) { + throw new Error( + "No model specified in request: " + JSON.stringify(request), + ); + } + + const mutableRequest: RawGenerateVideoRequest = { + model: modelName, + }; + + if (!!request.provider) mutableRequest.provider = request.provider; + if (!!request.prompt) mutableRequest.prompt = request.prompt; + if (!!request.negative_prompt) mutableRequest.negative_prompt = request.negative_prompt; + if (!!request.start_frame_image_media_token) { + mutableRequest.start_frame_image_media_token = request.start_frame_image_media_token; + } + if (!!request.end_frame_image_media_token) { + mutableRequest.end_frame_image_media_token = request.end_frame_image_media_token; + } + if (!!request.reference_image_media_tokens && request.reference_image_media_tokens.length > 0) { + mutableRequest.reference_image_media_tokens = request.reference_image_media_tokens; + } + if (!!request.reference_video_media_tokens && request.reference_video_media_tokens.length > 0) { + mutableRequest.reference_video_media_tokens = request.reference_video_media_tokens; + } + if (!!request.reference_audio_media_tokens && request.reference_audio_media_tokens.length > 0) { + mutableRequest.reference_audio_media_tokens = request.reference_audio_media_tokens; + } + if (!!request.reference_character_tokens && request.reference_character_tokens.length > 0) { + mutableRequest.reference_character_tokens = request.reference_character_tokens; + } + if (!!request.aspect_ratio) mutableRequest.aspect_ratio = request.aspect_ratio; + if (!!request.resolution) mutableRequest.resolution = request.resolution; + if (typeof request.duration_seconds === "number") { + mutableRequest.duration_seconds = request.duration_seconds; + } + if (typeof request.generate_audio === "boolean") { + mutableRequest.generate_audio = request.generate_audio; + } + if (typeof request.video_batch_count === "number") { + mutableRequest.video_batch_count = request.video_batch_count; + } + if (!!request.sora_orientation) mutableRequest.sora_orientation = request.sora_orientation; + if (!!request.grok_aspect_ratio) mutableRequest.grok_aspect_ratio = request.grok_aspect_ratio; + if (!!request.frontend_caller) mutableRequest.frontend_caller = request.frontend_caller; + if (!!request.frontend_subscriber_id) { + mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id; + } + if (!!request.frontend_subscriber_payload) { + mutableRequest.frontend_subscriber_payload = request.frontend_subscriber_payload; + } + + const result = await invoke("generate_video_command", { + request: mutableRequest, + }); + + return result as GenerateVideoResult; +};