307 lines
8.8 KiB
TypeScript
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()
|
|
|
|
|