feat: Add comprehensive timeline editor with frame editing and regeneration capabilities
This commit is contained in:
198
frontend/src/services/editor-api.ts
Normal file
198
frontend/src/services/editor-api.ts
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user