Files
AI-Video/frontend/src/services/editor-api.ts

307 lines
8.8 KiB
TypeScript

/**
* Editor API client for connecting to FastAPI backend
*/
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'
export interface StoryboardFrame {
id: string
index: number
order: number
narration: string
image_prompt?: string
image_path?: string
audio_path?: string
video_segment_path?: string
duration: number
}
export interface Storyboard {
id: string
title: string
frames: StoryboardFrame[]
total_duration: number
final_video_path?: string
created_at?: string
}
export interface PreviewResponse {
preview_path: string
duration: number
frames_count: number
}
export interface InpaintResponse {
image_path: string
success: boolean
}
class EditorApiClient {
private baseUrl: string
constructor(baseUrl: string = API_BASE) {
this.baseUrl = baseUrl
}
/**
* Fetch storyboard by ID
*/
async getStoryboard(storyboardId: string): Promise<Storyboard> {
const response = await fetch(`${this.baseUrl}/editor/storyboard/${storyboardId}`)
if (!response.ok) {
throw new Error(`Failed to fetch storyboard: ${response.statusText}`)
}
return response.json()
}
/**
* Reorder frames in storyboard
*/
async reorderFrames(storyboardId: string, order: string[]): Promise<Storyboard> {
const response = await fetch(`${this.baseUrl}/editor/storyboard/${storyboardId}/reorder`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order }),
})
if (!response.ok) {
throw new Error(`Failed to reorder frames: ${response.statusText}`)
}
return response.json()
}
/**
* Update frame duration
*/
async updateFrameDuration(
storyboardId: string,
frameId: string,
duration: number
): Promise<StoryboardFrame> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}/duration`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ duration }),
}
)
if (!response.ok) {
throw new Error(`Failed to update duration: ${response.statusText}`)
}
return response.json()
}
/**
* Generate preview video
*/
async generatePreview(
storyboardId: string,
startFrame?: number,
endFrame?: number
): Promise<PreviewResponse> {
const response = await fetch(`${this.baseUrl}/editor/storyboard/${storyboardId}/preview`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
start_frame: startFrame ?? 0,
end_frame: endFrame,
}),
})
if (!response.ok) {
throw new Error(`Failed to generate preview: ${response.statusText}`)
}
return response.json()
}
/**
* Update frame content (narration and/or image prompt)
*/
async updateFrame(
storyboardId: string,
frameId: string,
data: { narration?: string; image_prompt?: string }
): Promise<{ id: string; narration: string; image_prompt?: string; updated: boolean }> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to update frame: ${response.statusText}`)
}
return response.json()
}
/**
* Regenerate image for a frame
*/
async regenerateImage(
storyboardId: string,
frameId: string,
imagePrompt?: string
): Promise<{ image_path: string; success: boolean }> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}/regenerate-image`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_prompt: imagePrompt }),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to regenerate image: ${response.statusText}`)
}
return response.json()
}
/**
* Regenerate audio for a frame
*/
async regenerateAudio(
storyboardId: string,
frameId: string,
narration?: string,
voice?: string
): Promise<{ audio_path: string; duration: number; success: boolean }> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}/regenerate-audio`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ narration, voice }),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to regenerate audio: ${response.statusText}`)
}
return response.json()
}
/**
* Align image prompt with narration - regenerate prompt based on narration
*/
async alignPrompt(
storyboardId: string,
frameId: string,
narration?: string
): Promise<{ image_prompt: string; success: boolean }> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}/align-prompt`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ narration }),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to align prompt: ${response.statusText}`)
}
return response.json()
}
/**
* Inpaint (局部重绘) image for a frame
*/
async inpaintImage(
storyboardId: string,
frameId: string,
mask: string,
prompt?: string,
denoiseStrength?: number
): Promise<InpaintResponse> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/frames/${frameId}/inpaint`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mask,
prompt,
denoise_strength: denoiseStrength ?? 0.8,
}),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to inpaint image: ${response.statusText}`)
}
return response.json()
}
/**
* Export edited video
*/
async exportVideo(
storyboardId: string,
bgmPath?: string,
bgmVolume?: number
): Promise<{ task_id: string; status: string }> {
const response = await fetch(
`${this.baseUrl}/editor/storyboard/${storyboardId}/export`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bgm_path: bgmPath, bgm_volume: bgmVolume }),
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to start export: ${response.statusText}`)
}
return response.json()
}
/**
* Get export task status
*/
async getExportStatus(taskId: string): Promise<{
task_id: string
status: string
progress: number
video_path?: string
download_url?: string
error?: string
}> {
const response = await fetch(`${this.baseUrl}/editor/export/${taskId}/status`)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }))
throw new Error(error.detail || `Failed to get export status: ${response.statusText}`)
}
return response.json()
}
}
// Export singleton instance
export const editorApi = new EditorApiClient()