eprec 1.0.1 → 1.2.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 +90 -20
- package/app/assets/styles.css +592 -0
- package/app/client/app.tsx +7 -0
- package/app/client/counter.tsx +22 -0
- package/app/client/edit-session-data.ts +245 -0
- package/app/client/editing-workspace.tsx +825 -0
- package/app/client/entry.tsx +8 -0
- package/app/components/layout.tsx +35 -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 +106 -0
- package/app/routes/index.tsx +53 -0
- package/app-server.ts +62 -0
- package/cli.ts +23 -0
- package/package.json +11 -2
- package/public/robots.txt +2 -0
- package/server/bundling.ts +206 -0
- package/whispercpp-transcribe.ts +1 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { html, type SafeHtml } from 'remix/html-template'
|
|
2
|
+
import { baseImportMap } from '../config/import-map.ts'
|
|
3
|
+
|
|
4
|
+
export function Layout({
|
|
5
|
+
children,
|
|
6
|
+
title = 'Eprec Studio',
|
|
7
|
+
entryScript = '/app/client/entry.tsx',
|
|
8
|
+
}: {
|
|
9
|
+
children?: SafeHtml
|
|
10
|
+
title?: string
|
|
11
|
+
entryScript?: string | false
|
|
12
|
+
}) {
|
|
13
|
+
const importmap = { imports: baseImportMap }
|
|
14
|
+
const importmapJson = JSON.stringify(importmap)
|
|
15
|
+
const importmapScript = html.raw`<script type="importmap">${importmapJson}</script>`
|
|
16
|
+
const modulePreloads = Object.values(baseImportMap).map((value) => {
|
|
17
|
+
return html`<link rel="modulepreload" href="${value}" />`
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return html`<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="utf-8" />
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
24
|
+
<title>${title}</title>
|
|
25
|
+
<link rel="stylesheet" href="/assets/styles.css" />
|
|
26
|
+
${importmapScript} ${modulePreloads}
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="root">${children ?? ''}</div>
|
|
30
|
+
${entryScript
|
|
31
|
+
? html`<script type="module" src="${entryScript}"></script>`
|
|
32
|
+
: ''}
|
|
33
|
+
</body>
|
|
34
|
+
</html>`
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type AppEnv = {
|
|
2
|
+
NODE_ENV: 'development' | 'production' | 'test'
|
|
3
|
+
PORT: number
|
|
4
|
+
HOST: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = 3000
|
|
8
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
9
|
+
|
|
10
|
+
function parseNodeEnv(value: string | undefined): AppEnv['NODE_ENV'] {
|
|
11
|
+
if (value === 'production' || value === 'test') {
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
return 'development'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parsePort(value: string | undefined): number {
|
|
18
|
+
const parsed = Number(value)
|
|
19
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
20
|
+
return DEFAULT_PORT
|
|
21
|
+
}
|
|
22
|
+
return Math.floor(parsed)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getEnv(): AppEnv {
|
|
26
|
+
return {
|
|
27
|
+
NODE_ENV: parseNodeEnv(process.env.NODE_ENV),
|
|
28
|
+
PORT: parsePort(process.env.PORT),
|
|
29
|
+
HOST: process.env.HOST?.trim() || DEFAULT_HOST,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base import map for client-side modules.
|
|
3
|
+
*/
|
|
4
|
+
export const baseImportMap = {
|
|
5
|
+
'remix/component': '/node_modules/remix/component',
|
|
6
|
+
'remix/component/jsx-runtime': '/node_modules/remix/component/jsx-runtime',
|
|
7
|
+
'remix/component/jsx-dev-runtime':
|
|
8
|
+
'/node_modules/remix/component/jsx-dev-runtime',
|
|
9
|
+
} as const
|
package/app/router.tsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRouter, type Middleware } from 'remix/fetch-router'
|
|
3
|
+
import { html } from 'remix/html-template'
|
|
4
|
+
import { Layout } from './components/layout.tsx'
|
|
5
|
+
import routes from './config/routes.ts'
|
|
6
|
+
import { render } from './helpers/render.ts'
|
|
7
|
+
import indexHandlers from './routes/index.tsx'
|
|
8
|
+
|
|
9
|
+
const STATIC_CORS_HEADERS = {
|
|
10
|
+
'Access-Control-Allow-Origin': '*',
|
|
11
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
|
12
|
+
'Access-Control-Allow-Headers': 'Accept, Content-Type',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
function bunStaticFiles(
|
|
16
|
+
root: string,
|
|
17
|
+
options: { filter?: (pathname: string) => boolean; cacheControl?: string },
|
|
18
|
+
): Middleware {
|
|
19
|
+
const absoluteRoot = path.resolve(root)
|
|
20
|
+
return async (context, next) => {
|
|
21
|
+
if (context.method === 'OPTIONS') {
|
|
22
|
+
const relativePath = context.url.pathname.replace(/^\/+/, '')
|
|
23
|
+
if (options.filter && !options.filter(relativePath)) {
|
|
24
|
+
return next()
|
|
25
|
+
}
|
|
26
|
+
const filePath = path.join(absoluteRoot, relativePath)
|
|
27
|
+
if (!filePath.startsWith(absoluteRoot + path.sep)) {
|
|
28
|
+
return next()
|
|
29
|
+
}
|
|
30
|
+
const file = Bun.file(filePath)
|
|
31
|
+
if (!(await file.exists())) {
|
|
32
|
+
return next()
|
|
33
|
+
}
|
|
34
|
+
return new Response(null, {
|
|
35
|
+
status: 204,
|
|
36
|
+
headers: {
|
|
37
|
+
...STATIC_CORS_HEADERS,
|
|
38
|
+
'Access-Control-Max-Age': '86400',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (context.method !== 'GET' && context.method !== 'HEAD') {
|
|
44
|
+
return next()
|
|
45
|
+
}
|
|
46
|
+
const relativePath = context.url.pathname.replace(/^\/+/, '')
|
|
47
|
+
if (options.filter && !options.filter(relativePath)) {
|
|
48
|
+
return next()
|
|
49
|
+
}
|
|
50
|
+
const filePath = path.join(absoluteRoot, relativePath)
|
|
51
|
+
if (!filePath.startsWith(absoluteRoot + path.sep)) {
|
|
52
|
+
return next()
|
|
53
|
+
}
|
|
54
|
+
const file = Bun.file(filePath)
|
|
55
|
+
if (!(await file.exists())) {
|
|
56
|
+
return next()
|
|
57
|
+
}
|
|
58
|
+
return new Response(context.method === 'HEAD' ? null : file, {
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': file.type,
|
|
61
|
+
'Content-Length': String(file.size),
|
|
62
|
+
...(options.cacheControl
|
|
63
|
+
? { 'Cache-Control': options.cacheControl }
|
|
64
|
+
: {}),
|
|
65
|
+
...STATIC_CORS_HEADERS,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cacheControl =
|
|
72
|
+
process.env.NODE_ENV === 'production'
|
|
73
|
+
? 'public, max-age=31536000, immutable'
|
|
74
|
+
: 'no-cache'
|
|
75
|
+
|
|
76
|
+
export function createAppRouter(rootDir: string) {
|
|
77
|
+
const router = createRouter({
|
|
78
|
+
middleware: [
|
|
79
|
+
bunStaticFiles(path.join(rootDir, 'fixtures'), { cacheControl }),
|
|
80
|
+
bunStaticFiles(path.join(rootDir, 'public'), { cacheControl }),
|
|
81
|
+
bunStaticFiles(path.join(rootDir, 'app'), {
|
|
82
|
+
filter: (pathname) => pathname.startsWith('assets/'),
|
|
83
|
+
cacheControl,
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
defaultHandler() {
|
|
87
|
+
return render(
|
|
88
|
+
Layout({
|
|
89
|
+
title: 'Not Found',
|
|
90
|
+
entryScript: false,
|
|
91
|
+
children: html`<main class="app-shell">
|
|
92
|
+
<h1 class="app-title">404 - Not Found</h1>
|
|
93
|
+
</main>`,
|
|
94
|
+
}),
|
|
95
|
+
{ status: 404 },
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
router.map(routes.index, {
|
|
101
|
+
middleware: indexHandlers.middleware,
|
|
102
|
+
action: indexHandlers.loader,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return router
|
|
106
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { html } from 'remix/html-template'
|
|
2
|
+
import { Layout } from '../components/layout.tsx'
|
|
3
|
+
import { render } from '../helpers/render.ts'
|
|
4
|
+
|
|
5
|
+
const indexHandler = {
|
|
6
|
+
middleware: [],
|
|
7
|
+
loader() {
|
|
8
|
+
return render(
|
|
9
|
+
Layout({
|
|
10
|
+
title: 'Eprec Studio',
|
|
11
|
+
children: html`<main class="app-shell">
|
|
12
|
+
<header class="app-header">
|
|
13
|
+
<span class="app-kicker">Eprec Studio</span>
|
|
14
|
+
<h1 class="app-title">Editing workspace</h1>
|
|
15
|
+
<p class="app-subtitle">
|
|
16
|
+
Review transcript-based edits, refine cut ranges, and prepare
|
|
17
|
+
exports.
|
|
18
|
+
</p>
|
|
19
|
+
</header>
|
|
20
|
+
<section class="app-card app-card--full">
|
|
21
|
+
<h2>Timeline editor</h2>
|
|
22
|
+
<p class="app-muted">
|
|
23
|
+
Loading preview video, timeline controls, and cut ranges.
|
|
24
|
+
</p>
|
|
25
|
+
<div class="timeline-track timeline-track--skeleton"></div>
|
|
26
|
+
</section>
|
|
27
|
+
<div class="app-grid app-grid--two">
|
|
28
|
+
<section class="app-card">
|
|
29
|
+
<h2>Chapter plan</h2>
|
|
30
|
+
<p class="app-muted">
|
|
31
|
+
Output names and skip flags appear after the client boots.
|
|
32
|
+
</p>
|
|
33
|
+
</section>
|
|
34
|
+
<section class="app-card">
|
|
35
|
+
<h2>Command windows</h2>
|
|
36
|
+
<p class="app-muted">
|
|
37
|
+
Jarvis command detection will populate this panel.
|
|
38
|
+
</p>
|
|
39
|
+
</section>
|
|
40
|
+
</div>
|
|
41
|
+
<section class="app-card app-card--full">
|
|
42
|
+
<h2>Transcript search</h2>
|
|
43
|
+
<p class="app-muted">
|
|
44
|
+
Search and jump controls will load in the interactive UI.
|
|
45
|
+
</p>
|
|
46
|
+
</section>
|
|
47
|
+
</main>`,
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default indexHandler
|
package/app-server.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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(
|
|
38
|
+
`⚠️ Port ${desiredPort} was taken, using port ${port} instead`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return port
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function startAppServer(options: AppServerOptions = {}) {
|
|
45
|
+
const env = getEnv()
|
|
46
|
+
const host = options.host ?? env.HOST
|
|
47
|
+
const desiredPort = options.port ?? env.PORT
|
|
48
|
+
const port = await getServerPort(env.NODE_ENV, desiredPort)
|
|
49
|
+
const server = startServer(port, host)
|
|
50
|
+
const hostname = server.hostname.includes(':')
|
|
51
|
+
? `[${server.hostname}]`
|
|
52
|
+
: server.hostname
|
|
53
|
+
const url = `http://${hostname}:${server.port}`
|
|
54
|
+
|
|
55
|
+
console.log(`[app] running at ${url}`)
|
|
56
|
+
|
|
57
|
+
return { server, url }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (import.meta.main) {
|
|
61
|
+
await startAppServer()
|
|
62
|
+
}
|
package/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import type { CommandBuilder, CommandHandler } from 'yargs'
|
|
4
4
|
import yargs from 'yargs/yargs'
|
|
5
5
|
import { hideBin } from 'yargs/helpers'
|
|
6
|
+
import { startAppServer } from './app-server'
|
|
6
7
|
import { ensureFfmpegAvailable } from './process-course/ffmpeg'
|
|
7
8
|
import {
|
|
8
9
|
normalizeProcessArgs,
|
|
@@ -53,6 +54,28 @@ async function main() {
|
|
|
53
54
|
configureCombineVideosCommand as CommandBuilder,
|
|
54
55
|
handleCombineVideosCommand as CommandHandler,
|
|
55
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
|
+
)
|
|
56
79
|
.command(
|
|
57
80
|
'transcribe <input>',
|
|
58
81
|
'Transcribe a single audio/video file',
|
package/package.json
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eprec",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.2.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 ./app-server.ts",
|
|
11
12
|
"format": "prettier --write .",
|
|
12
13
|
"test": "bun test process-course utils.test.ts",
|
|
13
14
|
"test:e2e": "bun test e2e",
|
|
14
|
-
"test:
|
|
15
|
+
"test:smoke": "bunx playwright test -c playwright-smoke-config.ts",
|
|
16
|
+
"test:all": "bun test '**/*.test.ts'",
|
|
15
17
|
"validate": "bun run test"
|
|
16
18
|
},
|
|
17
19
|
"bin": {
|
|
18
20
|
"eprec": "./cli.ts"
|
|
19
21
|
},
|
|
20
22
|
"files": [
|
|
23
|
+
"app/**",
|
|
24
|
+
"app-server.ts",
|
|
21
25
|
"cli.ts",
|
|
22
26
|
"process-course/**",
|
|
23
27
|
"process-course-video.ts",
|
|
28
|
+
"public/**",
|
|
29
|
+
"server/**",
|
|
24
30
|
"speech-detection.ts",
|
|
25
31
|
"utils.ts",
|
|
26
32
|
"whispercpp-transcribe.ts"
|
|
@@ -28,6 +34,7 @@
|
|
|
28
34
|
"prettier": "@epic-web/config/prettier",
|
|
29
35
|
"devDependencies": {
|
|
30
36
|
"@epic-web/config": "^1.21.3",
|
|
37
|
+
"@playwright/test": "^1.58.0",
|
|
31
38
|
"@types/bun": "latest",
|
|
32
39
|
"@types/yargs": "^17.0.35",
|
|
33
40
|
"prettier": "^3.8.1"
|
|
@@ -36,7 +43,9 @@
|
|
|
36
43
|
"typescript": "^5"
|
|
37
44
|
},
|
|
38
45
|
"dependencies": {
|
|
46
|
+
"get-port": "^7.1.0",
|
|
39
47
|
"onnxruntime-node": "^1.23.2",
|
|
48
|
+
"remix": "3.0.0-alpha.0",
|
|
40
49
|
"yargs": "^18.0.0"
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
type PackageJson = {
|
|
4
|
+
exports?: Record<string, { default?: string; types?: string } | string>
|
|
5
|
+
module?: string
|
|
6
|
+
main?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function resolvePackageExport(
|
|
10
|
+
specifier: string,
|
|
11
|
+
rootDir: string,
|
|
12
|
+
): Promise<string | null> {
|
|
13
|
+
const parts = specifier.split('/')
|
|
14
|
+
let packageName: string
|
|
15
|
+
let subpathParts: string[]
|
|
16
|
+
|
|
17
|
+
if (specifier.startsWith('@')) {
|
|
18
|
+
if (parts.length < 2) return null
|
|
19
|
+
packageName = `${parts[0]}/${parts[1]}`
|
|
20
|
+
subpathParts = parts.slice(2)
|
|
21
|
+
} else {
|
|
22
|
+
if (parts.length === 0 || !parts[0]) return null
|
|
23
|
+
packageName = parts[0]
|
|
24
|
+
subpathParts = parts.slice(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const subpath = subpathParts.length > 0 ? `./${subpathParts.join('/')}` : '.'
|
|
28
|
+
const packageDir = path.join(rootDir, 'node_modules', packageName)
|
|
29
|
+
const packageJsonPath = path.join(packageDir, 'package.json')
|
|
30
|
+
const packageJsonFile = Bun.file(packageJsonPath)
|
|
31
|
+
|
|
32
|
+
if (!(await packageJsonFile.exists())) return null
|
|
33
|
+
|
|
34
|
+
const packageJson = JSON.parse(await packageJsonFile.text()) as PackageJson
|
|
35
|
+
|
|
36
|
+
if (!packageJson.exports) {
|
|
37
|
+
const entryFile = packageJson.module || packageJson.main
|
|
38
|
+
if (entryFile) {
|
|
39
|
+
const entryPath = path.join(packageDir, entryFile)
|
|
40
|
+
if (await Bun.file(entryPath).exists()) return entryPath
|
|
41
|
+
}
|
|
42
|
+
const indexPath = path.join(packageDir, 'index.js')
|
|
43
|
+
return (await Bun.file(indexPath).exists()) ? indexPath : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const exportEntry = packageJson.exports[subpath]
|
|
47
|
+
if (!exportEntry) return null
|
|
48
|
+
|
|
49
|
+
const exportPath =
|
|
50
|
+
typeof exportEntry === 'string' ? exportEntry : exportEntry.default
|
|
51
|
+
|
|
52
|
+
if (!exportPath) return null
|
|
53
|
+
|
|
54
|
+
const resolvedPath = path.join(packageDir, exportPath)
|
|
55
|
+
return (await Bun.file(resolvedPath).exists()) ? resolvedPath : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const BUNDLING_CORS_HEADERS = {
|
|
59
|
+
'Access-Control-Allow-Origin': '*',
|
|
60
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
|
61
|
+
'Access-Control-Allow-Headers': 'Accept, Content-Type',
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
export function createBundlingRoutes(rootDir: string) {
|
|
65
|
+
const clientDir = path.resolve(rootDir, 'app', 'client')
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
'/app/client/*': async (request: Request) => {
|
|
69
|
+
if (request.method === 'OPTIONS') {
|
|
70
|
+
return new Response(null, {
|
|
71
|
+
status: 204,
|
|
72
|
+
headers: {
|
|
73
|
+
...BUNDLING_CORS_HEADERS,
|
|
74
|
+
'Access-Control-Max-Age': '86400',
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const url = new URL(request.url)
|
|
80
|
+
const reqPath = path.posix.normalize(url.pathname.replace(/^\/+/, ''))
|
|
81
|
+
const resolved = path.resolve(rootDir, reqPath)
|
|
82
|
+
|
|
83
|
+
if (!resolved.startsWith(clientDir + path.sep)) {
|
|
84
|
+
return new Response('Forbidden', {
|
|
85
|
+
status: 403,
|
|
86
|
+
headers: BUNDLING_CORS_HEADERS,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!resolved.endsWith('.ts') && !resolved.endsWith('.tsx')) {
|
|
91
|
+
return new Response('Not Found', {
|
|
92
|
+
status: 404,
|
|
93
|
+
headers: BUNDLING_CORS_HEADERS,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entryFile = Bun.file(resolved)
|
|
98
|
+
if (!(await entryFile.exists())) {
|
|
99
|
+
return new Response('Not Found', {
|
|
100
|
+
status: 404,
|
|
101
|
+
headers: BUNDLING_CORS_HEADERS,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const buildResult = await Bun.build({
|
|
106
|
+
entrypoints: [resolved],
|
|
107
|
+
target: 'browser',
|
|
108
|
+
minify: Bun.env.NODE_ENV === 'production',
|
|
109
|
+
splitting: false,
|
|
110
|
+
format: 'esm',
|
|
111
|
+
sourcemap: Bun.env.NODE_ENV === 'production' ? 'none' : 'inline',
|
|
112
|
+
jsx: { importSource: 'remix/component' },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!buildResult.success) {
|
|
116
|
+
const errorMessage = buildResult.logs
|
|
117
|
+
.map((log) => log.message)
|
|
118
|
+
.join('\n')
|
|
119
|
+
return new Response(errorMessage || 'Build failed', {
|
|
120
|
+
status: 500,
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'text/plain',
|
|
123
|
+
...BUNDLING_CORS_HEADERS,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const output = buildResult.outputs[0]
|
|
129
|
+
return new Response(output, {
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/javascript',
|
|
132
|
+
'Cache-Control':
|
|
133
|
+
Bun.env.NODE_ENV === 'production'
|
|
134
|
+
? 'public, max-age=31536000, immutable'
|
|
135
|
+
: 'no-cache',
|
|
136
|
+
...BUNDLING_CORS_HEADERS,
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
'/node_modules/*': async (request: Request) => {
|
|
142
|
+
if (request.method === 'OPTIONS') {
|
|
143
|
+
return new Response(null, {
|
|
144
|
+
status: 204,
|
|
145
|
+
headers: {
|
|
146
|
+
...BUNDLING_CORS_HEADERS,
|
|
147
|
+
'Access-Control-Max-Age': '86400',
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const url = new URL(request.url)
|
|
153
|
+
const specifier = url.pathname.replace('/node_modules/', '')
|
|
154
|
+
const filepath = await resolvePackageExport(specifier, rootDir)
|
|
155
|
+
|
|
156
|
+
if (!filepath) {
|
|
157
|
+
return new Response('Package not found', {
|
|
158
|
+
status: 404,
|
|
159
|
+
headers: BUNDLING_CORS_HEADERS,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const nodeModulesDir = path.resolve(rootDir, 'node_modules')
|
|
164
|
+
if (!filepath.startsWith(nodeModulesDir + path.sep)) {
|
|
165
|
+
return new Response('Forbidden', {
|
|
166
|
+
status: 403,
|
|
167
|
+
headers: BUNDLING_CORS_HEADERS,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const buildResult = await Bun.build({
|
|
172
|
+
entrypoints: [filepath],
|
|
173
|
+
target: 'browser',
|
|
174
|
+
minify: Bun.env.NODE_ENV === 'production',
|
|
175
|
+
splitting: false,
|
|
176
|
+
format: 'esm',
|
|
177
|
+
sourcemap: Bun.env.NODE_ENV === 'production' ? 'none' : 'inline',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (!buildResult.success) {
|
|
181
|
+
const errorMessage = buildResult.logs
|
|
182
|
+
.map((log) => log.message)
|
|
183
|
+
.join('\n')
|
|
184
|
+
return new Response(errorMessage || 'Build failed', {
|
|
185
|
+
status: 500,
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'text/plain',
|
|
188
|
+
...BUNDLING_CORS_HEADERS,
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const output = buildResult.outputs[0]
|
|
194
|
+
return new Response(output, {
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/javascript',
|
|
197
|
+
'Cache-Control':
|
|
198
|
+
Bun.env.NODE_ENV === 'production'
|
|
199
|
+
? 'public, max-age=31536000, immutable'
|
|
200
|
+
: 'no-cache',
|
|
201
|
+
...BUNDLING_CORS_HEADERS,
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
package/whispercpp-transcribe.ts
CHANGED
|
@@ -113,9 +113,7 @@ async function readTranscriptText(transcriptPath: string, fallback: string) {
|
|
|
113
113
|
throw new Error('Whisper.cpp transcript output was empty.')
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
async function readTranscriptSegments(
|
|
117
|
-
transcriptPath: string,
|
|
118
|
-
): Promise<{
|
|
116
|
+
async function readTranscriptSegments(transcriptPath: string): Promise<{
|
|
119
117
|
segments: TranscriptSegment[]
|
|
120
118
|
source: TranscriptionResult['segmentsSource']
|
|
121
119
|
}> {
|