From 5a623e01de2429422f684d07f8d3f9cb92a38f16 Mon Sep 17 00:00:00 2001 From: Dawid Wenderski Date: Thu, 19 Mar 2026 15:47:56 +0100 Subject: [PATCH 1/3] DEV-1656 feat: add video-replace endpoint for character replacement in video Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/endpoint-registry.ts | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/lib/endpoint-registry.ts b/src/lib/endpoint-registry.ts index 04de839..82ba2aa 100644 --- a/src/lib/endpoint-registry.ts +++ b/src/lib/endpoint-registry.ts @@ -14,7 +14,7 @@ export const ENDPOINT_GROUPS: EndpointGroupMeta[] = [ id: 'video-generation', label: 'Video Generation', icon: '🎬', - description: 'Text-to-Video, Image-to-Video', + description: 'Text-to-Video, Image-to-Video, Video Replace', }, { id: 'audio-generation', @@ -370,6 +370,43 @@ export const ENDPOINTS: EndpointDefinition[] = [ ], }, + { + id: 'video-replace', + name: 'Video Replace', + group: 'video-generation', + method: 'POST', + path: '/videos/replace', + description: 'Replace a person in video with character from reference image', + contentType: 'multipart', + isAsync: true, + hasPriceCalc: true, + priceCalcPath: '/videos/replace/price', + params: [ + { + name: 'video', + label: 'Input Video', + type: 'file', + required: true, + accept: 'video/*,.mp4,.mpeg,.mov,.avi,.wmv,.ogg', + description: 'Input video file (MP4, MPEG, QuickTime, AVI, WMV, OGG)', + }, + { + name: 'ref_image', + label: 'Reference Image', + type: 'file', + required: true, + accept: 'image/*', + description: 'Reference character image (JPG, PNG, GIF, BMP, WebP, max 10MB)', + }, + promptParam(false), + modelSelectParam(), + widthParam(), + heightParam(), + stepsParam(), + seedParam(), + ], + }, + // ── Audio / TTS ─────────────────────────────────────────── { id: 'txt2audio', From bf0f01514a77cd2a68af4d4d974849bf343eb9ad Mon Sep 17 00:00:00 2001 From: Dawid Wenderski Date: Mon, 23 Mar 2026 14:15:15 +0100 Subject: [PATCH 2/3] DEV-1656 fix: video-replace width/height not sent when empty, add duration for price calc - Width/Height defined without nullable so empty values are not sent (API uses input video dimensions when omitted) - Added duration field needed for price calculation endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/endpoint-registry.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/lib/endpoint-registry.ts b/src/lib/endpoint-registry.ts index 82ba2aa..240e5a7 100644 --- a/src/lib/endpoint-registry.ts +++ b/src/lib/endpoint-registry.ts @@ -400,10 +400,29 @@ export const ENDPOINTS: EndpointDefinition[] = [ }, promptParam(false), modelSelectParam(), - widthParam(), - heightParam(), + { + name: 'width', + label: 'Width', + type: 'number', + required: false, + description: 'Output width in pixels (empty = use input video width)', + }, + { + name: 'height', + label: 'Height', + type: 'number', + required: false, + description: 'Output height in pixels (empty = use input video height)', + }, stepsParam(), seedParam(), + { + name: 'duration', + label: 'Duration (s)', + type: 'number', + required: false, + description: 'Input video duration in seconds (for price calculation)', + }, ], }, From 23b580ef80ec43b1db9a1b3661e2fea34ae5b814 Mon Sep 17 00:00:00 2001 From: Dawid Wenderski Date: Mon, 23 Mar 2026 14:19:36 +0100 Subject: [PATCH 3/3] DEV-1656 feat: send video file with price calc request for video-replace - handleCheckPrice sends FormData (with files) for multipart endpoints - Proxy cleans up _priceCalc from formData before forwarding - Removed manual duration field (extracted from video by API) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/proxy/route.ts | 3 ++- src/components/EndpointForm.tsx | 46 ++++++++++++++++++++++++++------- src/lib/endpoint-registry.ts | 7 ----- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 32d837b..a3de5bb 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -86,8 +86,9 @@ export async function POST(request: Request) { let bodyForLog: JsonValue; if (endpoint.contentType === 'multipart' && formData) { - // Remove our internal field + // Remove our internal fields formData.delete('_endpointId'); + formData.delete('_priceCalc'); fetchOptions = { method: endpoint.method, headers, diff --git a/src/components/EndpointForm.tsx b/src/components/EndpointForm.tsx index 0af03b1..156323c 100644 --- a/src/components/EndpointForm.tsx +++ b/src/components/EndpointForm.tsx @@ -471,15 +471,43 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: try { const filteredValues = buildFilteredValues(); - const res = await fetch('/api/proxy', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ...filteredValues, - _endpointId: endpoint.id, - _priceCalc: true, - }), - }); + let res: Response; + + // For multipart endpoints with files, send as FormData (includes files for price calc) + const hasFiles = Object.keys(files).length > 0; + if (endpoint.contentType === 'multipart' && hasFiles) { + const formData = new FormData(); + formData.append('_endpointId', endpoint.id); + formData.append('_priceCalc', 'true'); + + Object.entries(filteredValues).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + formData.append(key, String(value)); + } + }); + + Object.entries(files).forEach(([key, fileOrFiles]) => { + const param = endpoint.params.find((p) => p.name === key); + if (param?.visibleWhen && !isFieldVisible(param)) return; + if (Array.isArray(fileOrFiles)) { + fileOrFiles.forEach((file) => formData.append(key, file)); + } else { + formData.append(key, fileOrFiles); + } + }); + + res = await fetch('/api/proxy', { method: 'POST', body: formData }); + } else { + res = await fetch('/api/proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...filteredValues, + _endpointId: endpoint.id, + _priceCalc: true, + }), + }); + } const data = await res.json(); diff --git a/src/lib/endpoint-registry.ts b/src/lib/endpoint-registry.ts index 240e5a7..0eba027 100644 --- a/src/lib/endpoint-registry.ts +++ b/src/lib/endpoint-registry.ts @@ -416,13 +416,6 @@ export const ENDPOINTS: EndpointDefinition[] = [ }, stepsParam(), seedParam(), - { - name: 'duration', - label: 'Duration (s)', - type: 'number', - required: false, - description: 'Input video duration in seconds (for price calculation)', - }, ], },