diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 52e03c8b..b2947543 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -1978,6 +1978,109 @@ describe("GenerationComposer", () => { }); }); + it("uses edited local video duration for price requests before legacy node duration", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.startsWith("/api/points/quote")) { + return new Response(JSON.stringify({ + success: true, + data: { + points: 88, + }, + }), { status: 200 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + useWorkflowStore.setState({ + nodes: [ + videoNode("vid-1", true, { + durationSeconds: 9, + resolution: "1080p", + parameters: { + duration: 4, + }, + parameterSchema: [ + { + name: "duration", + type: "integer", + label: "Duration", + options: [ + { label: "4", value: 4 }, + { label: "10", value: 10 }, + ], + }, + ], + selectedModel: { + provider: "popiserver", + modelId: "51", + displayName: "Seedance Pro", + capabilities: ["text-to-video", "image-to-video"], + metadata: { + aiModelId: 51, + aiModelCode: "doubao-seedance-2-0-260128", + aiModelCodeAlias: "seedance-video-pro", + type: 2, + subType: 203, + billingMethod: 1, + }, + }, + }), + ], + }); + useModelStore.setState({ + detailsById: { + "51": { + id: 51, + aiModelCode: "doubao-seedance-2-0-260128", + aiModelCodeAlias: "seedance-video-pro", + name: "Seedance Pro", + type: 2, + subType: 203, + parameters: [ + { + name: "duration", + type: "integer", + label: "Duration", + options: [ + { label: "4", value: 4 }, + { label: "10", value: 10 }, + ], + }, + ], + extensionFields: [], + inputs: [], + }, + }, + }); + + render(); + + fireEvent.change(screen.getByLabelText("Duration"), { target: { value: "10" } }); + + await waitFor(() => { + expect(fetchMock.mock.calls.some(([input, init]) => { + if (!String(input).startsWith("/api/points/quote")) return false; + const body = JSON.parse(String((init as RequestInit).body)); + return body.payload?.parameters?.duration === 10; + })).toBe(true); + }); + const estimateCall = fetchMock.mock.calls.find(([input, init]) => { + if (!String(input).startsWith("/api/points/quote")) return false; + const body = JSON.parse(String((init as RequestInit).body)); + return body.payload?.parameters?.duration === 10; + }); + expect(JSON.parse(String((estimateCall?.[1] as RequestInit).body))).toMatchObject({ + source: "estimate", + payload: { + parameters: { + duration: 10, + output_video_duration_seconds: 10, + }, + }, + }); + }); + it("does not show PopiServer points when the model cannot be priced", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 0639092c..594d8814 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -380,12 +380,17 @@ function uniqueReferenceVideoInputs(videos: ComposerReferenceInput[]): ComposerR }); } -function readWorkflowNodeParameters(node: WorkflowNode | null | undefined): Record | null { +function readWorkflowNodeParameters( + node: WorkflowNode | null | undefined, + baseOverride?: Record +): Record | null { const data = node?.data as Record | undefined; const parameters = data?.parameters; - const baseParameters = parameters && typeof parameters === "object" && !Array.isArray(parameters) - ? parameters as Record - : {}; + const baseParameters = baseOverride ?? ( + parameters && typeof parameters === "object" && !Array.isArray(parameters) + ? parameters as Record + : {} + ); if (!data) return Object.keys(baseParameters).length > 0 ? baseParameters : null; const legacyParameters: Record = {}; @@ -405,11 +410,19 @@ function readWorkflowNodeParameters(node: WorkflowNode | null | undefined): Reco if (!("width" in baseParameters) && !("width" in legacyParameters)) legacyParameters.width = 1280; if (!("height" in baseParameters) && !("height" in legacyParameters)) legacyParameters.height = 720; } - if (!("duration" in baseParameters) && !("durationSeconds" in baseParameters) && !("duration_seconds" in baseParameters)) { - const durationSeconds = finiteNumberFromUnknown(data.durationSeconds); - if (durationSeconds !== null && durationSeconds > 0) { - legacyParameters.duration = durationSeconds; - legacyParameters.output_video_duration_seconds = durationSeconds; + const baseDuration = + finiteNumberFromUnknown(baseParameters.duration) ?? + finiteNumberFromUnknown(baseParameters.durationSeconds) ?? + finiteNumberFromUnknown(baseParameters.duration_seconds); + if (baseDuration !== null && baseDuration > 0) { + if (!("output_video_duration_seconds" in baseParameters)) { + legacyParameters.output_video_duration_seconds = baseDuration; + } + } else { + const nodeDurationSeconds = finiteNumberFromUnknown(data.durationSeconds); + if (nodeDurationSeconds !== null && nodeDurationSeconds > 0) { + legacyParameters.duration = nodeDurationSeconds; + legacyParameters.output_video_duration_seconds = nodeDurationSeconds; } } @@ -576,8 +589,8 @@ export function GenerationComposer({ targetNodeId, embedded = false, isHidden = prompt: finalPrompt, parameters: liveParameters ? { - ...(config.parameters ?? {}), ...liveParameters, + ...(config.parameters ?? {}), } : config.parameters, }; @@ -757,10 +770,6 @@ function ComposerEditor({ ? state.nodes.find((candidate) => candidate.id === context.node?.id) : undefined ); - const nodeExtraTaskParams = - (liveNode?.data as { extraTaskParams?: Record } | undefined)?.extraTaskParams || - EMPTY_EXTRA_TASK_PARAMS; - const buildConfigWithLiveNodeParameters = useCallback((baseConfig: ComposerConfig): ComposerConfig => { if (context.mode !== "node-edit" || !context.node) return baseConfig; const currentNode = liveNode ?? context.node; @@ -769,8 +778,8 @@ function ComposerEditor({ ? { ...baseConfig, parameters: { - ...(baseConfig.parameters ?? {}), ...liveParameters, + ...(baseConfig.parameters ?? {}), }, } : baseConfig; @@ -947,17 +956,17 @@ function ComposerEditor({ if (priceRequestIdRef.current !== requestId) return; const detailSchema = detail?.parameters ?? inputs.modelSchema; - const liveConfig = buildConfigWithLiveNodeParameters(nextConfig); - const selectedModel = liveConfig.selectedModel; - const currentParameters = liveConfig.parameters ?? {}; + const currentNode = context.mode === "node-edit" && context.node + ? liveNode ?? context.node + : null; + const currentParameters = readWorkflowNodeParameters(currentNode, nextConfig.parameters ?? {}) ?? nextConfig.parameters ?? {}; const normalizedConfig: ComposerConfig = { - ...liveConfig, - selectedModel, + ...nextConfig, parameters: { ...currentParameters, ...normalizeParametersBySchema(currentParameters, detailSchema), }, - extraTaskParams: liveConfig.extraTaskParams ?? {}, + extraTaskParams: nextConfig.extraTaskParams ?? {}, }; const quoteRequest = buildPopiserverPriceQuoteRequest(buildComposerPricingContext({ config: normalizedConfig, @@ -988,8 +997,10 @@ function ComposerEditor({ if (priceRequestIdRef.current === requestId) setPriceState({ amount: null, isLoading: false }); } }, [ - buildConfigWithLiveNodeParameters, context.capability, + context.mode, + context.node, + liveNode, onModelNeedsDetail, prompt, ]); @@ -1020,10 +1031,9 @@ function ComposerEditor({ if (latest.isHidden) return; if (suppressAutoPriceRefreshRef.current) return; if (!latest.config.selectedModel.modelId) return; - const nextConfig = buildConfigForTask(latest.config); - void refreshPrice(nextConfig, latest.prompt, latest.activeModelDetail, latest); + void refreshPrice(latest.config, latest.prompt, latest.activeModelDetail, latest); }, 200); - }, [buildConfigForTask, refreshPrice]); + }, [refreshPrice]); const clearScheduledPriceRefresh = useCallback(() => { if (priceRefreshTimerRef.current === null) return; @@ -1049,7 +1059,6 @@ function ComposerEditor({ config.selectedModel.modelId, connectedVideoDurationSeconds, isHidden, - nodeExtraTaskParams, popiserverPricingMedia, prompt, referenceMaterialsKey, @@ -1356,7 +1365,7 @@ function ComposerEditor({ config={config} modelSchema={modelSchema} extensionFields={extensionFields} - extraTaskParams={nodeExtraTaskParams} + extraTaskParams={config.extraTaskParams ?? EMPTY_EXTRA_TASK_PARAMS} canChooseModel={canChooseModel} isProcessNode={false} onModelNeedsDetail={onModelNeedsDetail} diff --git a/src/components/media/imageCropUtils.ts b/src/components/media/imageCropUtils.ts new file mode 100644 index 00000000..1dd3d7d6 --- /dev/null +++ b/src/components/media/imageCropUtils.ts @@ -0,0 +1,41 @@ +import type { MediaCropConfirmPayload } from "@/components/media/MediaCropOverlay"; + +export function getCropDrawableImageSource(source: string) { + return /^https?:\/\//i.test(source) ? `/api/proxy-media?url=${encodeURIComponent(source)}` : source; +} + +export function loadCropImage(source: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = "anonymous"; + image.onload = () => resolve(image); + image.onerror = () => reject(new Error("Image failed to load")); + image.src = getCropDrawableImageSource(source); + }); +} + +export async function createCroppedImageBlob( + sourceImage: string, + payload: MediaCropConfirmPayload +): Promise { + const canvas = document.createElement("canvas"); + canvas.width = payload.outputWidth; + canvas.height = payload.outputHeight; + const context = canvas.getContext("2d"); + if (!context) return null; + + const imageElement = await loadCropImage(sourceImage); + context.drawImage( + imageElement, + payload.cropRect.x, + payload.cropRect.y, + payload.cropRect.width, + payload.cropRect.height, + 0, + 0, + payload.outputWidth, + payload.outputHeight + ); + + return new Promise((resolve) => canvas.toBlob(resolve, "image/png")); +} diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 1e09a945..96835c68 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -30,8 +30,19 @@ import { SINGLE_INPUT_HANDLE_ID, SINGLE_OUTPUT_HANDLE_ID } from "@/utils/nodeHan import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount"; import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCapsule"; import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal"; +import { MediaCropOverlay, type MediaCropAspectRatio, type MediaCropConfirmPayload } from "@/components/media/MediaCropOverlay"; +import { CropRatioSelect, MEDIA_EDIT_TOOL, MediaEditToolbar, type MediaEditTool, type MediaEditToolOption } from "@/components/media/MediaEditToolbar"; +import { createCroppedImageBlob } from "@/components/media/imageCropUtils"; +import { uploadBlobToGatewayMedia } from "@/utils/gatewayMediaUpload"; const IMAGE_LOAD_RETRY_DELAYS_MS = [600, 1600]; +const IMAGE_MEDIA_EDIT_TOOLS: MediaEditToolOption[] = [ + { key: MEDIA_EDIT_TOOL.crop, label: "裁剪" }, + { key: MEDIA_EDIT_TOOL.splitGrid, label: "九宫格" }, + { key: MEDIA_EDIT_TOOL.repaint, label: "重绘" }, + { key: MEDIA_EDIT_TOOL.erase, label: "擦除" }, + { key: MEDIA_EDIT_TOOL.cutout, label: "抠图" }, +]; function isSameSelectedModel(a: SelectedModel | undefined, b: SelectedModel | undefined): boolean { return Boolean(a?.provider && b?.provider && a.provider === b.provider && a.modelId === b.modelId); @@ -55,6 +66,10 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV const adaptiveOutputImage = useAdaptiveImageSrc(displayImage, id); const outputImageSrc = adaptiveOutputImage; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const addNode = useWorkflowStore((state) => state.addNode); + const onConnect = useWorkflowStore((state) => state.onConnect); + const getNodeById = useWorkflowStore((state) => state.getNodeById); + const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); const openInlinePanel = useNodeInlinePanelStore((state) => state.openPanel); const updateImagePreference = useGenerationPreferenceStore((state) => state.updateImagePreference); const popiserverImageModels = useModelStore((state) => state.byKind.image); @@ -62,10 +77,18 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false); + const [isCropping, setIsCropping] = useState(false); + const [activeMediaTool, setActiveMediaTool] = useState(MEDIA_EDIT_TOOL.crop); + const [cropAspectRatio, setCropAspectRatio] = useState("original"); + const [cropPayload, setCropPayload] = useState(null); + const [isCropUploading, setIsCropUploading] = useState(false); + const [cropImageDimensions, setCropImageDimensions] = useState<{ width: number; height: number } | null>(null); + const imageRef = useRef(null); useEffect(() => { setDisplayFallbackImage(null); setImageRetryNonce(0); + setCropImageDimensions(null); }, [preferredDisplayImage]); // Register browse callback for floating header button @@ -166,6 +189,65 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV }); }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, updateNodeData]); + const handleConfirmCrop = useCallback(async (payload: MediaCropConfirmPayload) => { + const sourceImage = nodeData.outputImage; + if (!sourceImage) return; + + let blob: Blob | null = null; + try { + blob = await createCroppedImageBlob(sourceImage, payload); + } catch { + alert(t("imageInput.cropFailed")); + return; + } + + if (!blob) { + alert(t("imageInput.cropFailed")); + return; + } + + setIsCropUploading(true); + try { + const filename = `crop-${Date.now()}.png`; + const uploaded = await uploadBlobToGatewayMedia(blob, filename, "image/png", "image"); + const currentNode = getNodeById(id); + const currentPosition = currentNode?.position ?? { x: 0, y: 0 }; + const currentWidth = typeof currentNode?.width === "number" + ? currentNode.width + : typeof currentNode?.style?.width === "number" + ? currentNode.style.width + : 300; + const nextNodeId = addNode("smartImage", { + x: currentPosition.x + currentWidth + 220, + y: currentPosition.y, + }, { + image: uploaded.url, + imageRef: undefined, + previewImage: uploaded.previewUrl, + filename, + dimensions: { width: payload.outputWidth, height: payload.outputHeight }, + label: t("imageInput.crop"), + derivedImage: { + sourceNodeId: id, + operation: "crop", + }, + disablePreviewModal: true, + }); + onConnect({ + source: id, + sourceHandle: SINGLE_OUTPUT_HANDLE_ID, + target: nextNodeId, + targetHandle: SINGLE_INPUT_HANDLE_ID, + }); + selectSingleNode(nextNodeId); + setIsCropping(false); + } catch (error) { + alert(error instanceof Error ? error.message : t("imageInput.cropFailed")); + } finally { + setIsCropUploading(false); + } + }, [addNode, getNodeById, id, nodeData.outputImage, onConnect, selectSingleNode, t]); + const handleCarouselPrevious = useCallback(async () => { const history = nodeData.imageHistory || []; if (history.length === 0 || isLoadingCarouselImage) return; @@ -290,8 +372,28 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV const selectedNodeCount = useSelectedNodeCount(); const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && nodeData.outputImage); + const renderedImageSrc = isCropping ? nodeData.outputImage : outputImageSrc; const selectedActions: NodeActionCapsuleAction[] = nodeData.outputImage ? [ + { + key: "crop", + icon: ( + + + + + ), + label: t("imageInput.crop"), + onClick: (event) => { + event.preventDefault(); + event.stopPropagation(); + setActiveMediaTool(MEDIA_EDIT_TOOL.crop); + setCropAspectRatio("original"); + setCropPayload(null); + setCropImageDimensions(null); + setIsCropping(true); + }, + }, { key: "multi-angle", icon: ( @@ -392,7 +494,26 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV handleType="image" /> - {showSelectedActions && } + {showSelectedActions && !isCropping && } + + {isCropping && ( + setIsCropping(false)} + context={{ + value: cropAspectRatio, + onChange: setCropAspectRatio, + onConfirm: () => { + if (cropPayload) void handleConfirmCrop(cropPayload); + }, + }} + toolComponents={{ + [MEDIA_EDIT_TOOL.crop]: CropRatioSelect, + }} + /> + )}
Generated setImageRetryNonce(0)} + onLoad={(event) => { + setImageRetryNonce(0); + const image = event.currentTarget; + if (image.naturalWidth > 0 && image.naturalHeight > 0) { + setCropImageDimensions({ width: image.naturalWidth, height: image.naturalHeight }); + } + }} onError={() => { if (nodeData.previewImg && displayImage === nodeData.previewImg && nodeData.outputImage && nodeData.outputImage !== nodeData.previewImg) { setDisplayFallbackImage(nodeData.outputImage); @@ -428,6 +556,17 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV }} className="w-full h-full object-contain cursor-zoom-in" /> + {isCropping && cropImageDimensions && ( + + )} {/* Loading overlay for generation */} {nodeData.status === "loading" && (
diff --git a/src/components/nodes/ImageInputNode.tsx b/src/components/nodes/ImageInputNode.tsx index 655089d8..8828db50 100644 --- a/src/components/nodes/ImageInputNode.tsx +++ b/src/components/nodes/ImageInputNode.tsx @@ -24,6 +24,7 @@ import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCap import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal"; import { MediaCropOverlay, type MediaCropAspectRatio, type MediaCropConfirmPayload } from "@/components/media/MediaCropOverlay"; import { CropRatioSelect, MEDIA_EDIT_TOOL, MediaEditToolbar, type MediaEditTool, type MediaEditToolOption } from "@/components/media/MediaEditToolbar"; +import { createCroppedImageBlob } from "@/components/media/imageCropUtils"; type ImageInputNodeType = Node; const IMAGE_INPUT_DIMENSIONS = defaultNodeDimensions.imageInput; @@ -37,20 +38,6 @@ const IMAGE_MEDIA_EDIT_TOOLS: MediaEditToolOption[] = [ { key: MEDIA_EDIT_TOOL.cutout, label: "抠图" }, ]; -function getCropDrawableImageSource(source: string) { - return /^https?:\/\//i.test(source) ? `/api/proxy-media?url=${encodeURIComponent(source)}` : source; -} - -function loadCropImage(source: string): Promise { - return new Promise((resolve, reject) => { - const image = new Image(); - image.crossOrigin = "anonymous"; - image.onload = () => resolve(image); - image.onerror = () => reject(new Error("Image failed to load")); - image.src = getCropDrawableImageSource(source); - }); -} - export interface ImageInputNodeViewProps { id: string; data: ImageInputNodeData; @@ -309,30 +296,9 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal const sourceImage = nodeData.image || displayImage; if (!sourceImage) return; - const canvas = document.createElement("canvas"); - canvas.width = payload.outputWidth; - canvas.height = payload.outputHeight; - const context = canvas.getContext("2d"); - if (!context) { - alert(t("imageInput.cropFailed")); - return; - } - let blob: Blob | null = null; try { - const imageElement = await loadCropImage(sourceImage); - context.drawImage( - imageElement, - payload.cropRect.x, - payload.cropRect.y, - payload.cropRect.width, - payload.cropRect.height, - 0, - 0, - payload.outputWidth, - payload.outputHeight - ); - blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png")); + blob = await createCroppedImageBlob(sourceImage, payload); } catch { alert(t("imageInput.cropFailed")); return; diff --git a/src/utils/__tests__/composerPricing.test.ts b/src/utils/__tests__/composerPricing.test.ts index f2c923d3..f3ccbe98 100644 --- a/src/utils/__tests__/composerPricing.test.ts +++ b/src/utils/__tests__/composerPricing.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { buildPopiserverPriceQuoteRequest, - buildPopiserverPointsEstimatePayload, getQuotedPointAmount, } from "@/utils/composerPricing"; import type { SelectedModel } from "@/types"; @@ -42,7 +41,7 @@ function context(overrides: Partial & { config?: Generat describe("composerPricing", () => { it("builds PopiServer estimate payloads with provider model aliases", () => { - const payload = buildPopiserverPointsEstimatePayload(context({ + const request = buildPopiserverPriceQuoteRequest(context({ inputVideoDurationSeconds: 12, config: draft({ selectedModel: model({ @@ -56,7 +55,9 @@ describe("composerPricing", () => { }), })); - expect(payload).toEqual({ + expect(request).toEqual({ + source: "estimate", + payload: { model_name: "video-alias", estimation_type: "auto", parameters: { @@ -66,6 +67,7 @@ describe("composerPricing", () => { }, imageCount: 1, input_video_duration_seconds: 12, + }, }); }); @@ -83,13 +85,19 @@ describe("composerPricing", () => { }); it("uses PopiServer estimate only for billingMethod 2 or Seedance models", () => { - expect(buildPopiserverPointsEstimatePayload(context({ config: draft({ + expect(buildPopiserverPriceQuoteRequest(context({ config: draft({ selectedModel: model({ billingMethod: 2 }), - }) }))).not.toBeNull(); - expect(buildPopiserverPointsEstimatePayload(context({ config: draft({ - selectedModel: model({ billingMethod: 1 }), - }) }))).toBeNull(); - expect(buildPopiserverPointsEstimatePayload(context({ + }) }))).toMatchObject({ source: "estimate" }); + expect(buildPopiserverPriceQuoteRequest(context({ config: draft({ + selectedModel: model({ + billingMethod: 1, + aiModelId: 32, + aiModelCode: "image-code", + type: 1, + subType: 101, + }), + }) }))).toMatchObject({ source: "task-price" }); + expect(buildPopiserverPriceQuoteRequest(context({ capability: "video", config: draft({ selectedModel: model({ @@ -97,18 +105,19 @@ describe("composerPricing", () => { billingMethod: 1, }), }), - }))).not.toBeNull(); + }))).toMatchObject({ source: "estimate" }); }); it("builds unified PopiServer quote requests", () => { const estimateContext = context({ config: draft({ selectedModel: model({ billingMethod: 2 }), }) }); - const pointsPayload = buildPopiserverPointsEstimatePayload(estimateContext); - expect(buildPopiserverPriceQuoteRequest(estimateContext)).toEqual({ + expect(buildPopiserverPriceQuoteRequest(estimateContext)).toMatchObject({ source: "estimate", - payload: pointsPayload, + payload: { + estimation_type: "auto", + }, }); expect(buildPopiserverPriceQuoteRequest(context({ config: draft({ selectedModel: model({ diff --git a/src/utils/composerPricing.ts b/src/utils/composerPricing.ts index f330b9d7..5cc6ad9e 100644 --- a/src/utils/composerPricing.ts +++ b/src/utils/composerPricing.ts @@ -84,7 +84,7 @@ function getPopiserverModelName(model: SelectedModel): string { ); } -export function buildPopiserverPointsEstimatePayload( +function buildPopiserverPointsEstimatePayload( context: ComposerPricingContext ): PopiserverPointsEstimatePayload | null { const { config, capability, inputVideoDurationSeconds, media } = context; @@ -142,7 +142,7 @@ export function getQuotedPointAmount(response: PopiserverPriceQuoteResponse): nu return finiteNumberFromUnknown(response.data?.points); } -export function buildComposerPopiserverTaskPricePayload( +function buildComposerPopiserverTaskPricePayload( context: ComposerPricingContext ) { const { config, capability, media } = context;