refactor(video): extract Runway helpers

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
oyi77
2026-08-09 08:31:55 -03:00
committed by diegosouzapw
parent a5305808e6
commit a1f2d98a29
2 changed files with 133 additions and 142 deletions

View File

@@ -20,6 +20,14 @@ import { handleSegmindVideoGeneration } from "./videoGeneration/providers/segmin
import { handleAdobeFireflyVideoGeneration } from "./videoGeneration/adobeFireflyHandler.ts";
import { handleOpenAIVideoGeneration } from "./videoGeneration/openai.ts";
import { getVideoJobPreset, handleVideoJobGeneration } from "./videoGeneration/job.ts";
import {
extractRunwayFailureMessage,
normalizeRunwayVideoResult,
resolvePositiveInteger,
resolveRunwayDuration,
resolveRunwayPromptImage,
resolveRunwayRatio,
} from "./videoGeneration/runwayHelpers.ts";
import { getExecutor } from "../executors/index.ts";
import { getKieTaskId, isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
import {
@@ -966,148 +974,6 @@ const RUNWAY_TERMINAL_FAILURE_STATUSES = new Set([
"DELETED",
]);
function resolveRunwayPromptImage(body) {
const directCandidates = [
body.promptImage,
body.prompt_image,
body.image,
body.image_url,
body.imageUrl,
body.provider_options?.promptImage,
body.provider_options?.prompt_image,
];
for (const candidate of directCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
if (candidate && typeof candidate === "object") return candidate;
if (Array.isArray(candidate) && candidate.length > 0) return candidate;
}
const arrayCandidates = [
body.imageUrls,
body.image_urls,
body.provider_options?.imageUrls,
body.provider_options?.image_urls,
];
for (const candidate of arrayCandidates) {
if (Array.isArray(candidate) && candidate.length > 0) return candidate;
}
return null;
}
function resolveRunwayRatio(body) {
const aspectRatio = typeof body.aspect_ratio === "string" ? body.aspect_ratio : body.aspectRatio;
if (aspectRatio === "1280:720" || aspectRatio === "720:1280") return aspectRatio;
if (aspectRatio === "16:9") return "1280:720";
if (aspectRatio === "9:16") return "720:1280";
const size = typeof body.size === "string" ? body.size : "";
const [widthRaw, heightRaw] = size.split("x");
const width = Number(widthRaw);
const height = Number(heightRaw);
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
return width >= height ? "1280:720" : "720:1280";
}
return "1280:720";
}
function resolveRunwayDuration(body) {
if (Number.isFinite(body.duration)) {
return clampRunwayDuration(body.duration);
}
if (Number.isFinite(body.frames) && Number.isFinite(body.fps) && Number(body.fps) > 0) {
return clampRunwayDuration(Number(body.frames) / Number(body.fps));
}
return 5;
}
function clampRunwayDuration(value) {
const duration = Math.round(Number(value));
if (!Number.isFinite(duration)) return 5;
return Math.min(10, Math.max(2, duration));
}
function resolvePositiveInteger(value, fallback) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
return Math.floor(numeric);
}
function extractRunwayOutputUrls(task) {
const rawOutput = Array.isArray(task?.output)
? task.output
: Array.isArray(task?.result)
? task.result
: [];
return rawOutput
.map((entry) => {
if (typeof entry === "string") return entry;
if (!entry || typeof entry !== "object") return null;
return entry.url || entry.uri || entry.videoUrl || entry.video_url || null;
})
.filter((value) => typeof value === "string" && value.length > 0);
}
function extractRunwayFailureMessage(task) {
const directCandidates = [
task?.failure,
task?.failureReason,
task?.error,
task?.errorMessage,
task?.message,
];
for (const candidate of directCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
if (task?.failure && typeof task.failure === "object") {
const nestedCandidates = [
task.failure.message,
task.failure.reason,
task.failure.error,
task.failure.code,
];
for (const candidate of nestedCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
}
return null;
}
async function normalizeRunwayVideoResult(task, body) {
const urls = extractRunwayOutputUrls(task);
if (urls.length === 0) {
throw new Error(
`Runway task completed without output URLs: ${JSON.stringify(task).slice(0, 400)}`
);
}
if (body.response_format === "url") {
return urls.map((url) => ({ url, format: "mp4" }));
}
const videos = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Runway output fetch failed (${response.status})`);
}
const arrayBuffer = await response.arrayBuffer();
videos.push({
b64_json: Buffer.from(arrayBuffer).toString("base64"),
format: "mp4",
});
}
return videos;
}
async function handleHaiperVideoGeneration({
model,
provider,

View File

@@ -0,0 +1,125 @@
export function resolveRunwayPromptImage(body) {
const directCandidates = [
body.promptImage,
body.prompt_image,
body.image,
body.image_url,
body.imageUrl,
body.provider_options?.promptImage,
body.provider_options?.prompt_image,
];
for (const candidate of directCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
if (candidate && typeof candidate === "object") return candidate;
if (Array.isArray(candidate) && candidate.length > 0) return candidate;
}
const arrayCandidates = [
body.imageUrls,
body.image_urls,
body.provider_options?.imageUrls,
body.provider_options?.image_urls,
];
for (const candidate of arrayCandidates) {
if (Array.isArray(candidate) && candidate.length > 0) return candidate;
}
return null;
}
export function resolveRunwayRatio(body) {
const aspectRatio = typeof body.aspect_ratio === "string" ? body.aspect_ratio : body.aspectRatio;
if (aspectRatio === "1280:720" || aspectRatio === "720:1280") return aspectRatio;
if (aspectRatio === "16:9") return "1280:720";
if (aspectRatio === "9:16") return "720:1280";
const size = typeof body.size === "string" ? body.size : "";
const [widthRaw, heightRaw] = size.split("x");
const width = Number(widthRaw);
const height = Number(heightRaw);
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
return width >= height ? "1280:720" : "720:1280";
}
return "1280:720";
}
export function resolveRunwayDuration(body) {
if (Number.isFinite(body.duration)) return clampRunwayDuration(body.duration);
if (Number.isFinite(body.frames) && Number.isFinite(body.fps) && Number(body.fps) > 0) {
return clampRunwayDuration(Number(body.frames) / Number(body.fps));
}
return 5;
}
function clampRunwayDuration(value) {
const duration = Math.round(Number(value));
if (!Number.isFinite(duration)) return 5;
return Math.min(10, Math.max(2, duration));
}
export function resolvePositiveInteger(value, fallback) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
return Math.floor(numeric);
}
function extractRunwayOutputUrls(task) {
const rawOutput = Array.isArray(task?.output)
? task.output
: Array.isArray(task?.result)
? task.result
: [];
return rawOutput
.map((entry) => {
if (typeof entry === "string") return entry;
if (!entry || typeof entry !== "object") return null;
return entry.url || entry.uri || entry.videoUrl || entry.video_url || null;
})
.filter((value) => typeof value === "string" && value.length > 0);
}
export function extractRunwayFailureMessage(task) {
const directCandidates = [
task?.failure,
task?.failureReason,
task?.error,
task?.errorMessage,
task?.message,
];
for (const candidate of directCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
if (task?.failure && typeof task.failure === "object") {
const nestedCandidates = [
task.failure.message,
task.failure.reason,
task.failure.error,
task.failure.code,
];
for (const candidate of nestedCandidates) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
}
return null;
}
export async function normalizeRunwayVideoResult(task, body) {
const urls = extractRunwayOutputUrls(task);
if (urls.length === 0) {
throw new Error(
`Runway task completed without output URLs: ${JSON.stringify(task).slice(0, 400)}`
);
}
if (body.response_format === "url") return urls.map((url) => ({ url, format: "mp4" }));
const videos = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Runway output fetch failed (${response.status})`);
const arrayBuffer = await response.arrayBuffer();
videos.push({ b64_json: Buffer.from(arrayBuffer).toString("base64"), format: "mp4" });
}
return videos;
}