Skip to content

Commit 517bd9b

Browse files
committed
feat: add effect preview video generation system
- Created generateEffectPreview utility for rendering single effect previews - Added generateAllEffectPreviews for batch preview generation - Implemented useEffectPreviewGenerator hook for UI integration - Uses existing prerender_segment system from video-compiler - Generates 3-5 second preview videos with effects applied - Supports progress tracking and error handling - Can update effect metadata with generated preview paths - Enables pre-rendered previews instead of real-time WebGL rendering
1 parent 74ea099 commit 517bd9b

File tree

3 files changed

+319
-0
lines changed

3 files changed

+319
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Хук для генерации превью видео для эффектов
3+
*/
4+
5+
import { useCallback, useState } from "react"
6+
import { useTranslation } from "react-i18next"
7+
import { useNotifications } from "@/domains/system-integration"
8+
import type { BaseEffect } from "../types"
9+
import { generateAllEffectPreviews, type EffectPreviewConfig } from "../utils/generate-effect-previews"
10+
11+
export interface PreviewGenerationState {
12+
isGenerating: boolean
13+
progress: number
14+
total: number
15+
currentEffectId?: string
16+
completed: number
17+
failed: number
18+
error?: string
19+
}
20+
21+
export function useEffectPreviewGenerator() {
22+
const { t } = useTranslation()
23+
const { showSuccess, showError, showInfo } = useNotifications()
24+
const [state, setState] = useState<PreviewGenerationState>({
25+
isGenerating: false,
26+
progress: 0,
27+
total: 0,
28+
completed: 0,
29+
failed: 0,
30+
})
31+
32+
/**
33+
* Сгенерировать превью для всех эффектов
34+
*/
35+
const generatePreviews = useCallback(
36+
async (effects: BaseEffect[], config: Partial<EffectPreviewConfig> = {}) => {
37+
const fullConfig: EffectPreviewConfig = {
38+
sourceVideoPath: config.sourceVideoPath || "/t1.mp4",
39+
duration: config.duration || 3, // 3 секунды по умолчанию
40+
quality: config.quality || 75,
41+
outputDir: config.outputDir || "preview-videos/effects",
42+
}
43+
44+
setState({
45+
isGenerating: true,
46+
progress: 0,
47+
total: effects.length,
48+
completed: 0,
49+
failed: 0,
50+
})
51+
52+
showInfo(
53+
t("effects.preview.generating"),
54+
t("effects.preview.generatingMessage", { count: effects.length }),
55+
)
56+
57+
try {
58+
const results = await generateAllEffectPreviews(
59+
effects,
60+
fullConfig,
61+
(current, total, effectId) => {
62+
setState((prev) => ({
63+
...prev,
64+
progress: (current / total) * 100,
65+
currentEffectId: effectId,
66+
}))
67+
},
68+
)
69+
70+
const completed = results.size
71+
const failed = effects.length - completed
72+
73+
setState({
74+
isGenerating: false,
75+
progress: 100,
76+
total: effects.length,
77+
completed,
78+
failed,
79+
})
80+
81+
if (failed > 0) {
82+
showSuccess(
83+
t("effects.preview.completedWithErrors"),
84+
t("effects.preview.completedMessage", { completed, failed }),
85+
)
86+
} else {
87+
showSuccess(
88+
t("effects.preview.completed"),
89+
t("effects.preview.allCompletedMessage", { count: completed }),
90+
)
91+
}
92+
93+
return results
94+
} catch (error) {
95+
const errorMessage = error instanceof Error ? error.message : t("common.unknownError")
96+
97+
setState({
98+
isGenerating: false,
99+
progress: 0,
100+
total: effects.length,
101+
completed: 0,
102+
failed: effects.length,
103+
error: errorMessage,
104+
})
105+
106+
showError(t("effects.preview.error"), errorMessage)
107+
return new Map<string, string>()
108+
}
109+
},
110+
[t, showSuccess, showError, showInfo],
111+
)
112+
113+
/**
114+
* Отменить генерацию
115+
*/
116+
const cancelGeneration = useCallback(() => {
117+
setState({
118+
isGenerating: false,
119+
progress: 0,
120+
total: 0,
121+
completed: 0,
122+
failed: 0,
123+
})
124+
}, [])
125+
126+
return {
127+
...state,
128+
generatePreviews,
129+
cancelGeneration,
130+
}
131+
}

src/features/effects/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export {
7676
export { useUnifiedEffects } from "./hooks/use-unified-effects"
7777
export type { UseUserPresetsOptions, UseUserPresetsReturn } from "./hooks/use-user-presets"
7878
export { useUserPresets } from "./hooks/use-user-presets"
79+
export { useEffectPreviewGenerator } from "./hooks/use-effect-preview-generator"
80+
export type { PreviewGenerationState } from "./hooks/use-effect-preview-generator"
7981

8082
// ============================================================================
8183
// БИБЛИОТЕКИ ЭФФЕКТОВ
@@ -115,6 +117,14 @@ export {
115117
// УТИЛИТЫ
116118
// ============================================================================
117119

120+
// Генерация превью
121+
export {
122+
generateAllEffectPreviews,
123+
generateEffectPreview,
124+
updateEffectsWithPreviews,
125+
} from "./utils/generate-effect-previews"
126+
export type { EffectPreviewConfig } from "./utils/generate-effect-previews"
127+
118128
/**
119129
* Создает новый менеджер эффектов с предустановленными эффектами
120130
*/
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* Утилита для генерации превью видео для эффектов
3+
*
4+
* Использует систему пререндеринга для создания коротких демо-видео
5+
* с применённым эффектом для каждого эффекта в библиотеке
6+
*/
7+
8+
import type { BaseEffect } from "../types"
9+
import { prerenderSegment } from "@/domains/video-editing/services/compiler"
10+
import { createLogger } from "@/lib/tauri-logger"
11+
12+
const logger = createLogger("EffectPreviewGenerator")
13+
14+
export interface EffectPreviewConfig {
15+
/** Исходное видео для применения эффекта */
16+
sourceVideoPath: string
17+
/** Длительность превью в секундах */
18+
duration: number
19+
/** Качество превью (0-100) */
20+
quality: number
21+
/** Директория для сохранения превью */
22+
outputDir: string
23+
}
24+
25+
/**
26+
* Генерирует превью видео для одного эффекта
27+
*/
28+
export async function generateEffectPreview(
29+
effect: BaseEffect,
30+
config: EffectPreviewConfig,
31+
): Promise<string | null> {
32+
try {
33+
void logger.info("Generating preview for effect", { effectId: effect.id })
34+
35+
// Создаём минимальный проект с одним клипом и эффектом
36+
const projectSchema = {
37+
version: "1.0.0",
38+
settings: {
39+
resolution: { width: 1920, height: 1080 },
40+
frameRate: 30,
41+
audioSampleRate: 48000,
42+
audioChannels: 2,
43+
},
44+
tracks: [
45+
{
46+
id: "preview-track",
47+
type: "video" as const,
48+
clips: [
49+
{
50+
id: "preview-clip",
51+
mediaId: "preview-media",
52+
mediaFile: {
53+
id: "preview-media",
54+
path: config.sourceVideoPath,
55+
name: "preview.mp4",
56+
type: "video" as const,
57+
duration: config.duration,
58+
},
59+
startTime: 0,
60+
duration: config.duration,
61+
mediaStartTime: 0,
62+
mediaEndTime: config.duration,
63+
effects: [
64+
{
65+
id: `${effect.id}-preview`,
66+
effectId: effect.id,
67+
enabled: true,
68+
customParams: effect.parameters?.reduce(
69+
(acc, param) => {
70+
acc[param.id] = param.defaultValue
71+
return acc
72+
},
73+
{} as Record<string, any>,
74+
) || {},
75+
startTime: 0,
76+
endTime: config.duration,
77+
order: 0,
78+
},
79+
],
80+
filters: [],
81+
transitions: [],
82+
volume: 0, // Без звука для превью
83+
speed: 1.0,
84+
isReversed: false,
85+
opacity: 1.0,
86+
isSelected: false,
87+
isLocked: false,
88+
offset: 0,
89+
trackId: "preview-track",
90+
name: "Preview Clip",
91+
createdAt: new Date(),
92+
updatedAt: new Date(),
93+
},
94+
],
95+
},
96+
],
97+
metadata: {
98+
createdAt: new Date().toISOString(),
99+
modifiedAt: new Date().toISOString(),
100+
},
101+
}
102+
103+
const outputPath = `${config.outputDir}/effect_${effect.id}.mp4`
104+
105+
// Рендерим превью
106+
const result = await prerenderSegment({
107+
projectSchema: projectSchema as any,
108+
startTime: 0,
109+
endTime: config.duration,
110+
applyEffects: true,
111+
quality: config.quality,
112+
})
113+
114+
if (result?.filePath) {
115+
void logger.info("Preview generated successfully", {
116+
effectId: effect.id,
117+
outputPath: result.filePath,
118+
renderTime: result.renderTimeMs,
119+
})
120+
return result.filePath
121+
}
122+
123+
return null
124+
} catch (error) {
125+
void logger.error("Failed to generate preview", { effectId: effect.id, error })
126+
return null
127+
}
128+
}
129+
130+
/**
131+
* Генерирует превью для всех эффектов
132+
*/
133+
export async function generateAllEffectPreviews(
134+
effects: BaseEffect[],
135+
config: EffectPreviewConfig,
136+
onProgress?: (current: number, total: number, effectId: string) => void,
137+
): Promise<Map<string, string>> {
138+
const results = new Map<string, string>()
139+
140+
void logger.info("Starting batch preview generation", { total: effects.length })
141+
142+
for (let i = 0; i < effects.length; i++) {
143+
const effect = effects[i]
144+
onProgress?.(i + 1, effects.length, effect.id)
145+
146+
const previewPath = await generateEffectPreview(effect, config)
147+
if (previewPath) {
148+
results.set(effect.id, previewPath)
149+
}
150+
}
151+
152+
void logger.info("Batch preview generation completed", {
153+
total: effects.length,
154+
successful: results.size,
155+
failed: effects.length - results.size,
156+
})
157+
158+
return results
159+
}
160+
161+
/**
162+
* Обновляет эффекты с путями к превью
163+
*/
164+
export function updateEffectsWithPreviews(
165+
effects: BaseEffect[],
166+
previewPaths: Map<string, string>,
167+
): BaseEffect[] {
168+
return effects.map((effect) => {
169+
const previewPath = previewPaths.get(effect.id)
170+
if (previewPath) {
171+
return {
172+
...effect,
173+
preview: previewPath,
174+
}
175+
}
176+
return effect
177+
})
178+
}

0 commit comments

Comments
 (0)