Skip to content

Commit d36d2b9

Browse files
chatman-mediaclaude
andcommitted
feat(analysis): улучшения UI анализа и интеграция систем
- Добавлена интеграция useAIDirectorAnalysisV2 в AnalysisTasksDropdown для отображения текущих активных анализов в реальном времени - Исправлена кнопка "Начать анализ" - добавлен loading state и убран блокирующий await для отзывчивости UI - Исправлена логика кнопки "Очистить историю" - теперь учитывает failed и cancelled задачи, а не только completed - Добавлено сохранение результатов анализа в storage для истории - Улучшена панель настройки анализа с media-aware presets - Добавлена поддержка VLM моделей в настройках анализа - Рефакторинг browser компонентов и player provider - Обновлены тесты для новой функциональности 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 57f2fea commit d36d2b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2461
-794
lines changed

src/core/ports/ai.port.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export interface AIDirectorConfig {
198198
enable_mcp_agents: boolean
199199

200200
// AI Provider integration
201-
ai_provider: "Ollama" | "OpenAI" | "Anthropic" | null // Option<AIProvider>
201+
ai_provider: "ollama" | "openai" | "anthropic" | null // Option<AIProvider> - lowercase to match Rust serde
202202
ai_model: string | null // Option<String>
203203
ai_api_key: string | null // Option<String>
204204
enable_ai_enhanced_analysis: boolean
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/**
2+
* Analysis Results Tools
3+
* Инструменты для получения результатов AI анализа видео
4+
*/
5+
6+
import { analysisStorageService } from "@/domains/ai-services/services/analysis-storage-service"
7+
8+
import { BaseAITool } from "../../../base"
9+
import type { AIToolExecutionOptions, AIToolMetadata, AIToolResult, IAITool } from "../../../types"
10+
11+
// ============================================================================
12+
// Types
13+
// ============================================================================
14+
15+
interface GetAnalysisResultsInput {
16+
/** Путь к видео файлу или "all" для всех */
17+
videoPath?: string
18+
/** Тип анализа: comprehensive, montage, unified или all */
19+
analysisType?: "comprehensive" | "montage" | "unified" | "all"
20+
/** Включить метаданные */
21+
includeMetadata?: boolean
22+
}
23+
24+
interface AnalysisResultsOutput {
25+
success: boolean
26+
videoPath?: string
27+
analysisType: string
28+
results: {
29+
comprehensive?: any
30+
montage?: any
31+
unified?: any
32+
}
33+
metadata?: any
34+
analyzedVideos?: string[]
35+
stats?: {
36+
comprehensiveCount: number
37+
montageCount: number
38+
unifiedCount: number
39+
}
40+
}
41+
42+
// ============================================================================
43+
// GET ANALYSIS RESULTS TOOL
44+
// ============================================================================
45+
46+
export class GetAnalysisResultsTool extends BaseAITool implements IAITool {
47+
metadata: AIToolMetadata = {
48+
name: "get-analysis-results",
49+
displayName: "Получить результаты анализа",
50+
description:
51+
"Получает результаты AI Director анализа для видео. Можно запросить данные для конкретного видео или список всех проанализированных видео.",
52+
domain: "analysis",
53+
category: "content-intelligence",
54+
tags: ["analysis", "ai-director", "video", "results", "data"],
55+
version: "1.0.0",
56+
author: "Timeline Studio",
57+
dependencies: [],
58+
inputSchema: {
59+
type: "object",
60+
properties: {
61+
videoPath: {
62+
type: "string",
63+
description:
64+
'Путь к видео файлу. Оставьте пустым или "all" для получения списка всех проанализированных видео',
65+
},
66+
analysisType: {
67+
type: "string",
68+
enum: ["comprehensive", "montage", "unified", "all"],
69+
description: "Тип анализа для получения. По умолчанию: all",
70+
},
71+
includeMetadata: {
72+
type: "boolean",
73+
description: "Включить метаданные анализа (даты, длительность и т.д.)",
74+
},
75+
},
76+
required: [],
77+
},
78+
outputSchema: {
79+
type: "object",
80+
properties: {
81+
success: { type: "boolean" },
82+
videoPath: { type: "string" },
83+
analysisType: { type: "string" },
84+
results: { type: "object" },
85+
metadata: { type: "object" },
86+
analyzedVideos: { type: "array", items: { type: "string" } },
87+
stats: { type: "object" },
88+
},
89+
},
90+
examples: [
91+
{
92+
description: "Получить все результаты анализа для видео",
93+
input: { videoPath: "/path/to/video.mp4", analysisType: "all" },
94+
expectedOutput: { success: true, results: {} },
95+
},
96+
{
97+
description: "Получить список всех проанализированных видео",
98+
input: { videoPath: "all" },
99+
expectedOutput: { success: true, analyzedVideos: [] },
100+
},
101+
],
102+
}
103+
104+
async execute(
105+
input: GetAnalysisResultsInput,
106+
options?: AIToolExecutionOptions,
107+
): Promise<AIToolResult<AnalysisResultsOutput>> {
108+
return this.executeWithErrorHandling(
109+
async (_context) => {
110+
const { videoPath, analysisType = "all", includeMetadata = true } = input
111+
112+
// Если запрос на все видео или не указан путь
113+
if (!videoPath || videoPath === "all") {
114+
const analyzedVideos = await analysisStorageService.getAnalyzedVideos()
115+
const stats = await analysisStorageService.getStorageStats()
116+
117+
return {
118+
success: true,
119+
analysisType: "list",
120+
results: {},
121+
analyzedVideos,
122+
stats,
123+
}
124+
}
125+
126+
// Получаем результаты для конкретного видео
127+
const results: AnalysisResultsOutput["results"] = {}
128+
let metadata: any
129+
130+
if (analysisType === "comprehensive" || analysisType === "all") {
131+
const comprehensive = await analysisStorageService.loadComprehensiveAnalysis(videoPath)
132+
if (comprehensive.success && comprehensive.data) {
133+
results.comprehensive = comprehensive.data
134+
135+
// Загружаем метаданные
136+
if (includeMetadata && comprehensive.data.analysis_id) {
137+
metadata = await analysisStorageService.loadAnalysisMetadata(comprehensive.data.analysis_id)
138+
}
139+
}
140+
}
141+
142+
if (analysisType === "montage" || analysisType === "all") {
143+
const montage = await analysisStorageService.loadMontageAnalysis(videoPath)
144+
if (montage.success && montage.data) {
145+
results.montage = montage.data
146+
}
147+
}
148+
149+
if (analysisType === "unified" || analysisType === "all") {
150+
const unified = await analysisStorageService.loadUnifiedAnalysis(videoPath)
151+
if (unified.success && unified.data) {
152+
results.unified = unified.data
153+
}
154+
}
155+
156+
const hasResults = Object.keys(results).length > 0
157+
158+
return {
159+
success: hasResults,
160+
videoPath,
161+
analysisType,
162+
results,
163+
metadata: includeMetadata ? metadata : undefined,
164+
}
165+
},
166+
input,
167+
options,
168+
)
169+
}
170+
171+
validate(input: any): boolean {
172+
// Валидация не строгая - все поля опциональны
173+
return typeof input === "object" && input !== null
174+
}
175+
176+
getSchema(): { input: any; output: any } {
177+
return {
178+
input: this.metadata.inputSchema,
179+
output: this.metadata.outputSchema,
180+
}
181+
}
182+
}
183+
184+
// ============================================================================
185+
// LIST MEDIA FILES TOOL
186+
// ============================================================================
187+
188+
export class ListProjectMediaTool extends BaseAITool implements IAITool {
189+
metadata: AIToolMetadata = {
190+
name: "list-project-media",
191+
displayName: "Список медиа в проекте",
192+
description: "Получает список всех медиа файлов, добавленных в проект, с их основной информацией",
193+
domain: "analysis",
194+
category: "content-intelligence",
195+
tags: ["media", "project", "files", "list"],
196+
version: "1.0.0",
197+
author: "Timeline Studio",
198+
dependencies: [],
199+
inputSchema: {
200+
type: "object",
201+
properties: {
202+
includeAnalysisStatus: {
203+
type: "boolean",
204+
description: "Включить статус анализа для каждого файла",
205+
},
206+
},
207+
required: [],
208+
},
209+
outputSchema: {
210+
type: "object",
211+
properties: {
212+
success: { type: "boolean" },
213+
files: {
214+
type: "array",
215+
items: {
216+
type: "object",
217+
properties: {
218+
path: { type: "string" },
219+
name: { type: "string" },
220+
hasAnalysis: { type: "boolean" },
221+
},
222+
},
223+
},
224+
totalCount: { type: "number" },
225+
},
226+
},
227+
examples: [
228+
{
229+
description: "Получить список всех медиа файлов",
230+
input: { includeAnalysisStatus: true },
231+
expectedOutput: { success: true, files: [], totalCount: 0 },
232+
},
233+
],
234+
}
235+
236+
async execute(
237+
input: { includeAnalysisStatus?: boolean },
238+
options?: AIToolExecutionOptions,
239+
): Promise<AIToolResult<any>> {
240+
return this.executeWithErrorHandling(
241+
async (_context) => {
242+
// Получаем список проанализированных видео из storage
243+
const analyzedVideos = await analysisStorageService.getAnalyzedVideos()
244+
245+
const files = analyzedVideos.map((path) => ({
246+
path,
247+
name: path.split("/").pop() || path,
248+
hasAnalysis: true,
249+
}))
250+
251+
return {
252+
success: true,
253+
files,
254+
totalCount: files.length,
255+
}
256+
},
257+
input,
258+
options,
259+
)
260+
}
261+
262+
validate(input: any): boolean {
263+
return typeof input === "object" && input !== null
264+
}
265+
266+
getSchema(): { input: any; output: any } {
267+
return {
268+
input: this.metadata.inputSchema,
269+
output: this.metadata.outputSchema,
270+
}
271+
}
272+
}
273+
274+
// ============================================================================
275+
// Exports
276+
// ============================================================================
277+
278+
export const analysisResultsTools = [new GetAnalysisResultsTool(), new ListProjectMediaTool()]
279+
280+
export const ANALYSIS_RESULTS_TOOLS_COUNT = analysisResultsTools.length

src/domains/ai-tools/tools/analysis/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Инструменты анализа и обработки контента
44
*/
55

6+
// Analysis Results Tools
7+
export * from "./analysis-results"
68
// Audio Analysis Tools
79
export * from "./audio-analysis"
810
// Multimodal & Person ID Tools
@@ -13,6 +15,7 @@ export * from "./video-analysis"
1315
// Whisper Tools
1416
export * from "./whisper"
1517

18+
import { analysisResultsTools } from "./analysis-results"
1619
import { audioAnalysisTools } from "./audio-analysis"
1720
import { multimodalTools } from "./multimodal"
1821
import { personIdentificationTools } from "./person-identification"
@@ -22,6 +25,7 @@ import { whisperTools } from "./whisper"
2225

2326
// Все Analysis инструменты
2427
export const analysisTools = [
28+
...analysisResultsTools,
2529
...videoAnalysisTools,
2630
...audioAnalysisTools,
2731
...whisperTools,

src/domains/video-editing/hooks/use-playback-time-sync.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ export function usePlaybackTimeSync({
126126
}
127127

128128
const element = videoElementRef.current
129-
if (!element || element.paused || element.ended) {
129+
if (!element) {
130130
rafIdRef.current = requestAnimationFrame(updatePlaybackTime)
131131
return
132132
}
133133

134+
// Если видео закончилось - останавливаем цикл
135+
// Примечание: НЕ проверяем element.paused здесь, т.к. видео может быть
136+
// на паузе пока загружается, но isPlaying уже true. Цикл остановится
137+
// через cleanup useEffect когда isPlaying станет false.
138+
if (element.ended) {
139+
return
140+
}
141+
134142
const currentTime = element.currentTime
135143

136144
// L1: Обновляем локальное состояние каждый кадр

src/domains/video-editing/providers/__tests__/player-provider.test.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,18 @@ vi.mock("@/core/container", () => ({
4242
// Mock user settings
4343
const mockUserSettings = {
4444
playerVolume: 50,
45-
handlePlayerVolumeChange: vi.fn(),
45+
playerVideoSource: "browser" as const,
46+
updatePlayerVolume: vi.fn(),
47+
updatePlayerVideoSource: vi.fn(),
4648
}
4749

48-
vi.mock("@/features/user-settings", () => ({
50+
vi.mock("@/domains/project-management", () => ({
4951
useUserSettings: () => mockUserSettings,
5052
}))
5153

5254
// Mock playback time sync hook
5355
const mockUsePlaybackTimeSync = vi.fn()
54-
vi.mock("@/features/video-player/hooks/use-playback-time-sync", () => ({
56+
vi.mock("@/domains/video-editing/hooks", () => ({
5557
usePlaybackTimeSync: (config: any) => {
5658
mockUsePlaybackTimeSync(config)
5759
return config.initialTime
@@ -429,13 +431,14 @@ describe("PlayerProvider", () => {
429431

430432
const { result } = renderHook(() => usePlayer(), { wrapper })
431433

432-
expect(result.current.videoSource).toBe("timeline")
434+
// Default is "browser" from userSettings
435+
expect(result.current.videoSource).toBe("browser")
433436

434437
act(() => {
435-
result.current.setVideoSource("browser")
438+
result.current.setVideoSource("timeline")
436439
})
437440

438-
expect(result.current.videoSource).toBe("browser")
441+
expect(result.current.videoSource).toBe("timeline")
439442
expect(mockExecuteCommand).not.toHaveBeenCalled()
440443
})
441444

@@ -472,7 +475,7 @@ describe("PlayerProvider", () => {
472475
})
473476

474477
expect(result.current.volume).toBe(0.8)
475-
expect(mockUserSettings.handlePlayerVolumeChange).toHaveBeenCalledWith(80)
478+
expect(mockUserSettings.updatePlayerVolume).toHaveBeenCalledWith(80)
476479
})
477480

478481
it("setDuration updates duration locally", () => {

0 commit comments

Comments
 (0)