feat: Add comprehensive timeline editor with frame editing and regeneration capabilities

This commit is contained in:
empty
2026-01-05 14:48:43 +08:00
parent 7d78dcd078
commit ca018a9b1f
68 changed files with 14904 additions and 57 deletions

View File

@@ -0,0 +1,198 @@
/**
* 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
}
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()
}
}
// Export singleton instance
export const editorApi = new EditorApiClient()