eprec 1.4.0 → 1.6.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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/app/assets/styles.css +319 -182
  3. package/app/components/style-system-sample.tsx +197 -0
  4. package/app/styles/tokens.ts +99 -0
  5. package/package.json +8 -11
  6. package/process-course/chapter-processor.ts +6 -3
  7. package/process-course/cli.ts +1 -1
  8. package/process-course/edits/cli.ts +13 -5
  9. package/process-course/edits/combined-video-editor.ts +2 -2
  10. package/process-course/edits/edit-workspace.ts +1 -1
  11. package/process-course/edits/regenerate-transcript.ts +2 -2
  12. package/process-course/edits/remove-ranges.ts +1 -1
  13. package/process-course/edits/timestamp-refinement.ts +1 -1
  14. package/process-course/edits/transcript-output.test.ts +1 -1
  15. package/process-course/edits/transcript-output.ts +1 -1
  16. package/process-course/ffmpeg.ts +1 -1
  17. package/process-course/jarvis-commands/parser.test.ts +1 -1
  18. package/process-course/jarvis-commands/parser.ts +1 -1
  19. package/process-course/jarvis-commands/windows.ts +3 -3
  20. package/process-course/logging.ts +1 -1
  21. package/process-course/summary.ts +1 -1
  22. package/process-course/utils/filename.ts +1 -1
  23. package/process-course/utils/transcript.test.ts +1 -1
  24. package/process-course/utils/transcript.ts +1 -1
  25. package/{app-server.ts → src/app-server.ts} +13 -9
  26. package/{cli.ts → src/cli.ts} +30 -14
  27. package/{process-course-video.ts → src/process-course-video.ts} +9 -9
  28. package/{speech-detection.ts → src/speech-detection.ts} +4 -4
  29. package/src/utils.test.ts +71 -0
  30. /package/{utils.ts → src/utils.ts} +0 -0
  31. /package/{whispercpp-transcribe.ts → src/whispercpp-transcribe.ts} +0 -0
@@ -4,20 +4,20 @@ import type { 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'
7
- import { setLogHooks } from './process-course/logging'
8
- import { ensureFfmpegAvailable } from './process-course/ffmpeg'
7
+ import { setLogHooks } from '../process-course/logging'
8
+ import { ensureFfmpegAvailable } from '../process-course/ffmpeg'
9
9
  import {
10
10
  VIDEO_EXTENSIONS,
11
11
  normalizeProcessArgs,
12
12
  configureProcessCommand,
13
- } from './process-course/cli'
13
+ } from '../process-course/cli'
14
14
  import { runProcessCourse } from './process-course-video'
15
15
  import {
16
16
  configureEditVideoCommand,
17
17
  configureCombineVideosCommand,
18
18
  createCombineVideosHandler,
19
19
  createEditVideoHandler,
20
- } from './process-course/edits/cli'
20
+ } from '../process-course/edits/cli'
21
21
  import { detectSpeechSegmentsForFile } from './speech-detection'
22
22
  import {
23
23
  getDefaultWhisperModelPath,
@@ -34,7 +34,7 @@ import {
34
34
  type PathPicker,
35
35
  type Prompter,
36
36
  withSpinner,
37
- } from './cli-ux'
37
+ } from '../cli-ux'
38
38
 
39
39
  type CliUxContext = {
40
40
  interactive: boolean
@@ -235,33 +235,49 @@ function createCliUxContext(): CliUxContext {
235
235
  }
236
236
 
237
237
  async function promptForCommand(prompter: Prompter): Promise<string[] | null> {
238
- const selection = await prompter.select('Choose a command', [
238
+ const selection = await prompter.search('Choose a command (type to filter)', [
239
239
  {
240
- name: 'Process chapters into separate files',
240
+ name: 'process - Process chapters into separate files',
241
241
  value: 'process',
242
+ description: 'process [input...]',
243
+ keywords: ['chapters', 'course', 'split', 'export'],
242
244
  },
243
245
  {
244
- name: 'Edit a single video using transcript text edits',
246
+ name: 'edit - Edit a single video using transcript text edits',
245
247
  value: 'edit',
248
+ description: 'edit --input <file> --transcript <json> --edited <txt>',
249
+ keywords: ['transcript', 'cuts', 'remove', 'trim'],
246
250
  },
247
251
  {
248
- name: 'Combine two videos with speech-aligned padding',
252
+ name: 'combine - Combine two videos with speech-aligned padding',
249
253
  value: 'combine',
254
+ description: 'combine --video1 <file> --video2 <file>',
255
+ keywords: ['merge', 'join', 'splice', 'padding'],
250
256
  },
251
257
  {
252
- name: 'Start the web UI server',
258
+ name: 'app start - Start the web UI server',
253
259
  value: 'app-start',
260
+ description: 'app start --port <number> --host <host>',
261
+ keywords: ['app', 'ui', 'server', 'web', 'dashboard'],
254
262
  },
255
263
  {
256
- name: 'Transcribe a single audio/video file',
264
+ name: 'transcribe - Transcribe a single audio/video file',
257
265
  value: 'transcribe',
266
+ description: 'transcribe [input]',
267
+ keywords: ['whisper', 'speech', 'audio', 'subtitles'],
258
268
  },
259
269
  {
260
- name: 'Show detected speech segments for a file',
270
+ name: 'detect-speech - Show detected speech segments for a file',
261
271
  value: 'detect-speech',
272
+ description: 'detect-speech [input]',
273
+ keywords: ['speech', 'vad', 'silence', 'segments'],
262
274
  },
263
- { name: 'Show help', value: 'help' },
264
- { name: 'Exit', value: 'exit' },
275
+ {
276
+ name: '--help - Show help',
277
+ value: 'help',
278
+ keywords: ['usage', '--help'],
279
+ },
280
+ { name: 'exit - Exit', value: 'exit', keywords: ['quit', 'cancel'] },
265
281
  ])
266
282
  switch (selection) {
267
283
  case 'exit':
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env bun
2
2
  import path from 'node:path'
3
3
  import { mkdir } from 'node:fs/promises'
4
- import { ensureFfmpegAvailable, getChapters } from './process-course/ffmpeg'
5
- import { logInfo } from './process-course/logging'
6
- import { parseCliArgs, type CliArgs } from './process-course/cli'
7
- import { resolveChapterSelection } from './process-course/utils/chapter-selection'
8
- import { removeDirIfEmpty } from './process-course/utils/file-utils'
9
- import { writeJarvisLogs, writeSummaryLogs } from './process-course/summary'
4
+ import { ensureFfmpegAvailable, getChapters } from '../process-course/ffmpeg'
5
+ import { logInfo } from '../process-course/logging'
6
+ import { parseCliArgs, type CliArgs } from '../process-course/cli'
7
+ import { resolveChapterSelection } from '../process-course/utils/chapter-selection'
8
+ import { removeDirIfEmpty } from '../process-course/utils/file-utils'
9
+ import { writeJarvisLogs, writeSummaryLogs } from '../process-course/summary'
10
10
  import {
11
11
  processChapter,
12
12
  type ChapterProcessingOptions,
13
- } from './process-course/chapter-processor'
13
+ } from '../process-course/chapter-processor'
14
14
  import type {
15
15
  JarvisEdit,
16
16
  JarvisNote,
17
17
  JarvisWarning,
18
18
  ProcessedChapterInfo,
19
19
  EditWorkspaceInfo,
20
- } from './process-course/types'
20
+ } from '../process-course/types'
21
21
  import { formatSeconds } from './utils'
22
22
  import { checkSegmentHasSpeech } from './speech-detection'
23
23
 
@@ -103,7 +103,7 @@ async function processInputFile(options: {
103
103
  dryRun: boolean
104
104
  keepIntermediates: boolean
105
105
  writeLogs: boolean
106
- chapterSelection: import('./process-course/types').ChapterSelection | null
106
+ chapterSelection: import('../process-course/types').ChapterSelection | null
107
107
  enableTranscription: boolean
108
108
  whisperModelPath: string
109
109
  whisperLanguage: string
@@ -1,11 +1,11 @@
1
1
  import path from 'node:path'
2
2
  import { mkdir } from 'node:fs/promises'
3
3
  import * as ort from 'onnxruntime-node'
4
- import { readAudioSamples } from './process-course/ffmpeg'
5
- import { CONFIG } from './process-course/config'
4
+ import { readAudioSamples } from '../process-course/ffmpeg'
5
+ import { CONFIG } from '../process-course/config'
6
6
  import { formatSeconds, getMediaDurationSeconds } from './utils'
7
- import { speechFallback } from './process-course/utils/audio-analysis'
8
- import type { SpeechBounds } from './process-course/types'
7
+ import { speechFallback } from '../process-course/utils/audio-analysis'
8
+ import type { SpeechBounds } from '../process-course/types'
9
9
 
10
10
  export type VadConfig = {
11
11
  vadWindowSamples: number
@@ -0,0 +1,71 @@
1
+ import { test, expect } from 'bun:test'
2
+ import {
3
+ clamp,
4
+ formatCommand,
5
+ formatSeconds,
6
+ normalizeFilename,
7
+ runCommand,
8
+ toKebabCase,
9
+ } from './utils'
10
+
11
+ test('formatCommand quotes parts that include spaces', () => {
12
+ expect(formatCommand(['ffmpeg', '-i', 'my file.mp4'])).toBe(
13
+ 'ffmpeg -i "my file.mp4"',
14
+ )
15
+ })
16
+
17
+ test('formatCommand keeps parts without spaces unchanged', () => {
18
+ expect(formatCommand(['echo', 'hello'])).toBe('echo hello')
19
+ })
20
+
21
+ test('formatSeconds formats to two decimals with suffix', () => {
22
+ expect(formatSeconds(1)).toBe('1.00s')
23
+ expect(formatSeconds(1.234)).toBe('1.23s')
24
+ })
25
+
26
+ test('clamp keeps values within range', () => {
27
+ expect(clamp(5, 0, 10)).toBe(5)
28
+ })
29
+
30
+ test('clamp enforces minimum bound', () => {
31
+ expect(clamp(-2, 0, 10)).toBe(0)
32
+ })
33
+
34
+ test('clamp enforces maximum bound', () => {
35
+ expect(clamp(12, 0, 10)).toBe(10)
36
+ })
37
+
38
+ test('toKebabCase trims, lowercases, and removes punctuation', () => {
39
+ expect(toKebabCase('Hello, World!')).toBe('hello-world')
40
+ })
41
+
42
+ test('toKebabCase collapses repeated separators', () => {
43
+ expect(toKebabCase(' React Hooks ')).toBe('react-hooks')
44
+ })
45
+
46
+ test('toKebabCase returns untitled for empty input', () => {
47
+ expect(toKebabCase(' ')).toBe('untitled')
48
+ })
49
+
50
+ test('normalizeFilename converts number words and dots', () => {
51
+ expect(normalizeFilename('Lesson One point Five')).toBe('lesson 01.05')
52
+ })
53
+
54
+ test('normalizeFilename trims and lowercases', () => {
55
+ expect(normalizeFilename(' Intro ')).toBe('intro')
56
+ })
57
+
58
+ test('runCommand captures stdout for successful command', async () => {
59
+ const result = await runCommand(['echo', 'hello'])
60
+ expect(result.exitCode).toBe(0)
61
+ expect(result.stdout.trim()).toBe('hello')
62
+ })
63
+
64
+ test('runCommand throws on non-zero exit without allowFailure', async () => {
65
+ await expect(runCommand(['false'])).rejects.toThrow('Command failed')
66
+ })
67
+
68
+ test('runCommand returns exit code when allowFailure is true', async () => {
69
+ const result = await runCommand(['false'], { allowFailure: true })
70
+ expect(result.exitCode).toBe(1)
71
+ })
File without changes