eprec 1.6.0 → 1.7.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eprec",
3
3
  "type": "module",
4
- "version": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -54,6 +54,14 @@ import type {
54
54
  } from './types'
55
55
  import { createEditWorkspace } from './edits'
56
56
 
57
+ export type ChapterProgressReporter = {
58
+ start: (options: { stepCount: number; label?: string }) => void
59
+ step: (label: string) => void
60
+ setLabel: (label: string) => void
61
+ finish: (label?: string) => void
62
+ skip: (label: string) => void
63
+ }
64
+
57
65
  export interface ChapterProcessingOptions {
58
66
  inputPath: string
59
67
  outputDir: string
@@ -67,6 +75,7 @@ export interface ChapterProcessingOptions {
67
75
  writeLogs: boolean
68
76
  dryRun: boolean
69
77
  previousProcessedChapter?: ProcessedChapterInfo | null
78
+ progress?: ChapterProgressReporter
70
79
  }
71
80
 
72
81
  export interface ChapterProcessingResult {
@@ -100,11 +109,16 @@ export async function processChapter(
100
109
  )
101
110
  }
102
111
 
112
+ const progress = options.progress
113
+ const stepCount = options.dryRun ? 1 : options.enableTranscription ? 8 : 7
114
+
103
115
  const outputBasePath = path.join(
104
116
  options.outputDir,
105
117
  `${formatChapterFilename(chapter)}${path.extname(options.inputPath)}`,
106
118
  )
107
119
 
120
+ progress?.start({ stepCount, label: 'Starting' })
121
+
108
122
  // Check minimum duration before processing
109
123
  if (duration < options.minChapterDurationSeconds) {
110
124
  logInfo(
@@ -121,6 +135,7 @@ export async function processChapter(
121
135
  ])
122
136
  logWritten = true
123
137
  }
138
+ progress?.skip('Skipped (short)')
124
139
  return { status: 'skipped', skipReason: 'short-initial', logWritten }
125
140
  }
126
141
 
@@ -129,6 +144,7 @@ export async function processChapter(
129
144
  logInfo(
130
145
  `[dry-run] Would process chapter ${chapter.index + 1}: ${chapter.title}`,
131
146
  )
147
+ progress?.finish('Dry run')
132
148
  return { status: 'processed', skipReason: 'dry-run', logWritten: false }
133
149
  }
134
150
 
@@ -139,6 +155,7 @@ export async function processChapter(
139
155
 
140
156
  try {
141
157
  // Step 1: Extract raw segment with padding trimmed
158
+ progress?.step('Extracting segment')
142
159
  const rawTrimStart = chapter.start + CONFIG.rawTrimPaddingSeconds
143
160
  const rawTrimEnd = chapter.end - CONFIG.rawTrimPaddingSeconds
144
161
  const rawDuration = rawTrimEnd - rawTrimStart
@@ -156,6 +173,7 @@ export async function processChapter(
156
173
  })
157
174
 
158
175
  // Step 2: Normalize audio
176
+ progress?.step('Normalizing audio')
159
177
  const analysis = await analyzeLoudness(paths.rawPath, 0, rawDuration)
160
178
  await renderChapter({
161
179
  inputPath: paths.rawPath,
@@ -170,8 +188,10 @@ export async function processChapter(
170
188
  let commandFilenameOverride: string | null = null
171
189
  let hasEditCommand = false
172
190
  let commandNotes: Array<{ value: string; window: TimeRange }> = []
191
+ let usedSpliceStep = false
173
192
 
174
193
  if (options.enableTranscription) {
194
+ progress?.step('Transcribing audio')
175
195
  const transcriptionResult = await transcribeAndAnalyze({
176
196
  normalizedPath: paths.normalizedPath,
177
197
  transcriptionAudioPath: paths.transcriptionAudioPath,
@@ -192,6 +212,7 @@ export async function processChapter(
192
212
  logWritten = true
193
213
  }
194
214
  await safeUnlink(outputBasePath)
215
+ progress?.skip('Skipped (transcript)')
195
216
  return {
196
217
  status: 'skipped',
197
218
  skipReason: transcriptionResult.hasBadTake
@@ -213,6 +234,8 @@ export async function processChapter(
213
234
  `Combine previous command detected for chapter ${chapter.index + 1}, but no previous chapter available. Processing normally.`,
214
235
  )
215
236
  } else {
237
+ progress?.step('Combining previous')
238
+ usedSpliceStep = true
216
239
  const combineResult = await handleCombinePrevious({
217
240
  chapter,
218
241
  previousProcessedChapter: options.previousProcessedChapter,
@@ -227,8 +250,10 @@ export async function processChapter(
227
250
  })
228
251
  // If combine failed (returned null), continue with normal processing
229
252
  if (combineResult !== null) {
253
+ progress?.finish('Combined')
230
254
  return combineResult
231
255
  }
256
+ progress?.setLabel('Splicing commands')
232
257
  // Otherwise, fall through to normal processing
233
258
  }
234
259
  }
@@ -242,6 +267,9 @@ export async function processChapter(
242
267
  )
243
268
 
244
269
  // Step 5: Handle command splicing
270
+ if (!usedSpliceStep) {
271
+ progress?.step('Splicing commands')
272
+ }
245
273
  const spliceResult = await handleCommandSplicing({
246
274
  commandWindows,
247
275
  normalizedPath: paths.normalizedPath,
@@ -252,6 +280,7 @@ export async function processChapter(
252
280
  })
253
281
 
254
282
  // Step 6: Detect speech bounds
283
+ progress?.step('Detecting speech')
255
284
  const speechBounds = await detectSpeechBounds(
256
285
  spliceResult.sourcePath,
257
286
  0,
@@ -275,6 +304,7 @@ export async function processChapter(
275
304
  }
276
305
 
277
306
  // Step 7: Apply speech padding
307
+ progress?.step('Trimming')
278
308
  const paddedStart = clamp(
279
309
  speechBounds.start - CONFIG.preSpeechPaddingSeconds,
280
310
  0,
@@ -314,10 +344,12 @@ export async function processChapter(
314
344
  logWritten = true
315
345
  }
316
346
  await safeUnlink(outputBasePath)
347
+ progress?.skip('Skipped (trimmed)')
317
348
  return { status: 'skipped', skipReason: 'short-trimmed', logWritten }
318
349
  }
319
350
 
320
351
  // Step 9: Write final output
352
+ progress?.step('Writing output')
321
353
  await extractChapterSegment({
322
354
  inputPath: spliceResult.sourcePath,
323
355
  outputPath: finalOutputPath,
@@ -326,6 +358,7 @@ export async function processChapter(
326
358
  })
327
359
 
328
360
  // Step 10: Verify no jarvis in final output
361
+ progress?.step('Verifying output')
329
362
  let jarvisWarning: JarvisWarning | undefined
330
363
  await extractTranscriptionAudio({
331
364
  inputPath: finalOutputPath,
@@ -412,6 +445,7 @@ export async function processChapter(
412
445
  processedDuration: trimmedDuration,
413
446
  }
414
447
 
448
+ progress?.finish('Complete')
415
449
  return {
416
450
  status: 'processed',
417
451
  jarvisWarning,
@@ -361,7 +361,8 @@ async function promptForEditsCommand(
361
361
  {
362
362
  name: 'edit-video - Edit a single video using transcript text edits',
363
363
  value: 'edit-video',
364
- description: 'edit-video --input <file> --transcript <json> --edited <txt>',
364
+ description:
365
+ 'edit-video --input <file> --transcript <json> --edited <txt>',
365
366
  keywords: ['transcript', 'cuts', 'remove', 'trim'],
366
367
  },
367
368
  {
@@ -10,6 +10,7 @@ import { writeJarvisLogs, writeSummaryLogs } from '../process-course/summary'
10
10
  import {
11
11
  processChapter,
12
12
  type ChapterProcessingOptions,
13
+ type ChapterProgressReporter,
13
14
  } from '../process-course/chapter-processor'
14
15
  import type {
15
16
  JarvisEdit,
@@ -20,6 +21,7 @@ import type {
20
21
  } from '../process-course/types'
21
22
  import { formatSeconds } from './utils'
22
23
  import { checkSegmentHasSpeech } from './speech-detection'
24
+ import { setActiveSpinnerText } from '../cli-ux'
23
25
 
24
26
  interface ProcessingSummary {
25
27
  totalSelected: number
@@ -35,6 +37,137 @@ interface ProcessingSummary {
35
37
 
36
38
  export type ProcessCourseOptions = Omit<CliArgs, 'shouldExit'>
37
39
 
40
+ const PROGRESS_BAR_WIDTH = 12
41
+
42
+ type SpinnerProgressContext = {
43
+ fileIndex: number
44
+ fileCount: number
45
+ fileName: string
46
+ chapterCount: number
47
+ }
48
+
49
+ type ChapterProgressContext = {
50
+ chapterIndex: number
51
+ chapterTitle: string
52
+ }
53
+
54
+ function clampProgress(value: number) {
55
+ return Math.max(0, Math.min(1, value))
56
+ }
57
+
58
+ function formatPercent(value: number) {
59
+ return `${Math.round(clampProgress(value) * 100)}%`
60
+ }
61
+
62
+ function formatProgressBar(value: number, width = PROGRESS_BAR_WIDTH) {
63
+ const clamped = clampProgress(value)
64
+ const filled = Math.round(clamped * width)
65
+ return `[${'#'.repeat(filled)}${'-'.repeat(width - filled)}]`
66
+ }
67
+
68
+ function truncateLabel(value: string, maxLength: number) {
69
+ const trimmed = value.trim()
70
+ if (trimmed.length <= maxLength) {
71
+ return trimmed
72
+ }
73
+ return `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`
74
+ }
75
+
76
+ function buildProgressText(params: {
77
+ fileIndex: number
78
+ fileCount: number
79
+ fileName: string
80
+ chapterIndex: number
81
+ chapterCount: number
82
+ chapterTitle: string
83
+ stepIndex: number
84
+ stepCount: number
85
+ stepLabel: string
86
+ }) {
87
+ const chapterProgress =
88
+ params.stepCount > 0 ? params.stepIndex / params.stepCount : 0
89
+ const fileProgress =
90
+ params.chapterCount > 0
91
+ ? (params.chapterIndex - 1 + chapterProgress) / params.chapterCount
92
+ : 1
93
+ const fileLabel =
94
+ params.fileCount > 1
95
+ ? `File ${params.fileIndex}/${params.fileCount}`
96
+ : 'File'
97
+ const fileName = truncateLabel(params.fileName, 22)
98
+ const fileSegment = fileName ? `${fileLabel} ${fileName}` : fileLabel
99
+ const chapterLabel = `Chapter ${params.chapterIndex}/${params.chapterCount}`
100
+ const chapterTitle = truncateLabel(params.chapterTitle, 26)
101
+ const chapterSegment = chapterTitle
102
+ ? `${chapterLabel} ${chapterTitle}`
103
+ : chapterLabel
104
+ const stepSegment = truncateLabel(params.stepLabel, 28) || 'Working'
105
+ return `Processing course | ${fileSegment} ${formatPercent(fileProgress)} ${formatProgressBar(fileProgress)} | ${chapterSegment} ${formatPercent(chapterProgress)} ${formatProgressBar(chapterProgress)} | ${stepSegment}`
106
+ }
107
+
108
+ function createSpinnerProgressReporter(context: SpinnerProgressContext) {
109
+ const chapterCount = Math.max(1, context.chapterCount)
110
+ return {
111
+ createChapterProgress({ chapterIndex, chapterTitle }: ChapterProgressContext) {
112
+ let stepIndex = 0
113
+ let stepCount = 1
114
+ let stepLabel = 'Starting'
115
+
116
+ const normalizeStepCount = (value: number) =>
117
+ Math.max(1, Math.round(value))
118
+
119
+ const update = () => {
120
+ setActiveSpinnerText(
121
+ buildProgressText({
122
+ fileIndex: context.fileIndex,
123
+ fileCount: context.fileCount,
124
+ fileName: context.fileName,
125
+ chapterIndex,
126
+ chapterCount,
127
+ chapterTitle,
128
+ stepIndex,
129
+ stepCount,
130
+ stepLabel,
131
+ }),
132
+ )
133
+ }
134
+
135
+ const progress: ChapterProgressReporter = {
136
+ start({ stepCount: initialCount, label }) {
137
+ stepCount = normalizeStepCount(initialCount)
138
+ stepIndex = 0
139
+ stepLabel = label ?? 'Starting'
140
+ update()
141
+ },
142
+ step(label) {
143
+ stepCount = normalizeStepCount(stepCount)
144
+ stepIndex = Math.min(stepIndex + 1, stepCount)
145
+ stepLabel = label
146
+ update()
147
+ },
148
+ setLabel(label) {
149
+ stepLabel = label
150
+ update()
151
+ },
152
+ finish(label) {
153
+ stepCount = normalizeStepCount(stepCount)
154
+ stepIndex = stepCount
155
+ stepLabel = label ?? 'Complete'
156
+ update()
157
+ },
158
+ skip(label) {
159
+ stepCount = normalizeStepCount(stepCount)
160
+ stepIndex = stepCount
161
+ stepLabel = label
162
+ update()
163
+ },
164
+ }
165
+
166
+ return progress
167
+ },
168
+ }
169
+ }
170
+
38
171
  export async function runProcessCourse(options: ProcessCourseOptions) {
39
172
  const {
40
173
  inputPaths,
@@ -53,7 +186,7 @@ export async function runProcessCourse(options: ProcessCourseOptions) {
53
186
  await ensureFfmpegAvailable()
54
187
 
55
188
  // Process each input file in turn
56
- for (const inputPath of inputPaths) {
189
+ for (const [fileIndex, inputPath] of inputPaths.entries()) {
57
190
  // Determine output directory for this file
58
191
  let fileOutputDir: string
59
192
  if (outputDir) {
@@ -72,6 +205,8 @@ export async function runProcessCourse(options: ProcessCourseOptions) {
72
205
  }
73
206
 
74
207
  await processInputFile({
208
+ fileIndex: fileIndex + 1,
209
+ fileCount: inputPaths.length,
75
210
  inputPath,
76
211
  outputDir: fileOutputDir,
77
212
  minChapterDurationSeconds,
@@ -97,6 +232,8 @@ export async function runProcessCourseCli(rawArgs?: string[]) {
97
232
  }
98
233
 
99
234
  async function processInputFile(options: {
235
+ fileIndex: number
236
+ fileCount: number
100
237
  inputPath: string
101
238
  outputDir: string
102
239
  minChapterDurationSeconds: number
@@ -110,6 +247,8 @@ async function processInputFile(options: {
110
247
  whisperBinaryPath: string | undefined
111
248
  }) {
112
249
  const {
250
+ fileIndex,
251
+ fileCount,
113
252
  inputPath,
114
253
  outputDir,
115
254
  minChapterDurationSeconds,
@@ -168,6 +307,13 @@ async function processInputFile(options: {
168
307
  ? chapters.filter((chapter) => chapterIndexes.includes(chapter.index))
169
308
  : chapters
170
309
 
310
+ const progressReporter = createSpinnerProgressReporter({
311
+ fileIndex,
312
+ fileCount,
313
+ fileName: path.basename(inputPath),
314
+ chapterCount: selectedChapters.length,
315
+ })
316
+
171
317
  const summary: ProcessingSummary = {
172
318
  totalSelected: selectedChapters.length,
173
319
  processed: 0,
@@ -203,7 +349,11 @@ async function processInputFile(options: {
203
349
  const processedChaptersWithSpeech: ProcessedChapterInfo[] = []
204
350
  let previousProcessedChapter: ProcessedChapterInfo | null = null
205
351
 
206
- for (const chapter of selectedChapters) {
352
+ for (const [chapterOffset, chapter] of selectedChapters.entries()) {
353
+ const chapterProgress = progressReporter.createChapterProgress({
354
+ chapterIndex: chapterOffset + 1,
355
+ chapterTitle: chapter.title,
356
+ })
207
357
  // Determine which chapter to combine with
208
358
  // Always use the most recent processed chapter with speech (if any)
209
359
  const chapterToCombineWith: ProcessedChapterInfo | null =
@@ -222,6 +372,7 @@ async function processInputFile(options: {
222
372
  const result = await processChapter(chapter, {
223
373
  ...processingOptions,
224
374
  previousProcessedChapter: chapterToCombineWith,
375
+ progress: chapterProgress,
225
376
  })
226
377
 
227
378
  // Update summary based on result