fix: letter case issue (#2721)

* fix: letter case

* fix: name
This commit is contained in:
Daniel Salazar
2026-03-24 22:46:19 -07:00
committed by GitHub
parent 0a3ac7b035
commit 68985e9f47
2 changed files with 0 additions and 305 deletions
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<IVideoModel[]> {
return OPENAI_VIDEO_MODELS;
}
async generate (params: IGenerateVideoParams): Promise<unknown> {
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<IVideoModel | undefined> {
const allModels = await this.models();
return allModels.find(m => m.id.toLowerCase() === requestedModel?.toLowerCase());
}
async #pollUntilComplete (initialJob: OpenAI.Video): Promise<OpenAI.Video> {
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<void> {
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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',
},
];