File size: 6,239 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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | import { uuid } from '@/lib/uuid'
import { throwIfMissingModelResponse } from '@/lib/model-download'
import type { MultiTrackSegment, TrackData } from '@/types/multitrack'
export interface SmartSplitResult {
ranges: Array<[number, number]>
}
function parseSmartSplitResult(value: unknown): SmartSplitResult {
if (!value || typeof value !== 'object') throw new Error('Invalid smart split response')
const result = value as Partial<SmartSplitResult> & { error?: unknown }
if (typeof result.error === 'string') throw new Error(result.error)
if (!Array.isArray(result.ranges)) {
throw new Error('Invalid smart split response')
}
const ranges = result.ranges.map((range) => {
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
throw new Error('Invalid shot range in smart split response')
}
return [Number(range[0]), Number(range[1])] as [number, number]
})
return { ranges }
}
export async function requestSmartSplit(segment: MultiTrackSegment, fps: number): Promise<SmartSplitResult> {
const response = await fetch('/easy-media/video/smart-split', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source_type: segment.content.source_type ?? 'input',
file_path: segment.content.file_path,
local_path: segment.content.local_path,
url: segment.content.url,
fps,
}),
})
let payload: unknown
try {
payload = await response.json()
} catch (error) {
throw new Error(`Smart split returned invalid JSON: ${String(error)}`)
}
if (!response.ok) {
throwIfMissingModelResponse(payload)
const message = payload && typeof payload === 'object' && 'error' in payload
? String((payload as { error: unknown }).error)
: `Smart split failed (${response.status})`
throw new Error(message)
}
return parseSmartSplitResult(payload)
}
function splitSegment(
segment: MultiTrackSegment,
boundaries: number[],
originStartFrame?: number,
): MultiTrackSegment[] {
const points = [segment.start_frame, ...boundaries, segment.end_frame]
return points.slice(0, -1).map((startFrame, index) => ({
...segment,
id: index === 0 ? segment.id : uuid(),
start_frame: startFrame,
end_frame: points[index + 1],
...(originStartFrame === undefined ? {} : { origin_start_frame: originStartFrame }),
content: {
...segment.content,
images: segment.content.images?.map((image) => ({ ...image })),
},
}))
}
function findVideoSegment(data: TrackData, videoSegmentId: string): MultiTrackSegment | undefined {
return data.tracks
.find((track) => track.type === 'video' && track.segments.some((segment) => segment.id === videoSegmentId))
?.segments.find((segment) => segment.id === videoSegmentId)
}
function smartSplitBoundaries(
source: MultiTrackSegment,
result: SmartSplitResult,
): number[] {
const sourceOriginStart = source.origin_start_frame ?? source.start_frame
return [...new Set(result.ranges
.map((range) => range[1] + sourceOriginStart)
.filter((frame) => frame > source.start_frame && frame < source.end_frame))]
.sort((left, right) => left - right)
}
export function hasMatchingTaskSegment(data: TrackData, videoSegmentId: string): boolean {
const source = findVideoSegment(data, videoSegmentId)
if (!source) return false
return data.tracks.some((track) => track.type === 'task' && track.segments.some((segment) => (
segment.start_frame === source.start_frame && segment.end_frame === source.end_frame
)))
}
export function applySmartSplit(
data: TrackData,
videoSegmentId: string,
result: SmartSplitResult,
): TrackData {
const source = findVideoSegment(data, videoSegmentId)
if (!source) return data
const duration = source.end_frame - source.start_frame
const sourceOriginStart = source.origin_start_frame ?? source.start_frame
const boundaries = smartSplitBoundaries(source, result)
if (duration <= 1 || boundaries.length === 0) return data
const tracks = data.tracks.map((track) => ({
...track,
segments: track.segments.flatMap((segment) => {
const isSource = segment.id === videoSegmentId
const isLinkedTask = track.type === 'task'
&& segment.start_frame === source.start_frame
&& segment.end_frame === source.end_frame
if (isSource) return splitSegment(segment, boundaries, sourceOriginStart)
return isLinkedTask ? splitSegment(segment, boundaries) : [segment]
}).sort((left, right) => left.start_frame - right.start_frame),
}))
return { ...data, tracks }
}
export function applySmartSplitToMatchingTasks(
data: TrackData,
videoSegmentId: string,
result: SmartSplitResult,
): TrackData {
const source = findVideoSegment(data, videoSegmentId)
if (!source) return data
const boundaries = smartSplitBoundaries(source, result)
if (boundaries.length === 0) return data
return {
...data,
tracks: data.tracks.map((track) => ({
...track,
segments: track.segments.flatMap((segment) => (
track.type === 'task'
&& segment.start_frame === source.start_frame
&& segment.end_frame === source.end_frame
? splitSegment(segment, boundaries)
: [segment]
)).sort((left, right) => left.start_frame - right.start_frame),
})),
}
}
export function splitTrackSegmentAtFrame(
data: TrackData,
segmentId: string,
splitFrame: number,
): TrackData {
const target = data.tracks.flatMap((track) => track.segments).find((segment) => segment.id === segmentId)
const frame = Math.round(splitFrame)
if (!target || frame <= target.start_frame || frame >= target.end_frame) return data
const keepsSourceOffset = target.content.media_type === 'video' || target.content.media_type === 'audio'
const originStart = keepsSourceOffset ? target.origin_start_frame ?? target.start_frame : undefined
return {
...data,
tracks: data.tracks.map((track) => ({
...track,
segments: track.segments.flatMap((segment) => (
segment.id === segmentId ? splitSegment(segment, [frame], originStart) : [segment]
)).sort((left, right) => left.start_frame - right.start_frame),
})),
}
}
|