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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Kent C. Dodds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,138 @@
1
- # eprec
1
+ <div align="center">
2
+ <h1>eprec</h1>
2
3
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
+ <p>Tools for processing Epic Web course recordings</p>
5
+ </div>
4
6
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
7
+ ---
6
8
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
9
+ <!-- prettier-ignore-start -->
10
+ [![Build Status][build-badge]][build]
11
+ [![version][version-badge]][package]
12
+ [![downloads][downloads-badge]][npmtrends]
13
+ [![MIT License][license-badge]][license]
14
+ <!-- prettier-ignore-end -->
8
15
 
9
- ## Purpose
16
+ ## Overview
10
17
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `eprec`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
18
+ A Bun-based CLI that processes recorded course videos by splitting chapter
19
+ markers into separate files, trimming silence at the start/end, and optionally
20
+ removing spoken "Jarvis" command windows via transcript timestamps refined with
21
+ audio-based silence detection.
15
22
 
16
- ## What is OIDC Trusted Publishing?
23
+ ## Requirements
17
24
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
25
+ - **Bun** - runtime and package manager
26
+ - **ffmpeg + ffprobe** - must be available on PATH
27
+ - **whisper-cli** _(optional)_ - from
28
+ [whisper.cpp](https://github.com/ggerganov/whisper.cpp), required for
29
+ transcription
30
+ - Pass `--whisper-binary-path` if not on PATH
31
+ - Model file auto-downloads to `.cache/whispercpp/ggml-small.en.bin`
32
+ - **Silero VAD model** - auto-downloads to `.cache/silero-vad.onnx` on first use
19
33
 
20
- ## Setup Instructions
34
+ ## Installation
21
35
 
22
- To properly configure OIDC trusted publishing for this package:
36
+ ```bash
37
+ bun install
38
+ ```
23
39
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
40
+ ## Quick Start
28
41
 
29
- ## DO NOT USE THIS PACKAGE
42
+ ```bash
43
+ bun process-course-video.ts "/path/to/input.mp4" "/path/to/output" \
44
+ --enable-transcription \
45
+ --keep-intermediates \
46
+ --write-logs
47
+ ```
30
48
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
49
+ ## Web UI (experimental)
36
50
 
37
- ## More Information
51
+ Start the Remix-powered UI shell with the CLI:
38
52
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
53
+ ```bash
54
+ bun cli.ts app start
55
+ ```
42
56
 
43
- ---
57
+ Then open `http://127.0.0.1:3000`. Use `--port` or `--host` to override the
58
+ defaults.
59
+
60
+ ## CLI Options
61
+
62
+ | Option | Alias | Description | Default |
63
+ | ------------------------ | ----- | --------------------------------------- | ----------- |
64
+ | `input` | | Input video file (mp4/mkv) | _required_ |
65
+ | `outputDir` | | Output directory | `output` |
66
+ | `--min-chapter-seconds` | `-m` | Skip chapters shorter than this | `15` |
67
+ | `--dry-run` | `-d` | Don't write files or run ffmpeg | `false` |
68
+ | `--keep-intermediates` | `-k` | Keep `.tmp` files for debugging | `false` |
69
+ | `--write-logs` | `-l` | Write log files for skips/fallbacks | `false` |
70
+ | `--enable-transcription` | | Run whisper.cpp for command detection | `false` |
71
+ | `--whisper-model-path` | | Path to whisper.cpp model file | auto-cached |
72
+ | `--whisper-language` | | Language for whisper | `en` |
73
+ | `--whisper-binary-path` | | Path to `whisper-cli` binary | system PATH |
74
+ | `--chapter` | `-c` | Filter to specific chapters (see below) | all |
75
+
76
+ ## Chapter Selection
77
+
78
+ The `--chapter` flag supports flexible selection:
79
+
80
+ - Single: `--chapter 4`
81
+ - Range: `--chapter 4-6`
82
+ - Open range: `--chapter 4-*` (chapter 4 to end)
83
+ - Multiple: `--chapter 4,6,9-12`
84
+
85
+ Chapter numbers are 1-based by default.
86
+
87
+ ## Output Structure
88
+
89
+ Final files are written to the output directory with names like:
90
+
91
+ ```
92
+ chapter-01-intro.mp4
93
+ chapter-02-getting-started.mp4
94
+ chapter-03-custom-title.mp4
95
+ ```
96
+
97
+ When `--keep-intermediates` is enabled, intermediate files go to `output/.tmp/`:
98
+
99
+ | File Pattern | Description |
100
+ | --------------------- | ------------------------------------------------ |
101
+ | `*-raw.mp4` | Raw chapter clip with initial padding removed |
102
+ | `*-normalized.mp4` | Audio normalized (highpass + denoise + loudnorm) |
103
+ | `*-transcribe.wav` | Audio extracted for whisper |
104
+ | `*-transcribe.json` | Whisper JSON output |
105
+ | `*-transcribe.txt` | Whisper text output |
106
+ | `*-splice-*.mp4` | Segments before/after command windows |
107
+ | `*-spliced.mp4` | Concatenated output after command removal |
108
+ | `*.log` | Per-chapter skip/fallback logs |
109
+ | `process-summary.log` | Overall processing summary |
110
+
111
+ ## Voice Commands
112
+
113
+ Commands are spoken in the format: `jarvis <command> ... thanks`
114
+
115
+ | Command | Effect |
116
+ | --------------------------------------- | ----------------------- |
117
+ | `jarvis bad take thanks` | Skip the entire chapter |
118
+ | `jarvis filename my-custom-name thanks` | Rename output file |
119
+
120
+ The command window (from "jarvis" to "thanks") is removed from the final video.
121
+
122
+ ## More Details
123
+
124
+ Implementation notes and pipeline details live in `docs/README.md`.
125
+
126
+ This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com)
127
+ is a fast all-in-one JavaScript runtime.
44
128
 
45
- **Maintained for OIDC setup purposes only**
129
+ <!-- prettier-ignore-start -->
130
+ [build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/eprec/validate.yml?branch=main&logo=github&style=flat-square
131
+ [build]: https://github.com/epicweb-dev/eprec/actions?query=workflow%3Avalidate
132
+ [version-badge]: https://img.shields.io/npm/v/eprec.svg?style=flat-square
133
+ [package]: https://www.npmjs.com/package/eprec
134
+ [downloads-badge]: https://img.shields.io/npm/dm/eprec.svg?style=flat-square
135
+ [npmtrends]: https://www.npmtrends.com/eprec
136
+ [license-badge]: https://img.shields.io/npm/l/eprec.svg?style=flat-square
137
+ [license]: https://github.com/epicweb-dev/eprec/blob/main/LICENSE
138
+ <!-- prettier-ignore-end -->
@@ -0,0 +1,129 @@
1
+ :root {
2
+ color-scheme: light;
3
+ }
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
12
+ background: #f8fafc;
13
+ color: #0f172a;
14
+ }
15
+
16
+ h1,
17
+ h2,
18
+ h3,
19
+ p {
20
+ margin: 0;
21
+ }
22
+
23
+ .app-shell {
24
+ max-width: 960px;
25
+ margin: 0 auto;
26
+ padding: 48px 24px 72px;
27
+ }
28
+
29
+ .app-header {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 12px;
33
+ margin-bottom: 32px;
34
+ }
35
+
36
+ .app-kicker {
37
+ font-size: 12px;
38
+ letter-spacing: 0.16em;
39
+ text-transform: uppercase;
40
+ color: #64748b;
41
+ font-weight: 600;
42
+ }
43
+
44
+ .app-title {
45
+ font-size: 32px;
46
+ font-weight: 700;
47
+ }
48
+
49
+ .app-subtitle {
50
+ font-size: 16px;
51
+ color: #475569;
52
+ line-height: 1.6;
53
+ max-width: 640px;
54
+ }
55
+
56
+ .app-grid {
57
+ display: grid;
58
+ gap: 16px;
59
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
60
+ }
61
+
62
+ .app-card {
63
+ background: #ffffff;
64
+ border-radius: 16px;
65
+ border: 1px solid #e2e8f0;
66
+ padding: 20px;
67
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 12px;
71
+ }
72
+
73
+ .app-list {
74
+ margin: 0;
75
+ padding-left: 18px;
76
+ color: #334155;
77
+ line-height: 1.6;
78
+ }
79
+
80
+ .app-muted {
81
+ color: #64748b;
82
+ line-height: 1.5;
83
+ }
84
+
85
+ .status-pill {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: 6px;
89
+ padding: 4px 12px;
90
+ border-radius: 999px;
91
+ background: #e0f2fe;
92
+ color: #0369a1;
93
+ font-size: 12px;
94
+ font-weight: 600;
95
+ width: fit-content;
96
+ }
97
+
98
+ .counter-button {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ gap: 12px;
103
+ padding: 10px 14px;
104
+ border: none;
105
+ border-radius: 10px;
106
+ background: #0ea5e9;
107
+ color: #ffffff;
108
+ font-size: 14px;
109
+ font-weight: 600;
110
+ cursor: pointer;
111
+ transition: transform 0.15s ease, background 0.15s ease;
112
+ }
113
+
114
+ .counter-button:hover {
115
+ background: #0284c7;
116
+ }
117
+
118
+ .counter-button:disabled {
119
+ cursor: not-allowed;
120
+ opacity: 0.6;
121
+ }
122
+
123
+ .counter-button:active {
124
+ transform: translateY(1px);
125
+ }
126
+
127
+ .counter-value {
128
+ font-weight: 700;
129
+ }
@@ -0,0 +1,37 @@
1
+ import { Counter } from './counter.tsx'
2
+
3
+ export function App() {
4
+ return () => (
5
+ <main class="app-shell">
6
+ <header class="app-header">
7
+ <span class="app-kicker">Eprec Studio</span>
8
+ <h1 class="app-title">Editing workspace</h1>
9
+ <p class="app-subtitle">
10
+ Prepare edits with the CLI, then review them here.
11
+ </p>
12
+ </header>
13
+ <div class="app-grid">
14
+ <section class="app-card">
15
+ <h2>Workflow</h2>
16
+ <ol class="app-list">
17
+ <li>Run a CLI edit command.</li>
18
+ <li>Open the workspace UI.</li>
19
+ <li>Review and refine the cut list.</li>
20
+ </ol>
21
+ </section>
22
+ <section class="app-card">
23
+ <h2>UI status</h2>
24
+ <p class="status-pill">Running locally</p>
25
+ <p class="app-muted">
26
+ Server-rendered shell with client-side edits.
27
+ </p>
28
+ </section>
29
+ <section class="app-card">
30
+ <h2>Interaction check</h2>
31
+ <p class="app-muted">Click the counter to verify interactivity.</p>
32
+ <Counter setup={{ initial: 0 }} />
33
+ </section>
34
+ </div>
35
+ </main>
36
+ )
37
+ }
@@ -0,0 +1,22 @@
1
+ import type { Handle } from 'remix/component'
2
+
3
+ type CounterSetup = { initial?: number }
4
+
5
+ export function Counter(handle: Handle, props: { setup?: CounterSetup } = {}) {
6
+ let count = props.setup?.initial ?? 0
7
+ return () => (
8
+ <button
9
+ type="button"
10
+ class="counter-button"
11
+ on={{
12
+ click: () => {
13
+ count += 1
14
+ handle.update()
15
+ },
16
+ }}
17
+ >
18
+ <span>Click count</span>
19
+ <span class="counter-value">{count}</span>
20
+ </button>
21
+ )
22
+ }
@@ -0,0 +1,8 @@
1
+ import { createRoot } from 'remix/component'
2
+ import { App } from './app.tsx'
3
+
4
+ const rootElement = document.getElementById('root')
5
+ if (rootElement) {
6
+ rootElement.innerHTML = ''
7
+ }
8
+ createRoot(rootElement ?? document.body).render(<App />)
@@ -0,0 +1,37 @@
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
+ ${
31
+ entryScript
32
+ ? html`<script type="module" src="${entryScript}"></script>`
33
+ : ''
34
+ }
35
+ </body>
36
+ </html>`
37
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ if (!process.env.NODE_ENV) {
2
+ process.env.NODE_ENV = 'development'
3
+ }
@@ -0,0 +1,5 @@
1
+ import { route } from 'remix/fetch-router'
2
+
3
+ export default route({
4
+ index: '/',
5
+ })
@@ -0,0 +1,6 @@
1
+ import type { SafeHtml } from 'remix/html-template'
2
+ import { createHtmlResponse } from 'remix/response/html'
3
+
4
+ export function render(body: string | SafeHtml, init?: ResponseInit) {
5
+ return createHtmlResponse(body, init)
6
+ }
package/app/router.tsx ADDED
@@ -0,0 +1,102 @@
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, 'public'), { cacheControl }),
80
+ bunStaticFiles(path.join(rootDir, 'app'), {
81
+ filter: (pathname) => pathname.startsWith('assets/'),
82
+ cacheControl,
83
+ }),
84
+ ],
85
+ defaultHandler() {
86
+ return render(
87
+ Layout({
88
+ title: 'Not Found',
89
+ entryScript: false,
90
+ children: html`<main class="app-shell">
91
+ <h1 class="app-title">404 - Not Found</h1>
92
+ </main>`,
93
+ }),
94
+ { status: 404 },
95
+ )
96
+ },
97
+ })
98
+
99
+ router.map(routes.index, indexHandlers)
100
+
101
+ return router
102
+ }
@@ -0,0 +1,50 @@
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
+ Prepare edits with the CLI, then review them here.
17
+ </p>
18
+ </header>
19
+ <div class="app-grid">
20
+ <section class="app-card">
21
+ <h2>Workflow</h2>
22
+ <ol class="app-list">
23
+ <li>Run a CLI edit command.</li>
24
+ <li>Open the workspace UI.</li>
25
+ <li>Review and refine the cut list.</li>
26
+ </ol>
27
+ </section>
28
+ <section class="app-card">
29
+ <h2>UI status</h2>
30
+ <p class="status-pill">UI booted</p>
31
+ <p class="app-muted">
32
+ Client-side components load after the first paint.
33
+ </p>
34
+ </section>
35
+ <section class="app-card">
36
+ <h2>Interaction check</h2>
37
+ <p class="app-muted">Client bundle loads after this page.</p>
38
+ <button class="counter-button" type="button" disabled>
39
+ <span>Click count</span>
40
+ <span class="counter-value">0</span>
41
+ </button>
42
+ </section>
43
+ </div>
44
+ </main>`,
45
+ }),
46
+ )
47
+ },
48
+ }
49
+
50
+ export default indexHandler