| |
| |
| |
| |
|
|
| import { apiClient } from './client' |
|
|
| |
|
|
| export interface SoraGeneration { |
| id: number |
| user_id: number |
| model: string |
| prompt: string |
| media_type: string |
| status: string |
| storage_type: string |
| media_url: string |
| media_urls: string[] |
| s3_object_keys: string[] |
| file_size_bytes: number |
| error_message: string |
| created_at: string |
| completed_at?: string |
| } |
|
|
| export interface GenerateRequest { |
| model: string |
| prompt: string |
| video_count?: number |
| media_type?: string |
| image_input?: string |
| api_key_id?: number |
| } |
|
|
| export interface GenerateResponse { |
| generation_id: number |
| status: string |
| } |
|
|
| export interface GenerationListResponse { |
| data: SoraGeneration[] |
| total: number |
| page: number |
| } |
|
|
| export interface QuotaInfo { |
| quota_bytes: number |
| used_bytes: number |
| available_bytes: number |
| quota_source: string |
| source?: string |
| } |
|
|
| export interface StorageStatus { |
| s3_enabled: boolean |
| s3_healthy: boolean |
| local_enabled: boolean |
| } |
|
|
| |
| export interface SoraModel { |
| id: string |
| name: string |
| type: string |
| orientation?: string |
| duration?: number |
| } |
|
|
| |
| export interface SoraModelFamily { |
| id: string |
| name: string |
| type: string |
| orientations: string[] |
| durations?: number[] |
| } |
|
|
| type LooseRecord = Record<string, unknown> |
|
|
| function asRecord(value: unknown): LooseRecord | null { |
| return value !== null && typeof value === 'object' ? value as LooseRecord : null |
| } |
|
|
| function asArray<T = unknown>(value: unknown): T[] { |
| return Array.isArray(value) ? value as T[] : [] |
| } |
|
|
| function asPositiveInt(value: unknown): number | null { |
| const n = Number(value) |
| if (!Number.isFinite(n) || n <= 0) return null |
| return Math.round(n) |
| } |
|
|
| function dedupeStrings(values: string[]): string[] { |
| return Array.from(new Set(values)) |
| } |
|
|
| function extractOrientationFromModelID(modelID: string): string | null { |
| const m = modelID.match(/-(landscape|portrait|square)(?:-\d+s)?$/i) |
| return m ? m[1].toLowerCase() : null |
| } |
|
|
| function extractDurationFromModelID(modelID: string): number | null { |
| const m = modelID.match(/-(\d+)s$/i) |
| return m ? asPositiveInt(m[1]) : null |
| } |
|
|
| function normalizeLegacyFamilies(candidates: unknown[]): SoraModelFamily[] { |
| const familyMap = new Map<string, SoraModelFamily>() |
|
|
| for (const item of candidates) { |
| const model = asRecord(item) |
| if (!model || typeof model.id !== 'string' || model.id.trim() === '') continue |
|
|
| const rawID = model.id.trim() |
| const type = model.type === 'image' ? 'image' : 'video' |
| const name = typeof model.name === 'string' && model.name.trim() ? model.name.trim() : rawID |
| const baseID = rawID.replace(/-(landscape|portrait|square)(?:-\d+s)?$/i, '') |
| const orientation = |
| typeof model.orientation === 'string' && model.orientation |
| ? model.orientation.toLowerCase() |
| : extractOrientationFromModelID(rawID) |
| const duration = asPositiveInt(model.duration) ?? extractDurationFromModelID(rawID) |
| const familyKey = baseID || rawID |
|
|
| const family = familyMap.get(familyKey) ?? { |
| id: familyKey, |
| name, |
| type, |
| orientations: [], |
| durations: [] |
| } |
|
|
| if (orientation) { |
| family.orientations.push(orientation) |
| } |
| if (type === 'video' && duration) { |
| family.durations = family.durations || [] |
| family.durations.push(duration) |
| } |
|
|
| familyMap.set(familyKey, family) |
| } |
|
|
| return Array.from(familyMap.values()) |
| .map((family) => ({ |
| ...family, |
| orientations: |
| family.orientations.length > 0 |
| ? dedupeStrings(family.orientations) |
| : (family.type === 'image' ? ['square'] : ['landscape']), |
| durations: |
| family.type === 'video' |
| ? Array.from(new Set((family.durations || []).filter((d): d is number => Number.isFinite(d)))).sort((a, b) => a - b) |
| : [] |
| })) |
| .filter((family) => family.id !== '') |
| } |
|
|
| function normalizeModelFamilyRecord(item: unknown): SoraModelFamily | null { |
| const model = asRecord(item) |
| if (!model || typeof model.id !== 'string' || model.id.trim() === '') return null |
| |
| if (!Array.isArray(model.orientations) && !Array.isArray(model.durations)) return null |
|
|
| const orientations = asArray<string>(model.orientations).filter((o): o is string => typeof o === 'string' && o.length > 0) |
| const durations = asArray<unknown>(model.durations) |
| .map(asPositiveInt) |
| .filter((d): d is number => d !== null) |
|
|
| return { |
| id: model.id.trim(), |
| name: typeof model.name === 'string' && model.name.trim() ? model.name.trim() : model.id.trim(), |
| type: model.type === 'image' ? 'image' : 'video', |
| orientations: dedupeStrings(orientations), |
| durations: Array.from(new Set(durations)).sort((a, b) => a - b) |
| } |
| } |
|
|
| function extractCandidateArray(payload: unknown): unknown[] { |
| if (Array.isArray(payload)) return payload |
| const record = asRecord(payload) |
| if (!record) return [] |
|
|
| const keys: Array<keyof LooseRecord> = ['data', 'items', 'models', 'families'] |
| for (const key of keys) { |
| if (Array.isArray(record[key])) { |
| return record[key] as unknown[] |
| } |
| } |
| return [] |
| } |
|
|
| export function normalizeModelFamiliesResponse(payload: unknown): SoraModelFamily[] { |
| const candidates = extractCandidateArray(payload) |
| if (candidates.length === 0) return [] |
|
|
| const normalized = candidates |
| .map(normalizeModelFamilyRecord) |
| .filter((item): item is SoraModelFamily => item !== null) |
|
|
| if (normalized.length > 0) return normalized |
| return normalizeLegacyFamilies(candidates) |
| } |
|
|
| export function normalizeGenerationListResponse(payload: unknown): GenerationListResponse { |
| const record = asRecord(payload) |
| if (!record) { |
| return { data: [], total: 0, page: 1 } |
| } |
|
|
| const data = Array.isArray(record.data) |
| ? (record.data as SoraGeneration[]) |
| : Array.isArray(record.items) |
| ? (record.items as SoraGeneration[]) |
| : [] |
|
|
| const total = Number(record.total) |
| const page = Number(record.page) |
|
|
| return { |
| data, |
| total: Number.isFinite(total) ? total : data.length, |
| page: Number.isFinite(page) && page > 0 ? page : 1 |
| } |
| } |
|
|
| |
|
|
| |
| export async function generate(req: GenerateRequest): Promise<GenerateResponse> { |
| const { data } = await apiClient.post<GenerateResponse>('/sora/generate', req) |
| return data |
| } |
|
|
| |
| export async function listGenerations(params?: { |
| page?: number |
| page_size?: number |
| status?: string |
| storage_type?: string |
| media_type?: string |
| }): Promise<GenerationListResponse> { |
| const { data } = await apiClient.get<unknown>('/sora/generations', { params }) |
| return normalizeGenerationListResponse(data) |
| } |
|
|
| |
| export async function getGeneration(id: number): Promise<SoraGeneration> { |
| const { data } = await apiClient.get<SoraGeneration>(`/sora/generations/${id}`) |
| return data |
| } |
|
|
| |
| export async function deleteGeneration(id: number): Promise<{ message: string }> { |
| const { data } = await apiClient.delete<{ message: string }>(`/sora/generations/${id}`) |
| return data |
| } |
|
|
| |
| export async function cancelGeneration(id: number): Promise<{ message: string }> { |
| const { data } = await apiClient.post<{ message: string }>(`/sora/generations/${id}/cancel`) |
| return data |
| } |
|
|
| |
| export async function saveToStorage( |
| id: number |
| ): Promise<{ message: string; object_key: string; object_keys?: string[] }> { |
| const { data } = await apiClient.post<{ message: string; object_key: string; object_keys?: string[] }>( |
| `/sora/generations/${id}/save` |
| ) |
| return data |
| } |
|
|
| |
| export async function getQuota(): Promise<QuotaInfo> { |
| const { data } = await apiClient.get<QuotaInfo>('/sora/quota') |
| return data |
| } |
|
|
| |
| export async function getModels(): Promise<SoraModelFamily[]> { |
| const { data } = await apiClient.get<unknown>('/sora/models') |
| return normalizeModelFamiliesResponse(data) |
| } |
|
|
| |
| export async function getStorageStatus(): Promise<StorageStatus> { |
| const { data } = await apiClient.get<StorageStatus>('/sora/storage-status') |
| return data |
| } |
|
|
| const soraAPI = { |
| generate, |
| listGenerations, |
| getGeneration, |
| deleteGeneration, |
| cancelGeneration, |
| saveToStorage, |
| getQuota, |
| getModels, |
| getStorageStatus |
| } |
|
|
| export default soraAPI |
|
|