eprec 1.13.0 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/client/trim-points.tsx +30 -41
- package/app/routes/trim-points.tsx +2 -2
- package/app/trim-api.ts +56 -55
- package/package.json +1 -1
- package/process-course/edits/cli.ts +3 -0
- package/src/cli.ts +4 -1
|
@@ -75,8 +75,7 @@ function parseTimestampInput(value: string) {
|
|
|
75
75
|
if (parts.length !== 2 && parts.length !== 3) return null
|
|
76
76
|
const secondsPart = Number.parseFloat(parts[parts.length - 1] ?? '')
|
|
77
77
|
const minutesPart = Number.parseFloat(parts[parts.length - 2] ?? '')
|
|
78
|
-
const hoursPart =
|
|
79
|
-
parts.length === 3 ? Number.parseFloat(parts[0] ?? '') : 0
|
|
78
|
+
const hoursPart = parts.length === 3 ? Number.parseFloat(parts[0] ?? '') : 0
|
|
80
79
|
if (
|
|
81
80
|
!Number.isFinite(secondsPart) ||
|
|
82
81
|
!Number.isFinite(minutesPart) ||
|
|
@@ -99,7 +98,9 @@ function classNames(...values: Array<string | false | null | undefined>) {
|
|
|
99
98
|
export function TrimPoints(handle: Handle) {
|
|
100
99
|
const initialVideoPath = readInitialVideoPath()
|
|
101
100
|
let videoPathInput = initialVideoPath
|
|
102
|
-
let outputPathInput = initialVideoPath
|
|
101
|
+
let outputPathInput = initialVideoPath
|
|
102
|
+
? buildOutputPath(initialVideoPath)
|
|
103
|
+
: ''
|
|
103
104
|
let pathStatus: 'idle' | 'loading' | 'ready' | 'error' = initialVideoPath
|
|
104
105
|
? 'loading'
|
|
105
106
|
: 'idle'
|
|
@@ -117,9 +118,11 @@ export function TrimPoints(handle: Handle) {
|
|
|
117
118
|
let trimRanges: TrimRangeWithId[] = []
|
|
118
119
|
let selectedRangeId: string | null = null
|
|
119
120
|
let rangeCounter = 1
|
|
120
|
-
let activeDrag:
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
let activeDrag: {
|
|
122
|
+
rangeId: string
|
|
123
|
+
edge: 'start' | 'end'
|
|
124
|
+
pointerId: number
|
|
125
|
+
} | null = null
|
|
123
126
|
let runStatus: 'idle' | 'running' | 'success' | 'error' = 'idle'
|
|
124
127
|
let runProgress = 0
|
|
125
128
|
let runError = ''
|
|
@@ -155,6 +158,9 @@ export function TrimPoints(handle: Handle) {
|
|
|
155
158
|
previewReady = false
|
|
156
159
|
previewError = ''
|
|
157
160
|
previewDuration = 0
|
|
161
|
+
trimRanges = []
|
|
162
|
+
selectedRangeId = null
|
|
163
|
+
activeDrag = null
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
const syncVideoToTime = (
|
|
@@ -202,8 +208,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
202
208
|
const width = waveformNode.clientWidth
|
|
203
209
|
const height = waveformNode.clientHeight
|
|
204
210
|
if (width <= 0 || height <= 0) return
|
|
205
|
-
const dpr =
|
|
206
|
-
typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
|
|
211
|
+
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
|
|
207
212
|
waveformNode.width = Math.floor(width * dpr)
|
|
208
213
|
waveformNode.height = Math.floor(height * dpr)
|
|
209
214
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
@@ -271,17 +276,13 @@ export function TrimPoints(handle: Handle) {
|
|
|
271
276
|
audioBuffer.getChannelData(index),
|
|
272
277
|
)
|
|
273
278
|
const totalSamples = audioBuffer.length
|
|
274
|
-
const sampleCount = Math.max(
|
|
275
|
-
1,
|
|
276
|
-
Math.min(WAVEFORM_SAMPLES, totalSamples),
|
|
277
|
-
)
|
|
279
|
+
const sampleCount = Math.max(1, Math.min(WAVEFORM_SAMPLES, totalSamples))
|
|
278
280
|
const blockSize = Math.max(1, Math.floor(totalSamples / sampleCount))
|
|
279
281
|
const samples = new Array(sampleCount).fill(0)
|
|
280
282
|
let maxValue = 0
|
|
281
283
|
for (let i = 0; i < sampleCount; i++) {
|
|
282
284
|
const start = i * blockSize
|
|
283
|
-
const end =
|
|
284
|
-
i === sampleCount - 1 ? totalSamples : start + blockSize
|
|
285
|
+
const end = i === sampleCount - 1 ? totalSamples : start + blockSize
|
|
285
286
|
let peak = 0
|
|
286
287
|
for (let j = start; j < end; j++) {
|
|
287
288
|
let sum = 0
|
|
@@ -306,9 +307,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
306
307
|
if (waveformSource !== fetchedUrl) return
|
|
307
308
|
waveformStatus = 'error'
|
|
308
309
|
waveformError =
|
|
309
|
-
error instanceof Error
|
|
310
|
-
? error.message
|
|
311
|
-
: 'Unable to render waveform.'
|
|
310
|
+
error instanceof Error ? error.message : 'Unable to render waveform.'
|
|
312
311
|
handle.update()
|
|
313
312
|
}
|
|
314
313
|
}
|
|
@@ -415,9 +414,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
415
414
|
trimRanges = sortRanges(
|
|
416
415
|
trimRanges.map((range) => {
|
|
417
416
|
if (range.id !== rangeId) return range
|
|
418
|
-
let nextStart = Number.isFinite(patch.start)
|
|
419
|
-
? patch.start
|
|
420
|
-
: range.start
|
|
417
|
+
let nextStart = Number.isFinite(patch.start) ? patch.start : range.start
|
|
421
418
|
let nextEnd = Number.isFinite(patch.end) ? patch.end : range.end
|
|
422
419
|
if (edge === 'start') {
|
|
423
420
|
nextStart = clamp(
|
|
@@ -425,11 +422,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
425
422
|
0,
|
|
426
423
|
Math.max(previewDuration - MIN_TRIM_LENGTH, 0),
|
|
427
424
|
)
|
|
428
|
-
nextEnd = clamp(
|
|
429
|
-
nextEnd,
|
|
430
|
-
nextStart + MIN_TRIM_LENGTH,
|
|
431
|
-
previewDuration,
|
|
432
|
-
)
|
|
425
|
+
nextEnd = clamp(nextEnd, nextStart + MIN_TRIM_LENGTH, previewDuration)
|
|
433
426
|
} else if (edge === 'end') {
|
|
434
427
|
nextEnd = clamp(nextEnd, MIN_TRIM_LENGTH, previewDuration)
|
|
435
428
|
nextStart = clamp(nextStart, 0, nextEnd - MIN_TRIM_LENGTH)
|
|
@@ -506,10 +499,8 @@ export function TrimPoints(handle: Handle) {
|
|
|
506
499
|
range: TrimRangeWithId,
|
|
507
500
|
edge: 'start' | 'end',
|
|
508
501
|
) => {
|
|
509
|
-
const isForward =
|
|
510
|
-
|
|
511
|
-
const isBackward =
|
|
512
|
-
event.key === 'ArrowDown' || event.key === 'ArrowLeft'
|
|
502
|
+
const isForward = event.key === 'ArrowUp' || event.key === 'ArrowRight'
|
|
503
|
+
const isBackward = event.key === 'ArrowDown' || event.key === 'ArrowLeft'
|
|
513
504
|
if (!isForward && !isBackward) return
|
|
514
505
|
event.preventDefault()
|
|
515
506
|
const step = event.shiftKey ? SHIFT_STEP : KEYBOARD_STEP
|
|
@@ -693,7 +684,9 @@ export function TrimPoints(handle: Handle) {
|
|
|
693
684
|
MIN_TRIM_LENGTH,
|
|
694
685
|
)
|
|
695
686
|
const commandPreview =
|
|
696
|
-
videoPathInput.trim() &&
|
|
687
|
+
videoPathInput.trim() &&
|
|
688
|
+
outputPathInput.trim() &&
|
|
689
|
+
normalizedRanges.length > 0
|
|
697
690
|
? buildFfmpegCommandPreview({
|
|
698
691
|
inputPath: videoPathInput.trim(),
|
|
699
692
|
outputPath: outputPathInput.trim(),
|
|
@@ -731,8 +724,8 @@ export function TrimPoints(handle: Handle) {
|
|
|
731
724
|
<div>
|
|
732
725
|
<h2>Video source</h2>
|
|
733
726
|
<p class="app-muted">
|
|
734
|
-
Load a local video file to calculate the trim timeline and
|
|
735
|
-
command.
|
|
727
|
+
Load a local video file to calculate the trim timeline and
|
|
728
|
+
output command.
|
|
736
729
|
</p>
|
|
737
730
|
</div>
|
|
738
731
|
<span
|
|
@@ -938,7 +931,8 @@ export function TrimPoints(handle: Handle) {
|
|
|
938
931
|
</button>
|
|
939
932
|
</div>
|
|
940
933
|
<p class="app-muted trim-hint" id={hintId}>
|
|
941
|
-
Use arrow keys to nudge by {KEYBOARD_STEP}s. Hold Shift for
|
|
934
|
+
Use arrow keys to nudge by {KEYBOARD_STEP}s. Hold Shift for{' '}
|
|
935
|
+
{SHIFT_STEP}
|
|
942
936
|
s.
|
|
943
937
|
</p>
|
|
944
938
|
<div
|
|
@@ -998,8 +992,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
998
992
|
on={{
|
|
999
993
|
focus: () =>
|
|
1000
994
|
syncVideoToTime(range.start, { updateInput: true }),
|
|
1001
|
-
pointerdown: (event) =>
|
|
1002
|
-
startDrag(event, range.id, 'start'),
|
|
995
|
+
pointerdown: (event) => startDrag(event, range.id, 'start'),
|
|
1003
996
|
pointermove: moveDrag,
|
|
1004
997
|
pointerup: endDrag,
|
|
1005
998
|
pointercancel: endDrag,
|
|
@@ -1086,9 +1079,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
1086
1079
|
<section class="app-card">
|
|
1087
1080
|
<div class="panel-header">
|
|
1088
1081
|
<h2>Trim ranges</h2>
|
|
1089
|
-
<span class="summary-subtext">
|
|
1090
|
-
{sortedRanges.length} total
|
|
1091
|
-
</span>
|
|
1082
|
+
<span class="summary-subtext">{sortedRanges.length} total</span>
|
|
1092
1083
|
</div>
|
|
1093
1084
|
{sortedRanges.length === 0 ? (
|
|
1094
1085
|
<p class="app-muted">
|
|
@@ -1211,9 +1202,7 @@ export function TrimPoints(handle: Handle) {
|
|
|
1211
1202
|
<div class="summary-item">
|
|
1212
1203
|
<span class="summary-label">Output length</span>
|
|
1213
1204
|
<span class="summary-value">
|
|
1214
|
-
{previewReady
|
|
1215
|
-
? formatTimestamp(outputDuration)
|
|
1216
|
-
: '--:--.--'}
|
|
1205
|
+
{previewReady ? formatTimestamp(outputDuration) : '--:--.--'}
|
|
1217
1206
|
</span>
|
|
1218
1207
|
<span class="summary-subtext">
|
|
1219
1208
|
{previewReady && duration > 0
|
|
@@ -15,8 +15,8 @@ const trimPointsHandler = {
|
|
|
15
15
|
<span class="app-kicker">Eprec Studio</span>
|
|
16
16
|
<h1 class="app-title">Trim points</h1>
|
|
17
17
|
<p class="app-subtitle">
|
|
18
|
-
Add start and stop points, generate an ffmpeg trim command, and
|
|
19
|
-
it with live progress.
|
|
18
|
+
Add start and stop points, generate an ffmpeg trim command, and
|
|
19
|
+
run it with live progress.
|
|
20
20
|
</p>
|
|
21
21
|
<nav class="app-nav">
|
|
22
22
|
<a class="app-link" href="/">Editing workspace</a>
|
package/app/trim-api.ts
CHANGED
|
@@ -45,7 +45,10 @@ function parseRanges(ranges: unknown): TrimRange[] {
|
|
|
45
45
|
.map((entry) => {
|
|
46
46
|
if (!entry || typeof entry !== 'object') return null
|
|
47
47
|
const candidate = entry as TrimRange
|
|
48
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
!Number.isFinite(candidate.start) ||
|
|
50
|
+
!Number.isFinite(candidate.end)
|
|
51
|
+
) {
|
|
49
52
|
return null
|
|
50
53
|
}
|
|
51
54
|
return { start: candidate.start, end: candidate.end }
|
|
@@ -178,9 +181,7 @@ export async function handleTrimRequest(request: Request): Promise<Response> {
|
|
|
178
181
|
let outTimeSeconds = 0
|
|
179
182
|
const send = (payload: Record<string, unknown>) => {
|
|
180
183
|
try {
|
|
181
|
-
controller.enqueue(
|
|
182
|
-
encoder.encode(`${JSON.stringify(payload)}\n`),
|
|
183
|
-
)
|
|
184
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(payload)}\n`))
|
|
184
185
|
} catch {
|
|
185
186
|
// stream closed
|
|
186
187
|
}
|
|
@@ -191,63 +192,63 @@ export async function handleTrimRequest(request: Request): Promise<Response> {
|
|
|
191
192
|
stderr: 'pipe',
|
|
192
193
|
})
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
request.signal.addEventListener('abort', () => {
|
|
196
|
+
try {
|
|
197
|
+
process.kill()
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
})
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
if (key === 'out_time_us') {
|
|
210
|
-
const next = Number.parseFloat(value)
|
|
211
|
-
if (Number.isFinite(next)) outTimeSeconds = next / 1000000
|
|
212
|
-
}
|
|
213
|
-
if (key === 'out_time') {
|
|
214
|
-
const parsed = parseOutTimeValue(value)
|
|
215
|
-
if (parsed !== null) outTimeSeconds = parsed
|
|
216
|
-
}
|
|
217
|
-
if (key === 'progress') {
|
|
218
|
-
const progress =
|
|
219
|
-
outputDurationSeconds > 0
|
|
220
|
-
? clamp(outTimeSeconds / outputDurationSeconds, 0, 1)
|
|
221
|
-
: 0
|
|
222
|
-
send({ type: 'progress', progress })
|
|
223
|
-
if (value === 'end') {
|
|
224
|
-
send({ type: 'progress', progress: 1 })
|
|
203
|
+
const stdoutReader = readLines(process.stdout, (line) => {
|
|
204
|
+
const [key, rawValue] = line.split('=')
|
|
205
|
+
const value = rawValue ?? ''
|
|
206
|
+
if (key === 'out_time_ms') {
|
|
207
|
+
const next = Number.parseFloat(value)
|
|
208
|
+
if (Number.isFinite(next)) outTimeSeconds = next / 1000
|
|
225
209
|
}
|
|
226
|
-
|
|
227
|
-
|
|
210
|
+
if (key === 'out_time_us') {
|
|
211
|
+
const next = Number.parseFloat(value)
|
|
212
|
+
if (Number.isFinite(next)) outTimeSeconds = next / 1000000
|
|
213
|
+
}
|
|
214
|
+
if (key === 'out_time') {
|
|
215
|
+
const parsed = parseOutTimeValue(value)
|
|
216
|
+
if (parsed !== null) outTimeSeconds = parsed
|
|
217
|
+
}
|
|
218
|
+
if (key === 'progress') {
|
|
219
|
+
const progress =
|
|
220
|
+
outputDurationSeconds > 0
|
|
221
|
+
? clamp(outTimeSeconds / outputDurationSeconds, 0, 1)
|
|
222
|
+
: 0
|
|
223
|
+
send({ type: 'progress', progress })
|
|
224
|
+
if (value === 'end') {
|
|
225
|
+
send({ type: 'progress', progress: 1 })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
})
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
const stderrReader = readLines(process.stderr, (line) => {
|
|
231
|
+
send({ type: 'log', message: line })
|
|
232
|
+
})
|
|
232
233
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
Promise.all([stdoutReader, stderrReader, process.exited])
|
|
235
|
+
.then(([, , exitCode]) => {
|
|
236
|
+
send({
|
|
237
|
+
type: 'done',
|
|
238
|
+
success: exitCode === 0,
|
|
239
|
+
exitCode,
|
|
240
|
+
})
|
|
239
241
|
})
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
.catch((error) => {
|
|
243
|
+
send({
|
|
244
|
+
type: 'done',
|
|
245
|
+
success: false,
|
|
246
|
+
error: error instanceof Error ? error.message : String(error),
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
.finally(() => {
|
|
250
|
+
controller.close()
|
|
246
251
|
})
|
|
247
|
-
})
|
|
248
|
-
.finally(() => {
|
|
249
|
-
controller.close()
|
|
250
|
-
})
|
|
251
252
|
},
|
|
252
253
|
})
|
|
253
254
|
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createInquirerPrompter,
|
|
9
9
|
createPathPicker,
|
|
10
10
|
createStepProgressReporter,
|
|
11
|
+
handlePromptFailure,
|
|
11
12
|
isInteractive,
|
|
12
13
|
pauseActiveSpinner,
|
|
13
14
|
resolveOptionalString,
|
|
@@ -373,6 +374,8 @@ export async function runEditsCli(rawArgs = hideBin(process.argv)) {
|
|
|
373
374
|
)
|
|
374
375
|
.demandCommand(1)
|
|
375
376
|
.strict()
|
|
377
|
+
.fail(handlePromptFailure)
|
|
378
|
+
.exitProcess(false)
|
|
376
379
|
.help()
|
|
377
380
|
|
|
378
381
|
await parser.parseAsync()
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import type { Arguments, CommandBuilder, CommandHandler } from 'yargs'
|
|
3
|
+
import type { Argv, Arguments, CommandBuilder, CommandHandler } from 'yargs'
|
|
4
4
|
import yargs from 'yargs/yargs'
|
|
5
5
|
import { hideBin } from 'yargs/helpers'
|
|
6
6
|
import { startAppServer } from './app-server'
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
createInquirerPrompter,
|
|
29
29
|
createPathPicker,
|
|
30
30
|
createStepProgressReporter,
|
|
31
|
+
handlePromptFailure,
|
|
31
32
|
isInteractive,
|
|
32
33
|
pauseActiveSpinner,
|
|
33
34
|
resumeActiveSpinner,
|
|
@@ -249,6 +250,8 @@ async function main(rawArgs = hideBin(process.argv)) {
|
|
|
249
250
|
)
|
|
250
251
|
.demandCommand(1)
|
|
251
252
|
.strict()
|
|
253
|
+
.fail(handlePromptFailure)
|
|
254
|
+
.exitProcess(false)
|
|
252
255
|
.help()
|
|
253
256
|
|
|
254
257
|
await parser.parseAsync()
|