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
|
@@ -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:
|
|
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
|