diff --git a/package-lock.json b/package-lock.json index 33e3440..ae2d6d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4655,27 +4655,6 @@ "node": ">= 8" } }, - "node_modules/@remotion/renderer/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@remotion/sfx": { "version": "4.0.451", "resolved": "https://registry.npmjs.org/@remotion/sfx/-/sfx-4.0.451.tgz", @@ -6060,27 +6039,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -6869,9 +6827,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bare-events": { "version": "2.8.2", @@ -7021,14 +6984,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -7461,11 +7426,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "dev": true, @@ -16237,7 +16197,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index a4ed3ae..578293b 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,9 @@ "lightningcss-linux-x64-gnu": "1.30.2" }, "overrides": { - "postcss": "^8.5.10" + "postcss": "^8.5.10", + "ws": ">=8.21.0", + "brace-expansion": ">=5.0.6" }, "lint-staged": { "*.{ts,tsx}": "eslint --max-warnings=0", diff --git a/scripts/config/paths.test.ts b/scripts/config/paths.test.ts new file mode 100644 index 0000000..d0f32c2 --- /dev/null +++ b/scripts/config/paths.test.ts @@ -0,0 +1,172 @@ +import { + projectFile, + artifactDir, + transcribeInput, + transcriptRaw, + diarizationOutput, + hookQaDir, + transcriptOutput, + transcriptDoc, + syncVideoDir, + syncAudioDir, + syncOutputDir, + syncedVideo, + syncedVideoAngle, + cameraProfiles, + shortsDir, + shortClipDir, + shortTranscript, + shortDoc, + shortsCameraProfiles, + shortsTranscriptRaw, + carouselClipDir, + carouselTranscript, + carouselDoc, + thumbnailCameraProfiles, +} from './paths'; + +const ROOT = '/project/root'; + +describe('project / artifact', () => { + it('projectFile', () => { + expect(projectFile(ROOT)).toBe('/project/root/.ragtech/project.json'); + }); + + it('artifactDir', () => { + expect(artifactDir(ROOT)).toBe('/project/root/.ragtech/artifacts'); + }); +}); + +describe('transcribe pipeline', () => { + it('transcribeInput', () => { + expect(transcribeInput(ROOT)).toBe('/project/root/public/transcribe/input'); + }); + + it('transcriptRaw', () => { + expect(transcriptRaw(ROOT)).toBe('/project/root/public/transcribe/output/raw/transcript.raw.json'); + }); + + it('diarizationOutput', () => { + expect(diarizationOutput(ROOT)).toBe('/project/root/public/transcribe/output/raw/diarization.json'); + }); + + it('hookQaDir', () => { + expect(hookQaDir(ROOT)).toBe('/project/root/public/transcribe/output/hook-qa'); + }); +}); + +describe('edit / longform output', () => { + it('transcriptOutput', () => { + expect(transcriptOutput(ROOT)).toBe('/project/root/public/edit/transcript.json'); + }); + + it('transcriptDoc', () => { + expect(transcriptDoc(ROOT)).toBe('/project/root/public/edit/transcript.doc.txt'); + }); +}); + +describe('sync pipeline', () => { + it('syncVideoDir', () => { + expect(syncVideoDir(ROOT)).toBe('/project/root/public/sync/video'); + }); + + it('syncAudioDir', () => { + expect(syncAudioDir(ROOT)).toBe('/project/root/public/sync/audio'); + }); + + it('syncOutputDir', () => { + expect(syncOutputDir(ROOT)).toBe('/project/root/public/sync/output'); + }); + + it('syncedVideo', () => { + expect(syncedVideo(ROOT)).toBe('/project/root/public/sync/output/synced-output.mp4'); + }); + + it('syncedVideoAngle — index 1', () => { + expect(syncedVideoAngle(1, ROOT)).toBe('/project/root/public/sync/output/synced-output-1.mp4'); + }); + + it('syncedVideoAngle — index 2', () => { + expect(syncedVideoAngle(2, ROOT)).toBe('/project/root/public/sync/output/synced-output-2.mp4'); + }); + + it('syncedVideoAngle — index 0 throws', () => { + expect(() => syncedVideoAngle(0, ROOT)).toThrow(RangeError); + }); + + it('syncedVideoAngle — negative index throws', () => { + expect(() => syncedVideoAngle(-1, ROOT)).toThrow(RangeError); + }); +}); + +describe('camera', () => { + it('cameraProfiles', () => { + expect(cameraProfiles(ROOT)).toBe('/project/root/public/camera/camera-profiles.json'); + }); +}); + +describe('shorts pipeline', () => { + it('shortsDir', () => { + expect(shortsDir(ROOT)).toBe('/project/root/public/shorts'); + }); + + it('shortClipDir', () => { + expect(shortClipDir('clip-01', ROOT)).toBe('/project/root/public/shorts/clip-01'); + }); + + it('shortTranscript', () => { + expect(shortTranscript('clip-01', ROOT)).toBe('/project/root/public/shorts/clip-01/transcript.json'); + }); + + it('shortDoc', () => { + expect(shortDoc('clip-01', ROOT)).toBe('/project/root/public/shorts/clip-01/transcript.doc.txt'); + }); + + it('shortsCameraProfiles', () => { + expect(shortsCameraProfiles(ROOT)).toBe('/project/root/public/shorts/camera-profiles.json'); + }); + + it('shortsTranscriptRaw', () => { + expect(shortsTranscriptRaw(ROOT)).toBe( + '/project/root/public/shorts/transcribe/output/raw/transcript.raw.json', + ); + }); +}); + +describe('carousel pipeline', () => { + it('carouselClipDir', () => { + expect(carouselClipDir('ep-42', ROOT)).toBe('/project/root/public/carousel/ep-42'); + }); + + it('carouselTranscript', () => { + expect(carouselTranscript('ep-42', ROOT)).toBe('/project/root/public/carousel/ep-42/transcript.json'); + }); + + it('carouselDoc', () => { + expect(carouselDoc('ep-42', ROOT)).toBe('/project/root/public/carousel/ep-42/transcript.doc.txt'); + }); +}); + +describe('thumbnail pipeline', () => { + it('thumbnailCameraProfiles', () => { + expect(thumbnailCameraProfiles(ROOT)).toBe('/project/root/public/thumbnail/camera-profiles.json'); + }); +}); + +describe('cwd defaults', () => { + it('uses process.cwd() as root when cwd is omitted', () => { + const result = transcriptOutput(); + expect(result.startsWith(process.cwd())).toBe(true); + expect(result.endsWith('/public/edit/transcript.json')).toBe(true); + }); +}); + +describe('id guards', () => { + it('shortClipDir — empty string throws', () => { + expect(() => shortClipDir('', ROOT)).toThrow(TypeError); + }); + + it('carouselClipDir — empty string throws', () => { + expect(() => carouselClipDir('', ROOT)).toThrow(TypeError); + }); +}); diff --git a/scripts/config/paths.ts b/scripts/config/paths.ts new file mode 100644 index 0000000..4a1c7f4 --- /dev/null +++ b/scripts/config/paths.ts @@ -0,0 +1,176 @@ +/** + * Centralized path resolution for all pipeline scripts. + * + * All helpers are pure functions — no filesystem reads, no side effects. + * Pass `cwd` (defaults to `process.cwd()`) to get absolute paths. + * + * Internal dir helpers (not exported) are kept private to allow restructuring + * without breaking callers. Export only the paths scripts actually reference. + */ + +import path from 'path'; +import { PROJECT_DIR, PROJECT_FILENAME } from './project'; +import { ARTIFACTS_DIR } from './artifacts'; + +// --------------------------------------------------------------------------- +// Project / artifact store +// --------------------------------------------------------------------------- + +export function projectFile(cwd: string = process.cwd()): string { + return path.join(cwd, PROJECT_DIR, PROJECT_FILENAME); +} + +export function artifactDir(cwd: string = process.cwd()): string { + return path.join(cwd, ARTIFACTS_DIR); +} + +// --------------------------------------------------------------------------- +// Transcribe pipeline +// --------------------------------------------------------------------------- + +/** Directory where raw audio/video input files are placed before transcription. */ +export function transcribeInput(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'transcribe', 'input'); +} + +function transcribeRawDir(cwd: string): string { + return path.join(cwd, 'public', 'transcribe', 'output', 'raw'); +} + +export function transcriptRaw(cwd: string = process.cwd()): string { + return path.join(transcribeRawDir(cwd), 'transcript.raw.json'); +} + +export function diarizationOutput(cwd: string = process.cwd()): string { + return path.join(transcribeRawDir(cwd), 'diarization.json'); +} + +export function hookQaDir(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'transcribe', 'output', 'hook-qa'); +} + +// --------------------------------------------------------------------------- +// Edit / longform output +// --------------------------------------------------------------------------- + +export function transcriptOutput(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'edit', 'transcript.json'); +} + +export function transcriptDoc(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'edit', 'transcript.doc.txt'); +} + +// --------------------------------------------------------------------------- +// Sync pipeline +// --------------------------------------------------------------------------- + +export function syncVideoDir(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'sync', 'video'); +} + +export function syncAudioDir(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'sync', 'audio'); +} + +/** Directory containing synced output videos. Use for readdir; prefer {@link syncedVideo} or {@link syncedVideoAngle} for specific files. */ +export function syncOutputDir(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'sync', 'output'); +} + +/** + * Single-angle synced video (`synced-output.mp4`). + * For multi-angle recordings use {@link syncedVideoAngle} instead. + */ +export function syncedVideo(cwd: string = process.cwd()): string { + return path.join(syncOutputDir(cwd), 'synced-output.mp4'); +} + +/** + * Multi-angle synced video — produces `synced-output-{index}.mp4`. + * Index is 1-based. Throws if index < 1. + * For single-angle recordings use {@link syncedVideo} instead. + */ +export function syncedVideoAngle(index: number, cwd: string = process.cwd()): string { + if (index < 1) throw new RangeError(`syncedVideoAngle: index must be ≥ 1, got ${index}`); + return path.join(syncOutputDir(cwd), `synced-output-${index}.mp4`); +} + +// --------------------------------------------------------------------------- +// Camera +// --------------------------------------------------------------------------- + +function cameraProfilesDir(cwd: string): string { + return path.join(cwd, 'public', 'camera'); +} + +export function cameraProfiles(cwd: string = process.cwd()): string { + return path.join(cameraProfilesDir(cwd), 'camera-profiles.json'); +} + +// --------------------------------------------------------------------------- +// Shorts pipeline +// --------------------------------------------------------------------------- + +/** Root shorts directory. Use for existence checks and readdir. */ +export function shortsDir(cwd: string = process.cwd()): string { + return path.join(cwd, 'public', 'shorts'); +} + +export function shortClipDir(id: string, cwd: string = process.cwd()): string { + if (!id) throw new TypeError('shortClipDir: id must not be empty'); + return path.join(shortsDir(cwd), id); +} + +export function shortTranscript(id: string, cwd: string = process.cwd()): string { + return path.join(shortClipDir(id, cwd), 'transcript.json'); +} + +export function shortDoc(id: string, cwd: string = process.cwd()): string { + return path.join(shortClipDir(id, cwd), 'transcript.doc.txt'); +} + +export function shortsCameraProfiles(cwd: string = process.cwd()): string { + return path.join(shortsDir(cwd), 'camera-profiles.json'); +} + +function shortsTranscribeRawDir(cwd: string): string { + return path.join(shortsDir(cwd), 'transcribe', 'output', 'raw'); +} + +export function shortsTranscriptRaw(cwd: string = process.cwd()): string { + return path.join(shortsTranscribeRawDir(cwd), 'transcript.raw.json'); +} + +// --------------------------------------------------------------------------- +// Carousel pipeline +// --------------------------------------------------------------------------- + +function carouselDir(cwd: string): string { + return path.join(cwd, 'public', 'carousel'); +} + +export function carouselClipDir(id: string, cwd: string = process.cwd()): string { + if (!id) throw new TypeError('carouselClipDir: id must not be empty'); + return path.join(carouselDir(cwd), id); +} + +export function carouselTranscript(id: string, cwd: string = process.cwd()): string { + return path.join(carouselClipDir(id, cwd), 'transcript.json'); +} + +export function carouselDoc(id: string, cwd: string = process.cwd()): string { + return path.join(carouselClipDir(id, cwd), 'transcript.doc.txt'); +} + +// --------------------------------------------------------------------------- +// Thumbnail pipeline +// --------------------------------------------------------------------------- + +function thumbnailDir(cwd: string): string { + return path.join(cwd, 'public', 'thumbnail'); +} + +export function thumbnailCameraProfiles(cwd: string = process.cwd()): string { + return path.join(thumbnailDir(cwd), 'camera-profiles.json'); +}