eprec 1.9.0 → 1.10.1
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/app/assets/styles.css +0 -1
- package/app/client/editing-workspace.tsx +15 -16
- package/app/routes/index.tsx +1 -3
- package/app/video-api.ts +11 -4
- package/package.json +1 -1
- package/process-course/edits/edit-workspace.ts +14 -6
- package/src/app-server.test.ts +70 -0
- package/src/app-server.ts +53 -18
package/app/assets/styles.css
CHANGED
|
@@ -466,11 +466,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
466
466
|
primaryChapterId.length > 0 &&
|
|
467
467
|
secondaryChapterId.length > 0 &&
|
|
468
468
|
primaryChapterId !== secondaryChapterId
|
|
469
|
-
const commandPreview = buildCommandPreview(
|
|
470
|
-
sourceName,
|
|
471
|
-
chapters,
|
|
472
|
-
sourcePath,
|
|
473
|
-
)
|
|
469
|
+
const commandPreview = buildCommandPreview(sourceName, chapters, sourcePath)
|
|
474
470
|
const previewTime =
|
|
475
471
|
previewReady && previewDuration > 0
|
|
476
472
|
? (playhead / duration) * previewDuration
|
|
@@ -524,8 +520,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
524
520
|
value={videoPathInput}
|
|
525
521
|
on={{
|
|
526
522
|
input: (event) => {
|
|
527
|
-
const target =
|
|
528
|
-
event.currentTarget as HTMLInputElement
|
|
523
|
+
const target = event.currentTarget as HTMLInputElement
|
|
529
524
|
updateVideoPathInput(target.value)
|
|
530
525
|
},
|
|
531
526
|
}}
|
|
@@ -854,10 +849,7 @@ export function EditingWorkspace(handle: Handle) {
|
|
|
854
849
|
</span>
|
|
855
850
|
</div>
|
|
856
851
|
<span
|
|
857
|
-
class={classNames(
|
|
858
|
-
'status-pill',
|
|
859
|
-
previewStatus.className,
|
|
860
|
-
)}
|
|
852
|
+
class={classNames('status-pill', previewStatus.className)}
|
|
861
853
|
>
|
|
862
854
|
{previewStatus.label}
|
|
863
855
|
</span>
|
|
@@ -1424,14 +1416,21 @@ function buildCommandPreview(
|
|
|
1424
1416
|
? sourcePath
|
|
1425
1417
|
: sourceName
|
|
1426
1418
|
return [
|
|
1427
|
-
'
|
|
1428
|
-
` --input
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
` --output
|
|
1419
|
+
'eprec edit \\',
|
|
1420
|
+
` --input ${escapeShellArg(inputPath)} \\`,
|
|
1421
|
+
` --transcript ${escapeShellArg('transcript.json')} \\`,
|
|
1422
|
+
` --edited ${escapeShellArg('transcript.txt')} \\`,
|
|
1423
|
+
` --output ${escapeShellArg(outputName)}`,
|
|
1432
1424
|
].join('\n')
|
|
1433
1425
|
}
|
|
1434
1426
|
|
|
1427
|
+
function escapeShellArg(value: string) {
|
|
1428
|
+
if (value.length === 0) {
|
|
1429
|
+
return "''"
|
|
1430
|
+
}
|
|
1431
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1435
1434
|
function findPreviousCut(cutRanges: CutRange[], playhead: number) {
|
|
1436
1435
|
let previous: CutRange | null = null
|
|
1437
1436
|
for (const range of cutRanges) {
|
package/app/routes/index.tsx
CHANGED
|
@@ -21,9 +21,7 @@ const indexHandler = {
|
|
|
21
21
|
</header>
|
|
22
22
|
<section class="app-card app-card--full">
|
|
23
23
|
<h2>Source video</h2>
|
|
24
|
-
<p class="app-muted">
|
|
25
|
-
Paste a video file path once the UI loads.
|
|
26
|
-
</p>
|
|
24
|
+
<p class="app-muted">Paste a video file path once the UI loads.</p>
|
|
27
25
|
</section>
|
|
28
26
|
<section class="app-card app-card--full">
|
|
29
27
|
<h2>Processing actions</h2>
|
package/app/video-api.ts
CHANGED
|
@@ -8,7 +8,11 @@ function isLocalhostOrigin(origin: string | null): boolean {
|
|
|
8
8
|
try {
|
|
9
9
|
const url = new URL(origin)
|
|
10
10
|
const hostname = url.hostname.toLowerCase()
|
|
11
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
hostname === 'localhost' ||
|
|
13
|
+
hostname === '127.0.0.1' ||
|
|
14
|
+
hostname === '[::1]'
|
|
15
|
+
)
|
|
12
16
|
} catch {
|
|
13
17
|
return false
|
|
14
18
|
}
|
|
@@ -71,13 +75,16 @@ function parseRangeHeader(
|
|
|
71
75
|
return { start: rangeStart, end: size - 1 }
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
const rangeEnd =
|
|
75
|
-
end === null || end >= size ? Math.max(size - 1, 0) : end
|
|
78
|
+
const rangeEnd = end === null || end >= size ? Math.max(size - 1, 0) : end
|
|
76
79
|
if (start > rangeEnd) return null
|
|
77
80
|
return { start, end: rangeEnd }
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
function buildVideoHeaders(
|
|
83
|
+
function buildVideoHeaders(
|
|
84
|
+
contentType: string,
|
|
85
|
+
length: number,
|
|
86
|
+
origin: string | null,
|
|
87
|
+
) {
|
|
81
88
|
return {
|
|
82
89
|
'Content-Type': contentType,
|
|
83
90
|
'Content-Length': String(length),
|
package/package.json
CHANGED
|
@@ -70,21 +70,29 @@ function buildInstructions(options: {
|
|
|
70
70
|
transcriptTextPath: string
|
|
71
71
|
outputBasename: string
|
|
72
72
|
}): string {
|
|
73
|
+
const outputPath = path.join(options.editsDirectory, options.outputBasename)
|
|
73
74
|
return [
|
|
74
75
|
'# Manual edit workflow',
|
|
75
76
|
'',
|
|
76
77
|
'1) Edit `transcript.txt` and delete whole words only.',
|
|
77
78
|
'2) Run:',
|
|
78
79
|
'',
|
|
79
|
-
`
|
|
80
|
-
` --input
|
|
81
|
-
` --transcript
|
|
82
|
-
` --edited
|
|
83
|
-
` --output
|
|
80
|
+
` eprec edit \\`,
|
|
81
|
+
` --input ${escapeShellArg(options.originalVideoPath)} \\`,
|
|
82
|
+
` --transcript ${escapeShellArg(options.transcriptJsonPath)} \\`,
|
|
83
|
+
` --edited ${escapeShellArg(options.transcriptTextPath)} \\`,
|
|
84
|
+
` --output ${escapeShellArg(outputPath)}`,
|
|
84
85
|
'',
|
|
85
86
|
'If the transcript no longer matches, regenerate it with:',
|
|
86
87
|
'',
|
|
87
|
-
` bun process-course/edits/regenerate-transcript.ts --dir
|
|
88
|
+
` bun process-course/edits/regenerate-transcript.ts --dir ${escapeShellArg(options.editsDirectory)}`,
|
|
88
89
|
'',
|
|
89
90
|
].join('\n')
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
function escapeShellArg(value: string) {
|
|
94
|
+
if (value.length === 0) {
|
|
95
|
+
return "''"
|
|
96
|
+
}
|
|
97
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`
|
|
98
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test'
|
|
2
|
+
import { createShortcutInputHandler } from './app-server'
|
|
3
|
+
|
|
4
|
+
type ShortcutCounts = {
|
|
5
|
+
open: number
|
|
6
|
+
restart: number
|
|
7
|
+
stop: number
|
|
8
|
+
help: number
|
|
9
|
+
spacing: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createShortcutCounts(): ShortcutCounts {
|
|
13
|
+
return {
|
|
14
|
+
open: 0,
|
|
15
|
+
restart: 0,
|
|
16
|
+
stop: 0,
|
|
17
|
+
help: 0,
|
|
18
|
+
spacing: 0,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createShortcutHandler() {
|
|
23
|
+
const counts = createShortcutCounts()
|
|
24
|
+
const handler = createShortcutInputHandler({
|
|
25
|
+
open: () => {
|
|
26
|
+
counts.open += 1
|
|
27
|
+
},
|
|
28
|
+
restart: () => {
|
|
29
|
+
counts.restart += 1
|
|
30
|
+
},
|
|
31
|
+
stop: () => {
|
|
32
|
+
counts.stop += 1
|
|
33
|
+
},
|
|
34
|
+
help: () => {
|
|
35
|
+
counts.help += 1
|
|
36
|
+
},
|
|
37
|
+
spacing: () => {
|
|
38
|
+
counts.spacing += 1
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return { counts, handler }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('createShortcutInputHandler adds spacing for enter key', () => {
|
|
46
|
+
const { counts, handler } = createShortcutHandler()
|
|
47
|
+
|
|
48
|
+
handler.handleInput('\r')
|
|
49
|
+
|
|
50
|
+
expect(counts.spacing).toBe(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('createShortcutInputHandler ignores linefeed after carriage return', () => {
|
|
54
|
+
const { counts, handler } = createShortcutHandler()
|
|
55
|
+
|
|
56
|
+
handler.handleInput('\r\n')
|
|
57
|
+
|
|
58
|
+
expect(counts.spacing).toBe(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('createShortcutInputHandler maps shortcuts case-insensitively', () => {
|
|
62
|
+
const { counts, handler } = createShortcutHandler()
|
|
63
|
+
|
|
64
|
+
handler.handleInput('OrQh?')
|
|
65
|
+
|
|
66
|
+
expect(counts.open).toBe(1)
|
|
67
|
+
expect(counts.restart).toBe(1)
|
|
68
|
+
expect(counts.stop).toBe(1)
|
|
69
|
+
expect(counts.help).toBe(2)
|
|
70
|
+
})
|
package/src/app-server.ts
CHANGED
|
@@ -26,6 +26,14 @@ const SHORTCUT_COLORS: Record<string, string> = {
|
|
|
26
26
|
const ANSI_RESET = '\u001b[0m'
|
|
27
27
|
const APP_ROOT = path.resolve(import.meta.dirname, '..')
|
|
28
28
|
|
|
29
|
+
type ShortcutActions = {
|
|
30
|
+
open: () => void
|
|
31
|
+
restart: () => void
|
|
32
|
+
stop: () => void
|
|
33
|
+
help: () => void
|
|
34
|
+
spacing: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
function colorizeShortcut(key: string) {
|
|
30
38
|
if (!COLOR_ENABLED) {
|
|
31
39
|
return key
|
|
@@ -55,6 +63,7 @@ function getShortcutLines(url: string) {
|
|
|
55
63
|
` ${colorizeShortcut('r')}: restart server`,
|
|
56
64
|
` ${colorizeShortcut('q')}: quit server`,
|
|
57
65
|
` ${colorizeShortcut('h')}: show shortcuts`,
|
|
66
|
+
` ${colorizeShortcut('enter')}: add log spacing`,
|
|
58
67
|
]
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -95,44 +104,70 @@ function openBrowser(url: string) {
|
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
restart: () => void
|
|
101
|
-
stop: () => void
|
|
102
|
-
}) {
|
|
103
|
-
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
104
|
-
return () => {}
|
|
105
|
-
}
|
|
107
|
+
export function createShortcutInputHandler(actions: ShortcutActions) {
|
|
108
|
+
let lastKey: string | null = null
|
|
106
109
|
|
|
107
|
-
const stdin = process.stdin
|
|
108
110
|
const handleKey = (key: string) => {
|
|
109
111
|
if (key === '\u0003') {
|
|
110
|
-
|
|
112
|
+
actions.stop()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (key === '\r' || key === '\n') {
|
|
116
|
+
actions.spacing()
|
|
111
117
|
return
|
|
112
118
|
}
|
|
113
119
|
const lower = key.toLowerCase()
|
|
114
120
|
if (lower === 'o') {
|
|
115
|
-
|
|
121
|
+
actions.open()
|
|
116
122
|
return
|
|
117
123
|
}
|
|
118
124
|
if (lower === 'r') {
|
|
119
|
-
|
|
125
|
+
actions.restart()
|
|
120
126
|
return
|
|
121
127
|
}
|
|
122
128
|
if (lower === 'q') {
|
|
123
|
-
|
|
129
|
+
actions.stop()
|
|
124
130
|
return
|
|
125
131
|
}
|
|
126
132
|
if (lower === 'h' || lower === '?') {
|
|
127
|
-
|
|
133
|
+
actions.help()
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
return {
|
|
138
|
+
handleInput: (input: string) => {
|
|
139
|
+
for (const key of input) {
|
|
140
|
+
if (key === '\n' && lastKey === '\r') {
|
|
141
|
+
lastKey = key
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
handleKey(key)
|
|
145
|
+
lastKey = key
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function setupShortcutHandling(options: {
|
|
152
|
+
getUrl: () => string
|
|
153
|
+
restart: () => void
|
|
154
|
+
stop: () => void
|
|
155
|
+
}) {
|
|
156
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
157
|
+
return () => {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const stdin = process.stdin
|
|
161
|
+
const shortcutHandler = createShortcutInputHandler({
|
|
162
|
+
open: () => openBrowser(options.getUrl()),
|
|
163
|
+
restart: options.restart,
|
|
164
|
+
stop: options.stop,
|
|
165
|
+
help: () => logShortcuts(options.getUrl()),
|
|
166
|
+
spacing: () => console.log(''),
|
|
167
|
+
})
|
|
168
|
+
|
|
131
169
|
const onData = (chunk: Buffer | string) => {
|
|
132
|
-
|
|
133
|
-
for (const key of input) {
|
|
134
|
-
handleKey(key)
|
|
135
|
-
}
|
|
170
|
+
shortcutHandler.handleInput(chunk.toString())
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
stdin.setRawMode(true)
|