Browse Source

修改 比例字段传参

feature/composer
TianYun 4 weeks ago
parent
commit
0b3ef78d57
  1. 25
      src/app/api/__tests__/popiserverModels.test.ts
  2. 17
      src/app/api/_popiserverModels.ts
  3. 18
      src/app/api/generate/providers/__tests__/popiserver.test.ts
  4. 14
      src/app/api/generate/providers/popiserver.ts
  5. 4
      src/app/canvas/page.tsx
  6. 6
      src/app/globals.css
  7. 11
      src/app/layout.tsx
  8. 52
      src/components/AntdThemeProvider.tsx
  9. 16
      src/components/AssetLibraryDrawer.tsx
  10. 96
      src/components/WorkflowCanvas.tsx
  11. 2
      src/store/utils/__tests__/localStorage.test.ts
  12. 2
      src/types/canvas.ts
  13. 1
      src/utils/__tests__/generationConfig.test.ts
  14. 4
      src/utils/generationConfig.ts

25
src/app/api/__tests__/popiserverModels.test.ts

@ -166,4 +166,29 @@ describe("popiserver model helpers", () => {
}),
]);
});
it("uses displayDimensions name as the request parameter name", () => {
const parameters = buildPopiModelParameters({
id: 38,
code: "model-with-aspect-ratio-key",
displayDimensions: JSON.stringify({
dimensions: {
aspectRatio: {
name: "ratio",
options: [
{ value: "16:9", label: "16:9" },
],
},
},
}),
});
expect(parameters).toEqual([
expect.objectContaining({
name: "ratio",
options: [{ value: "16:9", label: "16:9" }],
default: "16:9",
}),
]);
});
});

17
src/app/api/_popiserverModels.ts

@ -282,6 +282,15 @@ const POPI_DIMENSION_PARAMETER_TYPES: Record<string, ModelParameter["type"]> = {
videoRatio: "integer",
};
const POPI_DISPLAY_DIMENSION_PARAMETER_NAMES = new Set([
"ratio",
"aspectRatio",
"aspect_ratio",
"resolution",
"duration",
"videoRatio",
]);
function parsePopiDisplayDimensions(displayDimensions: string | undefined): Record<string, PopiDisplayDimension> | null {
if (!displayDimensions) return null;
try {
@ -300,6 +309,10 @@ function buildPopiDisplayDimensionParameters(model: PopiAIModel): ModelParameter
const parameters: ModelParameter[] = [];
for (const [dimensionKey, dimension] of Object.entries(dimensions)) {
const dimensionName = typeof dimension.name === "string" ? dimension.name.trim() : "";
const parameterName = POPI_DISPLAY_DIMENSION_PARAMETER_NAMES.has(dimensionName)
? dimensionName
: dimensionKey;
const options = Array.isArray(dimension.options)
? dimension.options
.filter((option) => option.value !== undefined)
@ -314,9 +327,9 @@ function buildPopiDisplayDimensionParameters(model: PopiAIModel): ModelParameter
?? (typeof options[0]?.value === "number" ? "number" : "string");
parameters.push({
name: dimensionKey,
name: parameterName,
type,
label: typeof dimension.name === "string" ? dimension.name : undefined,
label: dimensionName || undefined,
options,
default: options[0]?.value,
required: false,

18
src/app/api/generate/providers/__tests__/popiserver.test.ts

@ -83,7 +83,8 @@ describe("popiserver generation provider", () => {
expect(result.taskId).toBe("1189");
const [, init] = vi.mocked(global.fetch).mock.calls[0];
expect(init?.method).toBe("POST");
expect(JSON.parse(String(init?.body))).toMatchObject({
const body = JSON.parse(String(init?.body));
expect(body).toMatchObject({
type: 2,
subType: 203,
aiPlatform: "GATEWAY",
@ -94,10 +95,11 @@ describe("popiserver generation provider", () => {
aiModelCodeAlias: "viduq2-pro",
batchSize: 1,
ratio: "16:9",
aspectRatio: "16:9",
resolution: "720",
videoRatio: 10,
});
expect(body.aspectRatio).toBeUndefined();
expect(body.aspect_ratio).toBeUndefined();
});
it("embeds ratio and resolution in chatPrompt for gpt-image-2-vip", async () => {
@ -133,7 +135,8 @@ describe("popiserver generation provider", () => {
await submitPopiTask(makeRequest(), "login-token", input);
const [, init] = vi.mocked(global.fetch).mock.calls[0];
expect(JSON.parse(String(init?.body))).toMatchObject({
const body = JSON.parse(String(init?.body));
expect(body).toMatchObject({
chatPrompt: "比例:16:9\n清晰度:1K\n提示词:create a cinematic cat",
model: "",
ratio: "16:9",
@ -167,19 +170,22 @@ describe("popiserver generation provider", () => {
};
input.prompt = "create a clean product render";
input.parameters = {
aspectRatio: "9:16",
ratio: "9:16",
resolution: "2K",
};
await submitPopiTask(makeRequest(), "login-token", input);
const [, init] = vi.mocked(global.fetch).mock.calls[0];
expect(JSON.parse(String(init?.body))).toMatchObject({
const body = JSON.parse(String(init?.body));
expect(body).toMatchObject({
chatPrompt: "比例:9:16\n清晰度:2K\n提示词:create a clean product render",
model: "",
aspectRatio: "9:16",
ratio: "9:16",
resolution: "2K",
});
expect(body.aspectRatio).toBeUndefined();
expect(body.aspect_ratio).toBeUndefined();
});
it("builds referenceSubjectList for image tasks without @ in names", async () => {

14
src/app/api/generate/providers/popiserver.ts

@ -335,6 +335,14 @@ function resolveAspectRatio(parameters: Record<string, unknown> | undefined): st
return asString(parameters?.ratio) ?? asString(parameters?.aspectRatio) ?? "16:9";
}
function removePopiRatioAliases(parameters: Record<string, unknown>): Record<string, unknown> {
const normalized = { ...parameters };
delete normalized.ratio;
delete normalized.aspectRatio;
delete normalized.aspect_ratio;
return normalized;
}
function dimensionsForAspectRatio(aspectRatio: string): { width: number; height: number } {
if (aspectRatio === "9:16") return { width: 720, height: 1280 };
if (aspectRatio === "1:1") return { width: 1024, height: 1024 };
@ -462,7 +470,8 @@ function buildReferenceSubjectList(
function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | null): Record<string, unknown> {
const rawParameters = removeLegacyAudioParameters(input.parameters || {});
const parameters = { ...rawParameters };
const aspectRatio = resolveAspectRatio(rawParameters);
const parameters = removePopiRatioAliases(rawParameters);
delete parameters.referenceSubjectList;
const selectedModel = {
provider: "popiserver" as const,
@ -473,7 +482,6 @@ function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | nu
};
const type = asNumber(parameters.type) ?? inferTaskType(input);
const subType = asNumber(parameters.subType) ?? inferTaskSubType(input, type);
const aspectRatio = resolveAspectRatio(parameters);
const dimensions = dimensionsForAspectRatio(aspectRatio);
const modelCode =
asString(input.model.metadata?.aiModelCode) ??
@ -535,7 +543,7 @@ function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | nu
styleId: asNumber(parameters.styleId) ?? 0,
width: asNumber(parameters.width) ?? dimensions.width,
height: asNumber(parameters.height) ?? dimensions.height,
aspectRatio,
ratio: aspectRatio,
duration: asNumber(parameters.duration) ?? undefined,
resolution: asString(parameters.resolution) ?? "720p",
};

4
src/app/canvas/page.tsx

@ -24,7 +24,7 @@ function parseCanvasTheme(value: string | null): CanvasTheme | null {
}
function getAppliedCanvasTheme(): CanvasTheme {
return document.documentElement.dataset.canvasTheme === "light" ? "light" : "dark";
return document.documentElement.dataset.canvasTheme === "dark" ? "dark" : "light";
}
function applyCanvasTheme(theme: CanvasTheme) {
@ -76,7 +76,7 @@ function CanvasPageContent() {
useEffect(() => {
return () => {
delete document.documentElement.dataset.canvasTheme;
document.documentElement.dataset.canvasTheme = "light";
};
}, []);

6
src/app/globals.css

@ -8,7 +8,7 @@
color-scheme: dark;
--canvas-bg: #101112;
--canvas-background: var(--canvas-bg);
--canvas-grid: rgba(255, 255, 255, 0.035);
--canvas-grid: #ffffff;
--surface-1: #171819;
--surface-2: #202122;
--surface-3: #2a2b2d;
@ -61,8 +61,8 @@
:root[data-canvas-theme="light"] {
color-scheme: light;
--canvas-bg: #f0f0f0;
--canvas-background: linear-gradient(132deg, #E9DAEE 3.04%, #EAEBEB 24.64%, #F0F0F0 50%, #DAD6E5 96.96%);
--canvas-grid: rgba(15, 23, 42, 0.025);
--canvas-background: linear-gradient(132deg, #E9DAEE 3.04%, #E9E9E9 50%, #DAD6E5 96.96%);
--canvas-grid: #EBEBEB;
--surface-1: #ffffff;
--surface-2: #ffffff;
--surface-3: #f3f3f2;

11
src/app/layout.tsx

@ -21,12 +21,15 @@ const canvasThemeScript = `
return;
}
const stored = window.localStorage.getItem("node-banana-canvas-navigation");
if (!stored) return;
const settings = JSON.parse(stored);
if (settings && settings.theme === "light") {
if (!stored) {
document.documentElement.dataset.canvasTheme = "light";
return;
}
} catch {}
const settings = JSON.parse(stored);
document.documentElement.dataset.canvasTheme = settings && settings.theme === "dark" ? "dark" : "light";
} catch {
document.documentElement.dataset.canvasTheme = "light";
}
})();
`;

52
src/components/AntdThemeProvider.tsx

@ -34,32 +34,32 @@ interface CanvasThemeTokens {
}
const FALLBACK_TOKENS: CanvasThemeTokens = {
mode: "dark",
background: "#171717",
panel: "#262626",
panelElevated: "#1f1f1f",
panelMuted: "#333333",
panelHover: "#404040",
panelActive: "#525252",
overlay: "rgba(0, 0, 0, 0.6)",
border: "#404040",
borderStrong: "#525252",
text: "#fafafa",
textSecondary: "#d4d4d4",
textMuted: "#a3a3a3",
textDisabled: "#737373",
mode: "light",
background: "#f0f0f0",
panel: "#ffffff",
panelElevated: "#ffffff",
panelMuted: "#f3f3f2",
panelHover: "#f5f5f4",
panelActive: "#ececea",
overlay: "rgba(15, 23, 42, 0.38)",
border: "rgba(234, 235, 236, 0.24)",
borderStrong: "rgba(15, 23, 42, 0.12)",
text: "#1f2328",
textSecondary: "#4b5563",
textMuted: "#737780",
textDisabled: "#a1a4aa",
textOnOverlay: "#ffffff",
accent: "#3b82f6",
accentHover: "#60a5fa",
accentSoft: "rgba(59, 130, 246, 0.16)",
accentRing: "rgba(59, 130, 246, 0.4)",
danger: "#ef4444",
dangerSoft: "rgba(239, 68, 68, 0.16)",
success: "#10b981",
successSoft: "rgba(16, 185, 129, 0.16)",
warning: "#f59e0b",
warningSoft: "rgba(245, 158, 11, 0.16)",
shadowPanel: "0 12px 30px rgba(0, 0, 0, 0.35)",
accent: "#2563eb",
accentHover: "#1d4ed8",
accentSoft: "rgba(37, 99, 235, 0.12)",
accentRing: "rgba(37, 99, 235, 0.28)",
danger: "#dc2626",
dangerSoft: "rgba(220, 38, 38, 0.12)",
success: "#059669",
successSoft: "rgba(5, 150, 105, 0.12)",
warning: "#d97706",
warningSoft: "rgba(217, 119, 6, 0.14)",
shadowPanel: "0 8px 22px rgba(15, 23, 42, 0.08)",
};
function readCssToken(styles: CSSStyleDeclaration, name: string, fallback: string): string {
@ -72,7 +72,7 @@ function readCanvasThemeTokens(): CanvasThemeTokens {
}
const styles = window.getComputedStyle(document.documentElement);
const mode = document.documentElement.dataset.canvasTheme === "light" ? "light" : "dark";
const mode = document.documentElement.dataset.canvasTheme === "dark" ? "dark" : "light";
return {
mode,

16
src/components/AssetLibraryDrawer.tsx

@ -402,16 +402,6 @@ export function AssetLibraryDrawer({ open, onClose }: AssetLibraryDrawerProps) {
}, [childrenByFolderId, creatingFolderParentId, loadedFolderIds, loadingFolderIds]);
const treeData = useMemo<AssetTreeNode[]>(() => buildTreeNodes(folders, 0), [buildTreeNodes, folders]);
const movePickerFolders = useMemo<UserFileItem[]>(() => [{
id: ROOT_PARENT_ID,
parentId: ROOT_PARENT_ID,
nodeType: "folder",
name: t("assetLibrary.rootFolder"),
}], [t]);
const movePickerChildrenByFolderId = useMemo(() => ({
...childrenByFolderId,
[ROOT_PARENT_ID]: folders,
}), [childrenByFolderId, folders]);
const disabledMoveFolderIds = useMemo(() => {
if (!movingItem || movingItem.nodeType !== "folder") return undefined;
@ -1201,11 +1191,11 @@ export function AssetLibraryDrawer({ open, onClose }: AssetLibraryDrawerProps) {
open={movingItem !== null}
title={t("assetLibrary.moveTitle")}
confirmText={t("assetLibrary.move")}
folders={movePickerFolders}
childrenByFolderId={movePickerChildrenByFolderId}
folders={folders}
childrenByFolderId={childrenByFolderId}
loadingFolderIds={loadingFolderIds}
loadedFolderIds={loadedFolderIds}
initialFolderId={movingItem?.parentId ?? ROOT_PARENT_ID}
initialFolderId={movingItem?.parentId === ROOT_PARENT_ID ? null : movingItem?.parentId ?? null}
disabledFolderIds={disabledMoveFolderIds}
confirmLoading={submittingMove}
onLoadFolderChildren={loadFolderChildren}

96
src/components/WorkflowCanvas.tsx

@ -139,6 +139,9 @@ function isLocalKeyboardTarget(target: EventTarget | null): boolean {
// Let fitView zoom out for large workflows, but never zoom in a single new node.
const FIT_VIEW_OPTIONS = { maxZoom: 1 };
const CANVAS_MIN_ZOOM = 0.1;
const CANVAS_MAX_ZOOM = 4;
const WHEEL_ZOOM_FACTOR = 1.15;
// Connection validation rules
// - Image handles can only connect to image handles
@ -439,7 +442,7 @@ export function WorkflowCanvas() {
const canUndo = useWorkflowStore((state) => state.canUndo);
const canRedo = useWorkflowStore((state) => state.canRedo);
const openAnnotationModal = useAnnotationStore((state) => state.openModal);
const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter, fitView } = useReactFlow();
const { screenToFlowPosition, getViewport, setViewport, setCenter, fitView } = useReactFlow();
const { show: showToast } = useToast();
const [isDragOver, setIsDragOver] = useState(false);
const [dropType, setDropType] = useState<"image" | "video" | "audio" | "workflow" | "node" | "text" | null>(null);
@ -455,27 +458,59 @@ export function WorkflowCanvas() {
const miniMapNodeColor = "var(--text-disabled)";
const boxSelectionActiveRef = useRef(false);
const boxSelectionEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const tutorialViewportSet = useRef(false);
const syncZoomPercent = useCallback(() => {
setZoomPercent(Math.round(getViewport().zoom * 100));
}, [getViewport]);
const handleZoomPercentChange = useCallback((value: number) => {
const getCanvasScreenCenter = useCallback(() => {
const bounds = reactFlowWrapper.current?.getBoundingClientRect();
if (!bounds) {
return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
}
return {
x: bounds.left + bounds.width / 2,
y: bounds.top + bounds.height / 2,
};
}, []);
const zoomAtScreenPoint = useCallback((
point: { x: number; y: number },
zoom: number,
options?: { duration?: number }
) => {
const viewport = getViewport();
const nextZoom = value / 100;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const flowCenterX = (centerX - viewport.x) / viewport.zoom;
const flowCenterY = (centerY - viewport.y) / viewport.zoom;
const currentZoom = Number.isFinite(viewport.zoom) && viewport.zoom > 0 ? viewport.zoom : 1;
const nextZoom = Math.min(CANVAS_MAX_ZOOM, Math.max(CANVAS_MIN_ZOOM, zoom));
const flowX = (point.x - viewport.x) / currentZoom;
const flowY = (point.y - viewport.y) / currentZoom;
setZoomPercent(value);
setZoomPercent(Math.round(nextZoom * 100));
setViewport({
x: centerX - flowCenterX * nextZoom,
y: centerY - flowCenterY * nextZoom,
x: point.x - flowX * nextZoom,
y: point.y - flowY * nextZoom,
zoom: nextZoom,
});
}, options);
}, [getViewport, setViewport]);
const getCanvasFlowCenter = useCallback(() => {
const viewport = getViewport();
const center = getCanvasScreenCenter();
const zoom = Number.isFinite(viewport.zoom) && viewport.zoom > 0 ? viewport.zoom : 1;
return {
centerX: (center.x - viewport.x) / zoom,
centerY: (center.y - viewport.y) / zoom,
};
}, [getCanvasScreenCenter, getViewport]);
const handleZoomPercentChange = useCallback((value: number) => {
zoomAtScreenPoint(getCanvasScreenCenter(), value / 100);
}, [getCanvasScreenCenter, zoomAtScreenPoint]);
const handleFitViewFromToolbar = useCallback(() => {
void fitView({ ...FIT_VIEW_OPTIONS, padding: 0.2, duration: 300 });
}, [fitView]);
@ -519,9 +554,6 @@ export function WorkflowCanvas() {
});
}, [canvasNavigationSettings, updateCanvasNavigationSettings]);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const tutorialViewportSet = useRef(false);
// FTUX tutorial state (client-side only to avoid SSR hydration issues)
const [tutorialActive, setTutorialActive] = useState(false);
const [lockedFeatures, setLockedFeatures] = useState(false);
@ -1868,15 +1900,16 @@ export function WorkflowCanvas() {
if (scrollableElement) return;
event.preventDefault();
if (event.deltaY < 0) zoomIn();
else zoomOut();
const viewport = getViewport();
const factor = event.deltaY < 0 ? WHEEL_ZOOM_FACTOR : 1 / WHEEL_ZOOM_FACTOR;
zoomAtScreenPoint({ x: event.clientX, y: event.clientY }, viewport.zoom * factor);
};
wrapper.addEventListener('wheel', handleWheelNonPassive, { passive: false });
return () => {
wrapper.removeEventListener('wheel', handleWheelNonPassive);
};
}, [isModalOpen, zoomIn, zoomOut]);
}, [getViewport, isModalOpen, zoomAtScreenPoint]);
// Keyboard shortcuts for copy/paste and stacking selected nodes
const handleKeyDown = useCallback((event: KeyboardEvent) => {
@ -1890,15 +1923,15 @@ export function WorkflowCanvas() {
if (isZoomInKey || isZoomOutKey || isZoomResetKey) {
event.preventDefault();
event.stopPropagation();
const center = getCanvasScreenCenter();
const viewport = getViewport();
if (isZoomInKey) {
zoomIn();
zoomAtScreenPoint(center, viewport.zoom * WHEEL_ZOOM_FACTOR, { duration: 160 });
} else if (isZoomOutKey) {
zoomOut();
zoomAtScreenPoint(center, viewport.zoom / WHEEL_ZOOM_FACTOR, { duration: 160 });
} else {
const viewport = getViewport();
setViewport({ ...viewport, zoom: 1 }, { duration: 160 });
zoomAtScreenPoint(center, 1, { duration: 160 });
}
syncZoomPercent();
return;
}
}
@ -1943,10 +1976,7 @@ export function WorkflowCanvas() {
// Helper to get viewport center position in flow coordinates
const getViewportCenter = () => {
const viewport = getViewport();
const centerX = (-viewport.x + window.innerWidth / 2) / viewport.zoom;
const centerY = (-viewport.y + window.innerHeight / 2) / viewport.zoom;
return { centerX, centerY };
return getCanvasFlowCenter();
};
// Handle node creation hotkeys (Shift + key)
@ -2056,9 +2086,7 @@ export function WorkflowCanvas() {
});
} else {
// No imageInput node selected - create a new one at viewport center
const viewport = getViewport();
const centerX = (-viewport.x + window.innerWidth / 2) / viewport.zoom;
const centerY = (-viewport.y + window.innerHeight / 2) / viewport.zoom;
const { centerX, centerY } = getCanvasFlowCenter();
// ImageInput node default dimensions: 300x280
const nodeId = addNode("imageInput", { x: centerX - 150, y: centerY - 140 });
@ -2076,9 +2104,7 @@ export function WorkflowCanvas() {
const blob = await item.getType('text/plain');
const text = await blob.text();
if (text.trim()) {
const viewport = getViewport();
const centerX = (-viewport.x + window.innerWidth / 2) / viewport.zoom;
const centerY = (-viewport.y + window.innerHeight / 2) / viewport.zoom;
const { centerX, centerY } = getCanvasFlowCenter();
const nodeId = addNode("smartText", { x: centerX - 160, y: centerY - 110 });
updateNodeData(nodeId, { manualLocked: true, manualText: text });
return; // Exit after handling text
@ -2182,7 +2208,7 @@ export function WorkflowCanvas() {
]);
});
}
}, [nodes, onNodesChange, copySelectedNodes, pasteNodes, clearClipboard, clipboard, getViewport, addNode, updateNodeData, executeWorkflow, setShortcutsDialogOpen, undo, redo, isModalOpen, zoomIn, zoomOut, setViewport, syncZoomPercent]);
}, [nodes, onNodesChange, copySelectedNodes, pasteNodes, clearClipboard, clipboard, getViewport, getCanvasScreenCenter, getCanvasFlowCenter, addNode, updateNodeData, executeWorkflow, setShortcutsDialogOpen, undo, redo, isModalOpen, zoomAtScreenPoint]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown, { capture: true });
@ -2722,8 +2748,8 @@ export function WorkflowCanvas() {
zoomOnScroll={tutorialActive ? false : false}
zoomOnPinch={tutorialActive ? false : !isModalOpen}
zoomOnDoubleClick={false}
minZoom={0.1}
maxZoom={4}
minZoom={CANVAS_MIN_ZOOM}
maxZoom={CANVAS_MAX_ZOOM}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
panActivationKeyCode={null}
nodesDraggable={!isModalOpen}

2
src/store/utils/__tests__/localStorage.test.ts

@ -366,7 +366,7 @@ describe("localStorage utilities", () => {
const result = getCanvasNavigationSettings();
expect(result).toEqual({
...legacySettings,
theme: "dark",
theme: "light",
assistantEnabled: false,
});
});

2
src/types/canvas.ts

@ -18,7 +18,7 @@ export interface CanvasNavigationSettings {
}
export const defaultCanvasNavigationSettings: CanvasNavigationSettings = {
theme: "dark",
theme: "light",
panMode: "always",
zoomMode: "scroll",
selectionMode: "rightSpace",

1
src/utils/__tests__/generationConfig.test.ts

@ -128,7 +128,6 @@ describe("generationConfig", () => {
subType: 203,
aiModelId: 37,
aiModelCode: "seedance",
aspectRatio: "9:16",
ratio: "9:16",
resolution: "1080p",
duration: 8,

4
src/utils/generationConfig.ts

@ -131,7 +131,7 @@ export type PopiserverTaskPricePayload = {
aiModelCode?: string;
aiModelCodeAlias?: string;
model: string;
aspectRatio: string;
ratio: string;
resolution: string;
duration?: number;
width: number;
@ -561,7 +561,7 @@ export function buildPopiserverTaskPricePayload(
aiModelCode: stringFromUnknown(model.metadata?.aiModelCode) || undefined,
aiModelCodeAlias: stringFromUnknown(model.metadata?.aiModelCodeAlias) || undefined,
model: modelName,
aspectRatio,
ratio: aspectRatio,
resolution: config.resolution || (type === 2 ? DEFAULT_VIDEO_RESOLUTION : "2K"),
duration: duration ?? undefined,
width: dimensions.width,

Loading…
Cancel
Save