eprec 1.3.0 → 1.4.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/README.md +4 -4
- package/app-server.ts +173 -6
- package/cli.ts +16 -9
- package/package.json +2 -2
- package/process-course/edits/cli.ts +8 -4
package/README.md
CHANGED
|
@@ -48,14 +48,14 @@ bun process-course-video.ts "/path/to/input.mp4" "/path/to/output" \
|
|
|
48
48
|
|
|
49
49
|
## Web UI (experimental)
|
|
50
50
|
|
|
51
|
-
Start the Remix-powered UI shell
|
|
51
|
+
Start the Remix-powered UI shell (watch mode enabled):
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
bun
|
|
54
|
+
bun run app:start
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Then open `http://
|
|
58
|
-
defaults.
|
|
57
|
+
Then open `http://localhost:3000`. Use `-- --port` or `-- --host` to override
|
|
58
|
+
the defaults.
|
|
59
59
|
|
|
60
60
|
## CLI Options
|
|
61
61
|
|
package/app-server.ts
CHANGED
|
@@ -10,6 +10,140 @@ type AppServerOptions = {
|
|
|
10
10
|
port?: number
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const LOCALHOST_ALIASES = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
14
|
+
const COLOR_ENABLED =
|
|
15
|
+
process.env.FORCE_COLOR === '1' ||
|
|
16
|
+
(Boolean(process.stdout.isTTY) && !process.env.NO_COLOR)
|
|
17
|
+
const SHORTCUT_COLORS: Record<string, string> = {
|
|
18
|
+
o: '\u001b[36m',
|
|
19
|
+
r: '\u001b[33m',
|
|
20
|
+
q: '\u001b[31m',
|
|
21
|
+
h: '\u001b[35m',
|
|
22
|
+
}
|
|
23
|
+
const ANSI_RESET = '\u001b[0m'
|
|
24
|
+
|
|
25
|
+
function colorizeShortcut(key: string) {
|
|
26
|
+
if (!COLOR_ENABLED) {
|
|
27
|
+
return key
|
|
28
|
+
}
|
|
29
|
+
const color = SHORTCUT_COLORS[key.toLowerCase()]
|
|
30
|
+
return color ? `${color}${key}${ANSI_RESET}` : key
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatHostnameForDisplay(hostname: string) {
|
|
34
|
+
if (LOCALHOST_ALIASES.has(hostname)) {
|
|
35
|
+
return 'localhost'
|
|
36
|
+
}
|
|
37
|
+
if (hostname.includes(':')) {
|
|
38
|
+
return `[${hostname}]`
|
|
39
|
+
}
|
|
40
|
+
return hostname
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatServerUrl(hostname: string, port: number) {
|
|
44
|
+
return `http://${formatHostnameForDisplay(hostname)}:${port}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getShortcutLines(url: string) {
|
|
48
|
+
return [
|
|
49
|
+
'[app] shortcuts:',
|
|
50
|
+
` ${colorizeShortcut('o')}: open ${url} in browser`,
|
|
51
|
+
` ${colorizeShortcut('r')}: restart server`,
|
|
52
|
+
` ${colorizeShortcut('q')}: quit server`,
|
|
53
|
+
` ${colorizeShortcut('h')}: show shortcuts`,
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function logShortcuts(url: string) {
|
|
58
|
+
for (const line of getShortcutLines(url)) {
|
|
59
|
+
console.log(line)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function openBrowser(url: string) {
|
|
64
|
+
const platform = process.platform
|
|
65
|
+
const command =
|
|
66
|
+
platform === 'darwin'
|
|
67
|
+
? ['open', url]
|
|
68
|
+
: platform === 'win32'
|
|
69
|
+
? ['cmd', '/c', 'start', '', url]
|
|
70
|
+
: ['xdg-open', url]
|
|
71
|
+
try {
|
|
72
|
+
const subprocess = Bun.spawn({
|
|
73
|
+
cmd: command,
|
|
74
|
+
stdin: 'ignore',
|
|
75
|
+
stdout: 'ignore',
|
|
76
|
+
stderr: 'ignore',
|
|
77
|
+
})
|
|
78
|
+
void subprocess.exited.catch((error) => {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[app] failed to open browser: ${
|
|
81
|
+
error instanceof Error ? error.message : String(error)
|
|
82
|
+
}`,
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[app] failed to open browser: ${
|
|
88
|
+
error instanceof Error ? error.message : String(error)
|
|
89
|
+
}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setupShortcutHandling(options: {
|
|
95
|
+
getUrl: () => string
|
|
96
|
+
restart: () => void
|
|
97
|
+
stop: () => void
|
|
98
|
+
}) {
|
|
99
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
100
|
+
return () => {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stdin = process.stdin
|
|
104
|
+
const handleKey = (key: string) => {
|
|
105
|
+
if (key === '\u0003') {
|
|
106
|
+
options.stop()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
const lower = key.toLowerCase()
|
|
110
|
+
if (lower === 'o') {
|
|
111
|
+
openBrowser(options.getUrl())
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (lower === 'r') {
|
|
115
|
+
options.restart()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (lower === 'q') {
|
|
119
|
+
options.stop()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (lower === 'h' || lower === '?') {
|
|
123
|
+
logShortcuts(options.getUrl())
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const onData = (chunk: Buffer | string) => {
|
|
128
|
+
const input = chunk.toString()
|
|
129
|
+
for (const key of input) {
|
|
130
|
+
handleKey(key)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stdin.setRawMode(true)
|
|
135
|
+
stdin.resume()
|
|
136
|
+
stdin.on('data', onData)
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
stdin.off('data', onData)
|
|
140
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
141
|
+
stdin.setRawMode(false)
|
|
142
|
+
}
|
|
143
|
+
stdin.pause()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
13
147
|
function startServer(port: number, hostname: string) {
|
|
14
148
|
const router = createAppRouter(import.meta.dirname)
|
|
15
149
|
return Bun.serve({
|
|
@@ -46,15 +180,48 @@ export async function startAppServer(options: AppServerOptions = {}) {
|
|
|
46
180
|
const host = options.host ?? env.HOST
|
|
47
181
|
const desiredPort = options.port ?? env.PORT
|
|
48
182
|
const port = await getServerPort(env.NODE_ENV, desiredPort)
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
183
|
+
let server = startServer(port, host)
|
|
184
|
+
const getUrl = () => formatServerUrl(server.hostname, server.port)
|
|
185
|
+
let cleanupInput = () => {}
|
|
186
|
+
let isRestarting = false
|
|
187
|
+
const stopServer = () => {
|
|
188
|
+
console.log('[app] stopping server...')
|
|
189
|
+
cleanupInput()
|
|
190
|
+
server.stop()
|
|
191
|
+
process.exit(0)
|
|
192
|
+
}
|
|
193
|
+
const restartServer = async () => {
|
|
194
|
+
if (isRestarting) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
isRestarting = true
|
|
198
|
+
try {
|
|
199
|
+
console.log('[app] restarting server...')
|
|
200
|
+
await server.stop()
|
|
201
|
+
server = startServer(port, host)
|
|
202
|
+
console.log(`[app] running at ${getUrl()}`)
|
|
203
|
+
} finally {
|
|
204
|
+
isRestarting = false
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
cleanupInput = setupShortcutHandling({
|
|
208
|
+
getUrl,
|
|
209
|
+
restart: restartServer,
|
|
210
|
+
stop: stopServer,
|
|
211
|
+
})
|
|
212
|
+
const url = getUrl()
|
|
54
213
|
|
|
55
214
|
console.log(`[app] running at ${url}`)
|
|
215
|
+
logShortcuts(url)
|
|
56
216
|
|
|
57
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
get server() { return server },
|
|
219
|
+
url,
|
|
220
|
+
stop: () => {
|
|
221
|
+
cleanupInput()
|
|
222
|
+
server.stop()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
58
225
|
}
|
|
59
226
|
|
|
60
227
|
if (import.meta.main) {
|
package/cli.ts
CHANGED
|
@@ -164,14 +164,15 @@ async function main(rawArgs = hideBin(process.argv)) {
|
|
|
164
164
|
})
|
|
165
165
|
resultText = result.text
|
|
166
166
|
},
|
|
167
|
-
{
|
|
167
|
+
{
|
|
168
|
+
successText: 'Transcription complete',
|
|
169
|
+
enabled: context.interactive,
|
|
170
|
+
},
|
|
168
171
|
)
|
|
169
172
|
console.log(
|
|
170
173
|
`Transcript written to ${transcribeArgs.outputBasePath}.txt`,
|
|
171
174
|
)
|
|
172
|
-
console.log(
|
|
173
|
-
`Segments written to ${transcribeArgs.outputBasePath}.json`,
|
|
174
|
-
)
|
|
175
|
+
console.log(`Segments written to ${transcribeArgs.outputBasePath}.json`)
|
|
175
176
|
console.log(resultText)
|
|
176
177
|
},
|
|
177
178
|
)
|
|
@@ -208,7 +209,10 @@ async function main(rawArgs = hideBin(process.argv)) {
|
|
|
208
209
|
end,
|
|
209
210
|
})
|
|
210
211
|
},
|
|
211
|
-
{
|
|
212
|
+
{
|
|
213
|
+
successText: 'Speech detection complete',
|
|
214
|
+
enabled: context.interactive,
|
|
215
|
+
},
|
|
212
216
|
)
|
|
213
217
|
console.log(JSON.stringify(segments, null, 2))
|
|
214
218
|
},
|
|
@@ -230,9 +234,7 @@ function createCliUxContext(): CliUxContext {
|
|
|
230
234
|
return { interactive, prompter, pathPicker }
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
async function promptForCommand(
|
|
234
|
-
prompter: Prompter,
|
|
235
|
-
): Promise<string[] | null> {
|
|
237
|
+
async function promptForCommand(prompter: Prompter): Promise<string[] | null> {
|
|
236
238
|
const selection = await prompter.select('Choose a command', [
|
|
237
239
|
{
|
|
238
240
|
name: 'Process chapters into separate files',
|
|
@@ -283,7 +285,12 @@ async function resolveProcessArgs(argv: Arguments, context: CliUxContext) {
|
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
let outputDir = resolveOptionalString(argv['output-dir'])
|
|
286
|
-
if (
|
|
288
|
+
if (
|
|
289
|
+
!outputDir &&
|
|
290
|
+
context.interactive &&
|
|
291
|
+
context.prompter &&
|
|
292
|
+
context.pathPicker
|
|
293
|
+
) {
|
|
287
294
|
const chooseOutput = await context.prompter.confirm(
|
|
288
295
|
'Choose a custom output directory?',
|
|
289
296
|
{ defaultValue: false },
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eprec",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/epicweb-dev/eprec"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"app:start": "bun ./
|
|
11
|
+
"app:start": "bun --watch ./cli.ts app start",
|
|
12
12
|
"format": "prettier --write .",
|
|
13
13
|
"test": "bun test process-course utils.test.ts",
|
|
14
14
|
"test:e2e": "bun test ./e2e",
|
|
@@ -40,7 +40,10 @@ type CliUxOptions = {
|
|
|
40
40
|
pathPicker?: PathPicker
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function buildCombinedOutputPath(
|
|
43
|
+
export function buildCombinedOutputPath(
|
|
44
|
+
video1Path: string,
|
|
45
|
+
video2Path: string,
|
|
46
|
+
) {
|
|
44
47
|
const dir = path.dirname(video1Path)
|
|
45
48
|
const ext = path.extname(video1Path) || path.extname(video2Path) || '.mp4'
|
|
46
49
|
const name1 = path.parse(video1Path).name
|
|
@@ -81,8 +84,7 @@ export async function resolveEditVideoArgs(
|
|
|
81
84
|
})
|
|
82
85
|
}
|
|
83
86
|
const defaultOutputPath = buildEditedOutputPath(input)
|
|
84
|
-
const outputPath =
|
|
85
|
-
resolveOptionalString(argv.output) ?? defaultOutputPath
|
|
87
|
+
const outputPath = resolveOptionalString(argv.output) ?? defaultOutputPath
|
|
86
88
|
const paddingMs = resolvePaddingMs(argv['padding-ms'])
|
|
87
89
|
|
|
88
90
|
return {
|
|
@@ -194,7 +196,9 @@ export function createEditVideoHandler(options: CliUxOptions): CommandHandler {
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
export function createCombineVideosHandler(
|
|
199
|
+
export function createCombineVideosHandler(
|
|
200
|
+
options: CliUxOptions,
|
|
201
|
+
): CommandHandler {
|
|
198
202
|
return async (argv) => {
|
|
199
203
|
const args = await resolveCombineVideosArgs(argv, options)
|
|
200
204
|
let outputPath = ''
|