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.
- package/LICENSE +21 -0
- package/README.md +122 -29
- package/app/assets/styles.css +129 -0
- package/app/client/app.tsx +37 -0
- package/app/client/counter.tsx +22 -0
- package/app/client/entry.tsx +8 -0
- package/app/components/layout.tsx +37 -0
- package/app/config/env.ts +31 -0
- package/app/config/import-map.ts +9 -0
- package/app/config/init-env.ts +3 -0
- package/app/config/routes.ts +5 -0
- package/app/helpers/render.ts +6 -0
- package/app/router.tsx +102 -0
- package/app/routes/index.tsx +50 -0
- package/app-server.ts +60 -0
- package/cli.ts +173 -0
- package/package.json +46 -7
- package/process-course/chapter-processor.ts +1037 -0
- package/process-course/cli.ts +236 -0
- package/process-course/config.ts +50 -0
- package/process-course/edits/cli.ts +167 -0
- package/process-course/edits/combined-video-editor.ts +316 -0
- package/process-course/edits/edit-workspace.ts +90 -0
- package/process-course/edits/index.ts +20 -0
- package/process-course/edits/regenerate-transcript.ts +84 -0
- package/process-course/edits/remove-ranges.test.ts +36 -0
- package/process-course/edits/remove-ranges.ts +287 -0
- package/process-course/edits/timestamp-refinement.test.ts +25 -0
- package/process-course/edits/timestamp-refinement.ts +172 -0
- package/process-course/edits/transcript-diff.test.ts +105 -0
- package/process-course/edits/transcript-diff.ts +214 -0
- package/process-course/edits/transcript-output.test.ts +50 -0
- package/process-course/edits/transcript-output.ts +36 -0
- package/process-course/edits/types.ts +26 -0
- package/process-course/edits/video-editor.ts +246 -0
- package/process-course/errors.test.ts +63 -0
- package/process-course/errors.ts +82 -0
- package/process-course/ffmpeg.ts +449 -0
- package/process-course/jarvis-commands/handlers.ts +71 -0
- package/process-course/jarvis-commands/index.ts +14 -0
- package/process-course/jarvis-commands/parser.test.ts +348 -0
- package/process-course/jarvis-commands/parser.ts +257 -0
- package/process-course/jarvis-commands/types.ts +46 -0
- package/process-course/jarvis-commands/windows.ts +254 -0
- package/process-course/logging.ts +24 -0
- package/process-course/paths.test.ts +59 -0
- package/process-course/paths.ts +53 -0
- package/process-course/summary.test.ts +209 -0
- package/process-course/summary.ts +210 -0
- package/process-course/types.ts +85 -0
- package/process-course/utils/audio-analysis.test.ts +348 -0
- package/process-course/utils/audio-analysis.ts +463 -0
- package/process-course/utils/chapter-selection.test.ts +307 -0
- package/process-course/utils/chapter-selection.ts +136 -0
- package/process-course/utils/file-utils.test.ts +83 -0
- package/process-course/utils/file-utils.ts +57 -0
- package/process-course/utils/filename.test.ts +27 -0
- package/process-course/utils/filename.ts +12 -0
- package/process-course/utils/time-ranges.test.ts +221 -0
- package/process-course/utils/time-ranges.ts +86 -0
- package/process-course/utils/transcript.test.ts +257 -0
- package/process-course/utils/transcript.ts +86 -0
- package/process-course/utils/video-editing.ts +44 -0
- package/process-course-video.ts +389 -0
- package/public/robots.txt +2 -0
- package/server/bundling.ts +210 -0
- package/speech-detection.ts +355 -0
- package/utils.ts +138 -0
- 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
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
}
|