From 68985e9f472c2cef47e4f3218e0f82140eda4abf Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Tue, 24 Mar 2026 22:46:19 -0700 Subject: [PATCH] fix: letter case issue (#2721) * fix: letter case * fix: name --- .../OpenAIVideoGenerationProvider.ts | 246 ------------------ .../OpenAiVideoGenerationProvider/models.ts | 59 ----- 2 files changed, 305 deletions(-) delete mode 100644 src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/OpenAIVideoGenerationProvider.ts delete mode 100644 src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/models.ts diff --git a/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/OpenAIVideoGenerationProvider.ts b/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/OpenAIVideoGenerationProvider.ts deleted file mode 100644 index 94d0fe64e..000000000 --- a/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/OpenAIVideoGenerationProvider.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import OpenAI from 'openai'; -import APIError from '../../../../../api/APIError.js'; -import { Context } from '../../../../../util/context.js'; -import { MeteringService } from '../../../../MeteringService/MeteringService.js'; -import { IGenerateVideoParams, IVideoModel, IVideoProvider } from '../types.js'; -import { TypedValue } from '../../../../drivers/meta/Runtime.js'; -import { Readable } from 'stream'; -import { OPENAI_VIDEO_MODELS, OPENAI_VIDEO_ALLOWED_SECONDS } from './models.js'; - -const DEFAULT_TEST_VIDEO_URL = 'https://assets.puter.site/txt2vid.mp4'; -const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; -const POLL_INTERVAL_MS = 5_000; -const DEFAULT_DURATION_SECONDS = 4; - -export class OpenAIVideoGenerationProvider implements IVideoProvider { - #openai: OpenAI; - #meteringService: MeteringService; - - constructor (config: { apiKey: string }, meteringService: MeteringService) { - if ( ! config.apiKey ) { - throw new Error('OpenAI video generation requires an API key'); - } - this.#openai = new OpenAI({ apiKey: config.apiKey }); - this.#meteringService = meteringService; - } - - getDefaultModel (): string { - return OPENAI_VIDEO_MODELS[0].id; - } - - async models (): Promise { - return OPENAI_VIDEO_MODELS; - } - - async generate (params: IGenerateVideoParams): Promise { - const { - prompt, - model: requestedModel, - duration, - seconds, - size, - resolution, - input_reference: inputReference, - test_mode: testMode, - } = params ?? {}; - - if ( typeof prompt !== 'string' || !prompt.trim() ) { - throw APIError.create('field_invalid', null, { - key: 'prompt', - expected: 'a non-empty string', - got: prompt, - }); - } - - const selectedModel = await this.#selectModel(requestedModel); - - if ( ! selectedModel ) { - throw new Error(`Unknown video model: ${requestedModel}`); - } - - if ( testMode ) { - return new TypedValue({ - $: 'string:url:web', - content_type: 'video', - }, DEFAULT_TEST_VIDEO_URL); - } - - const defaultSize = selectedModel.dimensions?.[0] ?? '720x1280'; - const normalizedSize = this.#normalizeSize(size ?? resolution, selectedModel) ?? defaultSize; - const normalizedSeconds = this.#normalizeSeconds(seconds ?? duration) ?? String(DEFAULT_DURATION_SECONDS); - - const sizeTier = this.#determineSizeTier(selectedModel, normalizedSize); - const costPerSecondCents = this.#getCostPerSecond(selectedModel, sizeTier); - - if ( ! costPerSecondCents ) { - throw new Error(`No pricing configured for model ${selectedModel.id} at size ${normalizedSize}`); - } - - const estimatedUnits = this.#parseSeconds(normalizedSeconds) ?? DEFAULT_DURATION_SECONDS; - const actor = Context.get('actor'); - const costInMicroCents = costPerSecondCents * 1_000_000; - const usageAllowed = await this.#meteringService.hasEnoughCredits(actor, costInMicroCents * estimatedUnits); - if ( ! usageAllowed ) { - throw APIError.create('insufficient_funds'); - } - - const createParams: OpenAI.VideoCreateParams = { - prompt, - model: selectedModel.id, - seconds: normalizedSeconds as OpenAI.VideoSeconds, - size: normalizedSize as OpenAI.VideoSize, - }; - - if ( inputReference ) { - createParams.input_reference = inputReference as OpenAI.VideoCreateParams['input_reference']; - } - - const createResponse = await this.#openai.videos.create(createParams); - const finalJob = await this.#pollUntilComplete(createResponse); - - if ( finalJob.status === 'failed' ) { - const errorMessage = finalJob.error?.message ?? 'Video generation failed'; - throw new Error(errorMessage); - } - - const finalResolution = this.#normalizeSize(finalJob.size, selectedModel) ?? normalizedSize; - const finalTier = this.#determineSizeTier(selectedModel, finalResolution); - const finalCostPerSecondCents = this.#getCostPerSecond(selectedModel, finalTier); - - if ( ! finalCostPerSecondCents ) { - throw new Error(`No pricing configured for model ${selectedModel.id} at size ${finalResolution}`); - } - - const finalCostInMicroCents = finalCostPerSecondCents * 1_000_000; - const actualSeconds = this.#parseSeconds(finalJob.seconds) ?? estimatedUnits; - - const downloadResponse = await this.#openai.videos.downloadContent(finalJob.id); - const contentType = downloadResponse.headers.get('content-type') ?? 'video/mp4'; - - let stream: any = downloadResponse.body; - if ( stream && typeof stream.getReader === 'function' ) { - stream = Readable.fromWeb(stream as any); - } - - if ( ! stream ) { - const arrayBuffer = await downloadResponse.arrayBuffer(); - stream = Readable.from(Buffer.from(arrayBuffer)); - } - - const finalUsageKey = this.#getUsageKey(selectedModel, finalTier); - await this.#meteringService.incrementUsage(actor, finalUsageKey, actualSeconds, finalCostInMicroCents * actualSeconds); - - return new TypedValue({ - $: 'stream', - content_type: contentType, - }, stream); - } - - async #selectModel (requestedModel?: string): Promise { - const allModels = await this.models(); - return allModels.find(m => m.id.toLowerCase() === requestedModel?.toLowerCase()); - } - - async #pollUntilComplete (initialJob: OpenAI.Video): Promise { - let job = initialJob; - const start = Date.now(); - - while ( job.status === 'queued' || job.status === 'in_progress' ) { - if ( Date.now() - start > DEFAULT_TIMEOUT_MS ) { - throw new Error('Timed out waiting for Sora video generation to complete'); - } - - await this.#delay(POLL_INTERVAL_MS); - job = await this.#openai.videos.retrieve(job.id); - } - - return job; - } - - async #delay (ms: number): Promise { - return await new Promise(resolve => setTimeout(resolve, ms)); - } - - #normalizeSize (candidate: unknown, model: IVideoModel): string | undefined { - if ( ! candidate ) return undefined; - const normalized = this.#normalizeResolution(candidate); - if ( normalized && model.dimensions?.includes(normalized) ) { - return normalized; - } - return undefined; - } - - #normalizeSeconds (value: unknown): string | undefined { - if ( value === null || value === undefined ) { - return undefined; - } - const parsed = typeof value === 'number' ? String(Math.round(value)) : typeof value === 'string' ? value.trim() : undefined; - if ( parsed && OPENAI_VIDEO_ALLOWED_SECONDS.includes(Number(parsed) as typeof OPENAI_VIDEO_ALLOWED_SECONDS[number]) ) { - return parsed; - } - return undefined; - } - - #determineSizeTier (model: IVideoModel, size: string): string { - if ( model.id === 'sora-2-pro' ) { - if ( size === '1080x1920' || size === '1920x1080' ) return 'xxl'; - if ( size === '1024x1792' || size === '1792x1024' ) return 'xl'; - } - return 'default'; - } - - #getCostPerSecond (model: IVideoModel, tier: string): number | undefined { - const key = tier === 'default' ? 'per-second' : `per-second-${tier}`; - return model.costs?.[key]; - } - - #getUsageKey (model: IVideoModel, tier: string): string { - return `openai:${model.id}:${tier}`; - } - - #normalizeResolution (value: unknown): string | undefined { - if ( ! value ) return undefined; - if ( typeof value === 'string' ) { - const match = value.match(/(\d+)\s*x\s*(\d+)/i); - if ( match ) { - const w = Number.parseInt(match[1], 10); - const h = Number.parseInt(match[2], 10); - if ( Number.isFinite(w) && Number.isFinite(h) ) { - return `${w}x${h}`; - } - } - } - return undefined; - } - - #parseSeconds (value: unknown): number | undefined { - if ( value === null || value === undefined ) return undefined; - if ( typeof value === 'number' && Number.isFinite(value) ) { - return Math.round(value); - } - if ( typeof value === 'string' ) { - const numeric = Number.parseInt(value, 10); - return Number.isFinite(numeric) ? numeric : undefined; - } - return undefined; - } -} diff --git a/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/models.ts b/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/models.ts deleted file mode 100644 index 061cd341d..000000000 --- a/src/backend/src/services/ai/video/providers/OpenAiVideoGenerationProvider/models.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { IVideoModel } from '../types.js'; - -export const OPENAI_VIDEO_ALLOWED_SECONDS = [4, 8, 12] as const; - -export const OPENAI_VIDEO_MODELS: IVideoModel[] = [ - { - id: 'sora-2', - puterId: 'openai:openai/sora-2', - aliases: ['openai/sora-2'], - name: 'Sora 2', - costs_currency: 'usd-cents', - costs: { - 'per-second': 10, - 'default-duration-per-video': 40, - }, - output_cost_key: 'default-duration-per-video', - durationSeconds: OPENAI_VIDEO_ALLOWED_SECONDS.slice(), - dimensions: ['720x1280', '1280x720'], - defaultUsageKey: 'openai:sora-2:default', - }, - { - id: 'sora-2-pro', - puterId: 'openai:openai/sora-2-pro', - aliases: ['openai/sora-2-pro'], - name: 'Sora 2 Pro', - costs_currency: 'usd-cents', - costs: { - 'per-second': 30, - 'default-duration-per-video': 120, - 'per-second-xl': 50, - 'default-duration-per-video-xl': 200, - 'per-second-xxl': 70, - 'default-duration-per-video-xxl': 280, - }, - output_cost_key: 'default-duration-per-video', - durationSeconds: OPENAI_VIDEO_ALLOWED_SECONDS.slice(), - dimensions: ['720x1280', '1280x720', '1024x1792', '1792x1024', '1080x1920', '1920x1080'], - defaultUsageKey: 'openai:sora-2-pro:default', - }, -];