Files
AI-Video/frontend/src/services/editor-api.ts
empty 79a6c2ef3e feat: Add inpainting (局部重绘) feature for timeline editor
- Add canvas-based mask drawing tools (brush, eraser, rect, lasso)
- Add undo/redo history support for mask editing
- Integrate inpainting UI into preview player
- Add backend API endpoint for inpainting requests
- Add MediaService.inpaint method with ComfyUI workflow support
- Add Flux inpainting workflows for selfhost and RunningHub

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 23:44:51 +08:00

235 lines
6.6 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()
}
/**
* 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 singleton instance
export const editorApi = new EditorApiClient()