eprec 0.0.1 → 1.1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -29
  3. package/app/assets/styles.css +129 -0
  4. package/app/client/app.tsx +37 -0
  5. package/app/client/counter.tsx +22 -0
  6. package/app/client/entry.tsx +8 -0
  7. package/app/components/layout.tsx +37 -0
  8. package/app/config/env.ts +31 -0
  9. package/app/config/import-map.ts +9 -0
  10. package/app/config/init-env.ts +3 -0
  11. package/app/config/routes.ts +5 -0
  12. package/app/helpers/render.ts +6 -0
  13. package/app/router.tsx +102 -0
  14. package/app/routes/index.tsx +50 -0
  15. package/app-server.ts +60 -0
  16. package/cli.ts +173 -0
  17. package/package.json +46 -7
  18. package/process-course/chapter-processor.ts +1037 -0
  19. package/process-course/cli.ts +236 -0
  20. package/process-course/config.ts +50 -0
  21. package/process-course/edits/cli.ts +167 -0
  22. package/process-course/edits/combined-video-editor.ts +316 -0
  23. package/process-course/edits/edit-workspace.ts +90 -0
  24. package/process-course/edits/index.ts +20 -0
  25. package/process-course/edits/regenerate-transcript.ts +84 -0
  26. package/process-course/edits/remove-ranges.test.ts +36 -0
  27. package/process-course/edits/remove-ranges.ts +287 -0
  28. package/process-course/edits/timestamp-refinement.test.ts +25 -0
  29. package/process-course/edits/timestamp-refinement.ts +172 -0
  30. package/process-course/edits/transcript-diff.test.ts +105 -0
  31. package/process-course/edits/transcript-diff.ts +214 -0
  32. package/process-course/edits/transcript-output.test.ts +50 -0
  33. package/process-course/edits/transcript-output.ts +36 -0
  34. package/process-course/edits/types.ts +26 -0
  35. package/process-course/edits/video-editor.ts +246 -0
  36. package/process-course/errors.test.ts +63 -0
  37. package/process-course/errors.ts +82 -0
  38. package/process-course/ffmpeg.ts +449 -0
  39. package/process-course/jarvis-commands/handlers.ts +71 -0
  40. package/process-course/jarvis-commands/index.ts +14 -0
  41. package/process-course/jarvis-commands/parser.test.ts +348 -0
  42. package/process-course/jarvis-commands/parser.ts +257 -0
  43. package/process-course/jarvis-commands/types.ts +46 -0
  44. package/process-course/jarvis-commands/windows.ts +254 -0
  45. package/process-course/logging.ts +24 -0
  46. package/process-course/paths.test.ts +59 -0
  47. package/process-course/paths.ts +53 -0
  48. package/process-course/summary.test.ts +209 -0
  49. package/process-course/summary.ts +210 -0
  50. package/process-course/types.ts +85 -0
  51. package/process-course/utils/audio-analysis.test.ts +348 -0
  52. package/process-course/utils/audio-analysis.ts +463 -0
  53. package/process-course/utils/chapter-selection.test.ts +307 -0
  54. package/process-course/utils/chapter-selection.ts +136 -0
  55. package/process-course/utils/file-utils.test.ts +83 -0
  56. package/process-course/utils/file-utils.ts +57 -0
  57. package/process-course/utils/filename.test.ts +27 -0
  58. package/process-course/utils/filename.ts +12 -0
  59. package/process-course/utils/time-ranges.test.ts +221 -0
  60. package/process-course/utils/time-ranges.ts +86 -0
  61. package/process-course/utils/transcript.test.ts +257 -0
  62. package/process-course/utils/transcript.ts +86 -0
  63. package/process-course/utils/video-editing.ts +44 -0
  64. package/process-course-video.ts +389 -0
  65. package/public/robots.txt +2 -0
  66. package/server/bundling.ts +210 -0
  67. package/speech-detection.ts +355 -0
  68. package/utils.ts +138 -0
  69. package/whispercpp-transcribe.ts +343 -0
package/app-server.ts ADDED
@@ -0,0 +1,60 @@
1
+ import './app/config/init-env.ts'
2
+
3
+ import getPort from 'get-port'
4
+ import { getEnv } from './app/config/env.ts'
5
+ import { createAppRouter } from './app/router.tsx'
6
+ import { createBundlingRoutes } from './server/bundling.ts'
7
+
8
+ type AppServerOptions = {
9
+ host?: string
10
+ port?: number
11
+ }
12
+
13
+ function startServer(port: number, hostname: string) {
14
+ const router = createAppRouter(import.meta.dirname)
15
+ return Bun.serve({
16
+ port,
17
+ hostname,
18
+ idleTimeout: 30,
19
+ routes: createBundlingRoutes(import.meta.dirname),
20
+ async fetch(request) {
21
+ try {
22
+ return await router.fetch(request)
23
+ } catch (error) {
24
+ console.error(error)
25
+ return new Response('Internal Server Error', { status: 500 })
26
+ }
27
+ },
28
+ })
29
+ }
30
+
31
+ async function getServerPort(nodeEnv: string, desiredPort: number) {
32
+ if (nodeEnv === 'production') {
33
+ return desiredPort
34
+ }
35
+ const port = await getPort({ port: desiredPort })
36
+ if (port !== desiredPort) {
37
+ console.warn(`⚠️ Port ${desiredPort} was taken, using port ${port} instead`)
38
+ }
39
+ return port
40
+ }
41
+
42
+ export async function startAppServer(options: AppServerOptions = {}) {
43
+ const env = getEnv()
44
+ const host = options.host ?? env.HOST
45
+ const desiredPort = options.port ?? env.PORT
46
+ const port = await getServerPort(env.NODE_ENV, desiredPort)
47
+ const server = startServer(port, host)
48
+ const hostname = server.hostname.includes(':')
49
+ ? `[${server.hostname}]`
50
+ : server.hostname
51
+ const url = `http://${hostname}:${server.port}`
52
+
53
+ console.log(`[app] running at ${url}`)
54
+
55
+ return { server, url }
56
+ }
57
+
58
+ if (import.meta.main) {
59
+ await startAppServer()
60
+ }
package/cli.ts ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bun
2
+ import path from 'node:path'
3
+ import type { CommandBuilder, CommandHandler } from 'yargs'
4
+ import yargs from 'yargs/yargs'
5
+ import { hideBin } from 'yargs/helpers'
6
+ import { startAppServer } from './app-server'
7
+ import { ensureFfmpegAvailable } from './process-course/ffmpeg'
8
+ import {
9
+ normalizeProcessArgs,
10
+ configureProcessCommand,
11
+ } from './process-course/cli'
12
+ import { runProcessCourse } from './process-course-video'
13
+ import {
14
+ configureEditVideoCommand,
15
+ configureCombineVideosCommand,
16
+ handleCombineVideosCommand,
17
+ handleEditVideoCommand,
18
+ } from './process-course/edits/cli'
19
+ import { detectSpeechSegmentsForFile } from './speech-detection'
20
+ import {
21
+ getDefaultWhisperModelPath,
22
+ transcribeAudio,
23
+ } from './whispercpp-transcribe'
24
+
25
+ function resolveOptionalString(value: unknown) {
26
+ if (typeof value !== 'string') {
27
+ return undefined
28
+ }
29
+ const trimmed = value.trim()
30
+ return trimmed.length > 0 ? trimmed : undefined
31
+ }
32
+
33
+ async function main() {
34
+ const parser = yargs(hideBin(process.argv))
35
+ .scriptName('eprec')
36
+ .command(
37
+ 'process <input...>',
38
+ 'Process chapters into separate files',
39
+ configureProcessCommand,
40
+ async (argv) => {
41
+ const args = normalizeProcessArgs(argv)
42
+ await runProcessCourse(args)
43
+ },
44
+ )
45
+ .command(
46
+ 'edit',
47
+ 'Edit a single video using transcript text edits',
48
+ configureEditVideoCommand as CommandBuilder,
49
+ handleEditVideoCommand as CommandHandler,
50
+ )
51
+ .command(
52
+ 'combine',
53
+ 'Combine two videos with speech-aligned padding',
54
+ configureCombineVideosCommand as CommandBuilder,
55
+ handleCombineVideosCommand as CommandHandler,
56
+ )
57
+ .command(
58
+ 'app start',
59
+ 'Start the web UI server',
60
+ (command) =>
61
+ command
62
+ .option('port', {
63
+ type: 'number',
64
+ describe: 'Port for the app server',
65
+ })
66
+ .option('host', {
67
+ type: 'string',
68
+ describe: 'Host to bind for the app server',
69
+ }),
70
+ async (argv) => {
71
+ const port =
72
+ typeof argv.port === 'number' && Number.isFinite(argv.port)
73
+ ? argv.port
74
+ : undefined
75
+ const host = resolveOptionalString(argv.host)
76
+ await startAppServer({ port, host })
77
+ },
78
+ )
79
+ .command(
80
+ 'transcribe <input>',
81
+ 'Transcribe a single audio/video file',
82
+ (command) =>
83
+ command
84
+ .positional('input', {
85
+ type: 'string',
86
+ describe: 'Input audio/video file',
87
+ })
88
+ .option('model-path', {
89
+ type: 'string',
90
+ describe: 'Path to whisper.cpp model file',
91
+ default: getDefaultWhisperModelPath(),
92
+ })
93
+ .option('language', {
94
+ type: 'string',
95
+ describe: 'Language passed to whisper.cpp',
96
+ default: 'en',
97
+ })
98
+ .option('threads', {
99
+ type: 'number',
100
+ describe: 'Thread count for whisper.cpp',
101
+ })
102
+ .option('binary-path', {
103
+ type: 'string',
104
+ describe: 'Path to whisper.cpp CLI (whisper-cli)',
105
+ })
106
+ .option('output-base', {
107
+ type: 'string',
108
+ describe: 'Output base path (without extension)',
109
+ }),
110
+ async (argv) => {
111
+ const inputPath = path.resolve(String(argv.input))
112
+ const outputBasePath =
113
+ resolveOptionalString(argv['output-base']) ??
114
+ path.join(
115
+ path.dirname(inputPath),
116
+ `${path.parse(inputPath).name}-transcript`,
117
+ )
118
+ const threads =
119
+ typeof argv.threads === 'number' && Number.isFinite(argv.threads)
120
+ ? argv.threads
121
+ : undefined
122
+ const result = await transcribeAudio(inputPath, {
123
+ modelPath: resolveOptionalString(argv['model-path']),
124
+ language: resolveOptionalString(argv.language),
125
+ threads,
126
+ binaryPath: resolveOptionalString(argv['binary-path']),
127
+ outputBasePath,
128
+ })
129
+ console.log(`Transcript written to ${outputBasePath}.txt`)
130
+ console.log(`Segments written to ${outputBasePath}.json`)
131
+ console.log(result.text)
132
+ },
133
+ )
134
+ .command(
135
+ 'detect-speech <input>',
136
+ 'Show detected speech segments for a file',
137
+ (command) =>
138
+ command
139
+ .positional('input', {
140
+ type: 'string',
141
+ describe: 'Input audio/video file',
142
+ })
143
+ .option('start', {
144
+ type: 'number',
145
+ describe: 'Start time in seconds',
146
+ })
147
+ .option('end', {
148
+ type: 'number',
149
+ describe: 'End time in seconds',
150
+ }),
151
+ async (argv) => {
152
+ await ensureFfmpegAvailable()
153
+ const segments = await detectSpeechSegmentsForFile({
154
+ inputPath: String(argv.input),
155
+ start: typeof argv.start === 'number' ? argv.start : undefined,
156
+ end: typeof argv.end === 'number' ? argv.end : undefined,
157
+ })
158
+ console.log(JSON.stringify(segments, null, 2))
159
+ },
160
+ )
161
+ .demandCommand(1)
162
+ .strict()
163
+ .help()
164
+
165
+ await parser.parseAsync()
166
+ }
167
+
168
+ main().catch((error) => {
169
+ console.error(
170
+ `[error] ${error instanceof Error ? error.message : String(error)}`,
171
+ )
172
+ process.exit(1)
173
+ })
package/package.json CHANGED
@@ -1,10 +1,49 @@
1
1
  {
2
2
  "name": "eprec",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for eprec",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "1.1.0",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/epicweb-dev/eprec"
9
+ },
10
+ "scripts": {
11
+ "app:start": "bun ./app-server.ts",
12
+ "format": "prettier --write .",
13
+ "test": "bun test process-course utils.test.ts",
14
+ "test:e2e": "bun test e2e",
15
+ "test:all": "bun test",
16
+ "validate": "bun run test"
17
+ },
18
+ "bin": {
19
+ "eprec": "./cli.ts"
20
+ },
21
+ "files": [
22
+ "app/**",
23
+ "app-server.ts",
24
+ "cli.ts",
25
+ "process-course/**",
26
+ "process-course-video.ts",
27
+ "public/**",
28
+ "server/**",
29
+ "speech-detection.ts",
30
+ "utils.ts",
31
+ "whispercpp-transcribe.ts"
32
+ ],
33
+ "prettier": "@epic-web/config/prettier",
34
+ "devDependencies": {
35
+ "@epic-web/config": "^1.21.3",
36
+ "@types/bun": "latest",
37
+ "@types/yargs": "^17.0.35",
38
+ "prettier": "^3.8.1"
39
+ },
40
+ "peerDependencies": {
41
+ "typescript": "^5"
42
+ },
43
+ "dependencies": {
44
+ "get-port": "^7.1.0",
45
+ "onnxruntime-node": "^1.23.2",
46
+ "remix": "3.0.0-alpha.0",
47
+ "yargs": "^18.0.0"
48
+ }
10
49
  }