File size: 4,919 Bytes
7312c42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { describe, expect, it } from 'vitest'
import {
  applySmartSplit,
  applySmartSplitToMatchingTasks,
  hasMatchingTaskSegment,
  splitTrackSegmentAtFrame,
  type SmartSplitResult,
} from '@/lib/smart-split'
import type { TrackData } from '@/types/multitrack'

function result(ranges: Array<[number, number]>): SmartSplitResult {
  return {
    ranges,
  }
}

function trackData(): TrackData {
  return {
    frame_rate: 24,
    total_length: 300,
    tracks: [
      {
        id: 'task-track', name: 'Task', type: 'task', color: 'var(--primary)', muted: false, locked: false,
        segments: [
          {
            id: 'linked-task', start_frame: 24, end_frame: 264, color: 'var(--primary)',
            content: {
              media_type: 'none', text: 'linked', system_prompt: 'system',
              images: [{ id: 'image', source_type: 'input', file_path: 'image.png' }],
            },
          },
          { id: 'other-task', start_frame: 0, end_frame: 24, color: 'var(--primary)', content: { media_type: 'none', text: 'other' } },
        ],
      },
      {
        id: 'video-track', name: 'Video', type: 'video', color: 'var(--secondary)', muted: false, locked: false,
        segments: [{
          id: 'video', start_frame: 24, end_frame: 264, color: 'var(--secondary)',
          content: { media_type: 'video', source_type: 'input', file_path: 'clip.mp4' },
        }],
      },
    ],
  }
}

describe('applySmartSplit', () => {
  it('maps detector frames to timeline frames and splits linked task ranges', () => {
    const updated = applySmartSplit(trackData(), 'video', result([[0, 60], [60, 120], [120, 240]]))
    const video = updated.tracks.find((track) => track.type === 'video')!
    const task = updated.tracks.find((track) => track.type === 'task')!

    expect(video.segments.map(({ start_frame, end_frame }) => [start_frame, end_frame])).toEqual([
      [24, 84], [84, 144], [144, 264],
    ])
    expect(video.segments.map((segment) => segment.origin_start_frame)).toEqual([24, 24, 24])
    expect(task.segments.filter((segment) => segment.content.text === 'linked')
      .map(({ start_frame, end_frame }) => [start_frame, end_frame])).toEqual([
      [24, 84], [84, 144], [144, 264],
    ])
    expect(task.segments.find((segment) => segment.id === 'other-task')).toMatchObject({ start_frame: 0, end_frame: 24 })
    expect(task.segments.every((segment) => segment.origin_start_frame === undefined)).toBe(true)
    const clonedTasks = task.segments.filter((segment) => segment.content.text === 'linked')
    expect(clonedTasks.every((segment) => (
      segment.content.system_prompt === 'system' && segment.content.images?.[0]?.file_path === 'image.png'
    ))).toBe(true)
    expect(clonedTasks[0].content.images).not.toBe(clonedTasks[1].content.images)
  })

  it('preserves an existing source trim origin when splitting an already-trimmed video', () => {
    const data = trackData()
    data.tracks[1].segments[0].origin_start_frame = 0

    const updated = applySmartSplit(data, 'video', result([[0, 60], [60, 240]]))

    expect(updated.tracks[1].segments.map((segment) => segment.origin_start_frame)).toEqual([0, 0, 0])
    expect(updated.tracks[1].segments.map(({ start_frame, end_frame }) => [start_frame, end_frame])).toEqual([
      [24, 60], [60, 240], [240, 264],
    ])
  })

  it('leaves data unchanged when no internal cut was detected', () => {
    const data = trackData()
    expect(applySmartSplit(data, 'video', result([[0, 240]]))).toBe(data)
  })

  it('splits only matching task segments and leaves the video unchanged', () => {
    const data = trackData()
    expect(hasMatchingTaskSegment(data, 'video')).toBe(true)

    const updated = applySmartSplitToMatchingTasks(data, 'video', result([[0, 120], [120, 240]]))

    expect(updated.tracks[1].segments).toEqual(data.tracks[1].segments)
    expect(updated.tracks[0].segments.filter((segment) => segment.content.text === 'linked')
      .map(({ start_frame, end_frame }) => [start_frame, end_frame])).toEqual([
      [24, 144], [144, 264],
    ])
  })

  it('manually cuts media with a shared source origin and clones task content', () => {
    const data = trackData()
    const videoResult = splitTrackSegmentAtFrame(data, 'video', 84)
    expect(videoResult.tracks[1].segments.map((segment) => ({
      range: [segment.start_frame, segment.end_frame],
      origin: segment.origin_start_frame,
    }))).toEqual([
      { range: [24, 84], origin: 24 },
      { range: [84, 264], origin: 24 },
    ])

    const taskResult = splitTrackSegmentAtFrame(data, 'linked-task', 84)
    const tasks = taskResult.tracks[0].segments.filter((segment) => segment.content.text === 'linked')
    expect(tasks).toHaveLength(2)
    expect(tasks.every((segment) => segment.content.images?.[0]?.file_path === 'image.png')).toBe(true)
    expect(tasks[0].content.images).not.toBe(tasks[1].content.images)
  })
})