eprec 1.2.0 → 1.4.0

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.
@@ -1,7 +1,18 @@
1
1
  #!/usr/bin/env bun
2
+ import path from 'node:path'
2
3
  import type { Argv, Arguments, CommandBuilder, CommandHandler } from 'yargs'
3
4
  import yargs from 'yargs/yargs'
4
5
  import { hideBin } from 'yargs/helpers'
6
+ import {
7
+ PromptCancelled,
8
+ createInquirerPrompter,
9
+ createPathPicker,
10
+ isInteractive,
11
+ resolveOptionalString,
12
+ type PathPicker,
13
+ type Prompter,
14
+ withSpinner,
15
+ } from '../../cli-ux'
5
16
  import { editVideo, buildEditedOutputPath } from './video-editor'
6
17
  import { combineVideos } from './combined-video-editor'
7
18
 
@@ -24,21 +35,209 @@ export type CombineVideosCommandArgs = {
24
35
  'padding-ms'?: number
25
36
  }
26
37
 
38
+ type CliUxOptions = {
39
+ interactive: boolean
40
+ pathPicker?: PathPicker
41
+ }
42
+
43
+ export function buildCombinedOutputPath(
44
+ video1Path: string,
45
+ video2Path: string,
46
+ ) {
47
+ const dir = path.dirname(video1Path)
48
+ const ext = path.extname(video1Path) || path.extname(video2Path) || '.mp4'
49
+ const name1 = path.parse(video1Path).name
50
+ const name2 = path.parse(video2Path).name
51
+ return path.join(dir, `combined-${name1}-${name2}${ext}`)
52
+ }
53
+
54
+ export async function resolveEditVideoArgs(
55
+ argv: Arguments,
56
+ options: CliUxOptions,
57
+ ): Promise<EditVideoCommandArgs> {
58
+ const pathPicker = options.pathPicker
59
+ let input = resolveOptionalString(argv.input)
60
+ if (!input) {
61
+ if (!options.interactive || !pathPicker) {
62
+ throw new Error('Input video path is required.')
63
+ }
64
+ input = await pathPicker.pickExistingFile({
65
+ message: 'Select input video file',
66
+ })
67
+ }
68
+ let transcript = resolveOptionalString(argv.transcript)
69
+ if (!transcript) {
70
+ if (!options.interactive || !pathPicker) {
71
+ throw new Error('Transcript JSON path is required.')
72
+ }
73
+ transcript = await pathPicker.pickExistingFile({
74
+ message: 'Select transcript JSON file',
75
+ })
76
+ }
77
+ let edited = resolveOptionalString(argv.edited)
78
+ if (!edited) {
79
+ if (!options.interactive || !pathPicker) {
80
+ throw new Error('Edited transcript path is required.')
81
+ }
82
+ edited = await pathPicker.pickExistingFile({
83
+ message: 'Select edited transcript text file',
84
+ })
85
+ }
86
+ const defaultOutputPath = buildEditedOutputPath(input)
87
+ const outputPath = resolveOptionalString(argv.output) ?? defaultOutputPath
88
+ const paddingMs = resolvePaddingMs(argv['padding-ms'])
89
+
90
+ return {
91
+ input,
92
+ transcript,
93
+ edited,
94
+ output: outputPath,
95
+ 'padding-ms': paddingMs,
96
+ }
97
+ }
98
+
99
+ export async function resolveCombineVideosArgs(
100
+ argv: Arguments,
101
+ options: CliUxOptions,
102
+ ): Promise<CombineVideosCommandArgs> {
103
+ const pathPicker = options.pathPicker
104
+ let video1 = resolveOptionalString(argv.video1)
105
+ if (!video1) {
106
+ if (!options.interactive || !pathPicker) {
107
+ throw new Error('First video path is required.')
108
+ }
109
+ video1 = await pathPicker.pickExistingFile({
110
+ message: 'Select first video',
111
+ })
112
+ }
113
+ let video2 = resolveOptionalString(argv.video2)
114
+ if (!video2) {
115
+ if (!options.interactive || !pathPicker) {
116
+ throw new Error('Second video path is required.')
117
+ }
118
+ video2 = await pathPicker.pickExistingFile({
119
+ message: 'Select second video',
120
+ })
121
+ }
122
+ let transcript1 = resolveOptionalString(argv.transcript1)
123
+ let transcript2 = resolveOptionalString(argv.transcript2)
124
+ const edited1 = resolveOptionalString(argv.edited1)
125
+ const edited2 = resolveOptionalString(argv.edited2)
126
+
127
+ if (edited1 && !transcript1) {
128
+ if (!options.interactive || !pathPicker) {
129
+ throw new Error('Transcript JSON is required for edited1.')
130
+ }
131
+ transcript1 = await pathPicker.pickExistingFile({
132
+ message: 'Select transcript JSON for first video',
133
+ })
134
+ }
135
+ if (edited2 && !transcript2) {
136
+ if (!options.interactive || !pathPicker) {
137
+ throw new Error('Transcript JSON is required for edited2.')
138
+ }
139
+ transcript2 = await pathPicker.pickExistingFile({
140
+ message: 'Select transcript JSON for second video',
141
+ })
142
+ }
143
+
144
+ let output = resolveOptionalString(argv.output)
145
+ if (!output) {
146
+ if (options.interactive && pathPicker) {
147
+ output = await pathPicker.pickOutputPath({
148
+ message: 'Select output video path',
149
+ defaultPath: buildCombinedOutputPath(video1, video2),
150
+ })
151
+ } else {
152
+ output = buildCombinedOutputPath(video1, video2)
153
+ }
154
+ }
155
+ const paddingMs = resolvePaddingMs(argv['padding-ms'])
156
+
157
+ return {
158
+ video1,
159
+ transcript1,
160
+ edited1,
161
+ video2,
162
+ transcript2,
163
+ edited2,
164
+ output,
165
+ 'padding-ms': paddingMs,
166
+ }
167
+ }
168
+
169
+ function resolvePaddingMs(value: unknown) {
170
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
171
+ return undefined
172
+ }
173
+ return value
174
+ }
175
+
176
+ export function createEditVideoHandler(options: CliUxOptions): CommandHandler {
177
+ return async (argv) => {
178
+ const args = await resolveEditVideoArgs(argv, options)
179
+ await withSpinner(
180
+ 'Editing video',
181
+ async () => {
182
+ const result = await editVideo({
183
+ inputPath: String(args.input),
184
+ transcriptJsonPath: String(args.transcript),
185
+ editedTextPath: String(args.edited),
186
+ outputPath: String(args.output),
187
+ paddingMs: args['padding-ms'],
188
+ })
189
+ if (!result.success) {
190
+ throw new Error(result.error ?? 'Edit failed.')
191
+ }
192
+ },
193
+ { successText: 'Edit complete' },
194
+ )
195
+ console.log(`Edited video written to ${args.output}`)
196
+ }
197
+ }
198
+
199
+ export function createCombineVideosHandler(
200
+ options: CliUxOptions,
201
+ ): CommandHandler {
202
+ return async (argv) => {
203
+ const args = await resolveCombineVideosArgs(argv, options)
204
+ let outputPath = ''
205
+ await withSpinner(
206
+ 'Combining videos',
207
+ async () => {
208
+ const result = await combineVideos({
209
+ video1Path: String(args.video1),
210
+ video1TranscriptJsonPath: args.transcript1,
211
+ video1EditedTextPath: args.edited1,
212
+ video2Path: String(args.video2),
213
+ video2TranscriptJsonPath: args.transcript2,
214
+ video2EditedTextPath: args.edited2,
215
+ outputPath: String(args.output),
216
+ overlapPaddingMs: args['padding-ms'],
217
+ })
218
+ if (!result.success) {
219
+ throw new Error(result.error ?? 'Combine failed.')
220
+ }
221
+ outputPath = result.outputPath
222
+ },
223
+ { successText: 'Combine complete' },
224
+ )
225
+ console.log(`Combined video written to ${outputPath}`)
226
+ }
227
+ }
228
+
27
229
  export function configureEditVideoCommand(command: Argv) {
28
230
  return command
29
231
  .option('input', {
30
232
  type: 'string',
31
- demandOption: true,
32
233
  describe: 'Input video file',
33
234
  })
34
235
  .option('transcript', {
35
236
  type: 'string',
36
- demandOption: true,
37
237
  describe: 'Transcript JSON path',
38
238
  })
39
239
  .option('edited', {
40
240
  type: 'string',
41
- demandOption: true,
42
241
  describe: 'Edited transcript text path',
43
242
  })
44
243
  .option('output', {
@@ -52,31 +251,14 @@ export function configureEditVideoCommand(command: Argv) {
52
251
  }
53
252
 
54
253
  export async function handleEditVideoCommand(argv: Arguments) {
55
- const args = argv as EditVideoCommandArgs
56
- const outputPath =
57
- typeof args.output === 'string' && args.output.trim().length > 0
58
- ? args.output
59
- : buildEditedOutputPath(String(args.input))
60
- const result = await editVideo({
61
- inputPath: String(args.input),
62
- transcriptJsonPath: String(args.transcript),
63
- editedTextPath: String(args.edited),
64
- outputPath,
65
- paddingMs:
66
- typeof args['padding-ms'] === 'number' ? args['padding-ms'] : undefined,
67
- })
68
- if (!result.success) {
69
- console.error(result.error ?? 'Edit failed.')
70
- process.exit(1)
71
- }
72
- console.log(`Edited video written to ${outputPath}`)
254
+ const options = createDefaultCliUxOptions()
255
+ await createEditVideoHandler(options)(argv)
73
256
  }
74
257
 
75
258
  export function configureCombineVideosCommand(command: Argv) {
76
259
  return command
77
260
  .option('video1', {
78
261
  type: 'string',
79
- demandOption: true,
80
262
  describe: 'First video path',
81
263
  })
82
264
  .option('transcript1', {
@@ -89,7 +271,6 @@ export function configureCombineVideosCommand(command: Argv) {
89
271
  })
90
272
  .option('video2', {
91
273
  type: 'string',
92
- demandOption: true,
93
274
  describe: 'Second video path',
94
275
  })
95
276
  .option('transcript2', {
@@ -102,7 +283,6 @@ export function configureCombineVideosCommand(command: Argv) {
102
283
  })
103
284
  .option('output', {
104
285
  type: 'string',
105
- demandOption: true,
106
286
  describe: 'Output video path',
107
287
  })
108
288
  .option('padding-ms', {
@@ -112,43 +292,47 @@ export function configureCombineVideosCommand(command: Argv) {
112
292
  }
113
293
 
114
294
  export async function handleCombineVideosCommand(argv: Arguments) {
115
- const args = argv as CombineVideosCommandArgs
116
- const result = await combineVideos({
117
- video1Path: String(args.video1),
118
- video1TranscriptJsonPath:
119
- typeof args.transcript1 === 'string' ? args.transcript1 : undefined,
120
- video1EditedTextPath:
121
- typeof args.edited1 === 'string' ? args.edited1 : undefined,
122
- video2Path: String(args.video2),
123
- video2TranscriptJsonPath:
124
- typeof args.transcript2 === 'string' ? args.transcript2 : undefined,
125
- video2EditedTextPath:
126
- typeof args.edited2 === 'string' ? args.edited2 : undefined,
127
- outputPath: String(args.output),
128
- overlapPaddingMs:
129
- typeof args['padding-ms'] === 'number' ? args['padding-ms'] : undefined,
130
- })
131
- if (!result.success) {
132
- console.error(result.error ?? 'Combine failed.')
133
- process.exit(1)
295
+ const options = createDefaultCliUxOptions()
296
+ await createCombineVideosHandler(options)(argv)
297
+ }
298
+
299
+ function createDefaultCliUxOptions(): CliUxOptions {
300
+ const interactive = isInteractive()
301
+ if (!interactive) {
302
+ return { interactive }
134
303
  }
135
- console.log(`Combined video written to ${result.outputPath}`)
304
+ const prompter = createInquirerPrompter()
305
+ return { interactive, pathPicker: createPathPicker(prompter) }
136
306
  }
137
307
 
138
- export async function runEditsCli() {
139
- const parser = yargs(hideBin(process.argv))
308
+ export async function runEditsCli(rawArgs = hideBin(process.argv)) {
309
+ const interactive = isInteractive()
310
+ const prompter = interactive ? createInquirerPrompter() : null
311
+ const pathPicker = prompter ? createPathPicker(prompter) : undefined
312
+ let args = rawArgs
313
+
314
+ if (interactive && args.length === 0 && prompter) {
315
+ const selection = await promptForEditsCommand(prompter)
316
+ if (!selection) {
317
+ return
318
+ }
319
+ args = selection
320
+ }
321
+
322
+ const handlerOptions: CliUxOptions = { interactive, pathPicker }
323
+ const parser = yargs(args)
140
324
  .scriptName('video-edits')
141
325
  .command(
142
326
  'edit-video',
143
327
  'Edit a single video using transcript text edits',
144
328
  configureEditVideoCommand as CommandBuilder,
145
- handleEditVideoCommand as CommandHandler,
329
+ createEditVideoHandler(handlerOptions),
146
330
  )
147
331
  .command(
148
332
  'combine-videos',
149
333
  'Combine two videos with speech-aligned padding',
150
334
  configureCombineVideosCommand as CommandBuilder,
151
- handleCombineVideosCommand as CommandHandler,
335
+ createCombineVideosHandler(handlerOptions),
152
336
  )
153
337
  .demandCommand(1)
154
338
  .strict()
@@ -159,9 +343,37 @@ export async function runEditsCli() {
159
343
 
160
344
  if (import.meta.main) {
161
345
  runEditsCli().catch((error) => {
346
+ if (error instanceof PromptCancelled) {
347
+ console.log('[info] Cancelled.')
348
+ return
349
+ }
162
350
  console.error(
163
351
  `[error] ${error instanceof Error ? error.message : String(error)}`,
164
352
  )
165
353
  process.exit(1)
166
354
  })
167
355
  }
356
+
357
+ async function promptForEditsCommand(
358
+ prompter: Prompter,
359
+ ): Promise<string[] | null> {
360
+ const selection = await prompter.select('Choose a command', [
361
+ {
362
+ name: 'Edit a single video using transcript text edits',
363
+ value: 'edit-video',
364
+ },
365
+ {
366
+ name: 'Combine two videos with speech-aligned padding',
367
+ value: 'combine-videos',
368
+ },
369
+ { name: 'Show help', value: 'help' },
370
+ { name: 'Exit', value: 'exit' },
371
+ ])
372
+ if (selection === 'exit') {
373
+ return null
374
+ }
375
+ if (selection === 'help') {
376
+ return ['--help']
377
+ }
378
+ return [selection]
379
+ }
@@ -1,16 +1,41 @@
1
1
  import { formatCommand } from '../utils'
2
2
  import { buildChapterLogPath } from './paths'
3
3
 
4
+ type LogHook = () => void
5
+
6
+ let beforeLogHook: LogHook | null = null
7
+ let afterLogHook: LogHook | null = null
8
+
9
+ export function setLogHooks(hooks: {
10
+ beforeLog?: LogHook
11
+ afterLog?: LogHook
12
+ }) {
13
+ beforeLogHook = hooks.beforeLog ?? null
14
+ afterLogHook = hooks.afterLog ?? null
15
+ }
16
+
17
+ function withLogHooks(callback: () => void) {
18
+ beforeLogHook?.()
19
+ callback()
20
+ afterLogHook?.()
21
+ }
22
+
4
23
  export function logCommand(command: string[]) {
5
- console.log(`[cmd] ${formatCommand(command)}`)
24
+ withLogHooks(() => {
25
+ console.log(`[cmd] ${formatCommand(command)}`)
26
+ })
6
27
  }
7
28
 
8
29
  export function logInfo(message: string) {
9
- console.log(`[info] ${message}`)
30
+ withLogHooks(() => {
31
+ console.log(`[info] ${message}`)
32
+ })
10
33
  }
11
34
 
12
35
  export function logWarn(message: string) {
13
- console.warn(`[warn] ${message}`)
36
+ withLogHooks(() => {
37
+ console.warn(`[warn] ${message}`)
38
+ })
14
39
  }
15
40
 
16
41
  export async function writeChapterLog(