From 514c12abee4285508df61ae92b6db12c0343b45a Mon Sep 17 00:00:00 2001 From: enieuwy Date: Mon, 20 Apr 2026 12:18:15 +0800 Subject: [PATCH 1/4] Add missing projectsDir() function causing 'Failed to save project' The project save handler (and all other project API handlers) called projectsDir() as a function, but no such function existed. Only studioSettings.projectsDir (a string) was defined. This caused a TypeError caught by the bare catch block, surfaced as the generic 'Failed to save project' error. Also improves the catch block to log and return the actual error message instead of swallowing it. --- apps/editor-web/vite.config.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/editor-web/vite.config.ts b/apps/editor-web/vite.config.ts index ed6c602..1541387 100644 --- a/apps/editor-web/vite.config.ts +++ b/apps/editor-web/vite.config.ts @@ -383,6 +383,11 @@ function sanitizeProjectName(raw: string): string { return name.replace(/[^a-zA-Z0-9 _.-]/g, "_").slice(0, 80) || "Project"; } +function projectsDir(): string { + return path.resolve(studioSettings.projectsDir || DEFAULT_SETTINGS.projectsDir); +} + + function normalizeRange(input: RangeInput): { startSec: number; endSec: number } | null { const start = Number(input.sourceStartSec ?? input.startSec); const end = Number(input.sourceEndSec ?? input.endSec); @@ -1192,9 +1197,10 @@ function studioApiPlugin(): Plugin { fs.writeFileSync(out, JSON.stringify(payload, null, 2), "utf-8"); res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ ok: true, projectId: id, projectName, outputPath: out })); - } catch { + } catch (err) { + console.error("Failed to save project:", err); res.statusCode = 500; - res.end(JSON.stringify({ error: "Failed to save project" })); + res.end(JSON.stringify({ error: `Failed to save project: ${err instanceof Error ? err.message : String(err)}` })); } }); @@ -1882,6 +1888,7 @@ function studioApiPlugin(): Plugin { } pushLog(job, `Running Whisper (${model}, ${device}, ${computeType}, beam=${beamSize}${vadFilter ? ", vad" : ""})`); + let whisperStderr = ""; const tr = spawn("bash", ["-lc", `${command} "${wavPath}" --model "${model}" --device "${device}" --compute-type "${computeType}" --beam-size "${beamSize}" ${vadFilter ? "--vad-filter" : ""} --language "${language}" --out "${transcriptAbsPath}"`], { cwd: REPO_ROOT, }); @@ -1896,6 +1903,7 @@ function studioApiPlugin(): Plugin { }); tr.stderr.on("data", (d) => { const text = String(d); + whisperStderr += text; const sec = parseWhisperProgressSec(text); if (sec !== undefined) { job.transcribedSec = Math.max(job.transcribedSec ?? 0, sec); @@ -1913,7 +1921,8 @@ function studioApiPlugin(): Plugin { } else { job.status = "error"; job.phase = "error"; - job.error = `Whisper failed (${trCode})`; + const tail = whisperStderr.trim().split("\n").slice(-5).join(" | "); + job.error = `Whisper failed (${trCode}): ${tail || "no stderr"}`; } }); }); From 8be7d1182470231c1f9ef9ead6e52011d4e39af3 Mon Sep 17 00:00:00 2001 From: enieuwy Date: Wed, 22 Apr 2026 11:07:35 +0800 Subject: [PATCH 2/4] chore(editor-web): add missing @types dependencies and vite env declaration --- apps/editor-web/package.json | 3 ++ apps/editor-web/src/vite-env.d.ts | 1 + package-lock.json | 53 ++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 apps/editor-web/src/vite-env.d.ts diff --git a/apps/editor-web/package.json b/apps/editor-web/package.json index 1170cc2..f256c39 100644 --- a/apps/editor-web/package.json +++ b/apps/editor-web/package.json @@ -14,6 +14,9 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.2", "typescript": "^5.6.2", "vite": "^5.4.8" diff --git a/apps/editor-web/src/vite-env.d.ts b/apps/editor-web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/editor-web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/package-lock.json b/package-lock.json index 2c3eced..79c14da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,11 @@ "workspaces": [ "apps/*", "packages/*" - ] + ], + "engines": { + "node": ">=20 <21", + "npm": ">=9 <10" + } }, "apps/editor-web": { "name": "@prune/editor-web", @@ -21,6 +25,9 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.2", "typescript": "^5.6.2", "vite": "^5.4.8" @@ -1100,6 +1107,36 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1191,6 +1228,13 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1512,6 +1556,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", From a605237c44b9c6eebba472c9b01dbedaf270f2fb Mon Sep 17 00:00:00 2001 From: enieuwy Date: Wed, 22 Apr 2026 11:07:43 +0800 Subject: [PATCH 3/4] fix(editor-web): fix AAF export bugs and declare missing export job maps in vite config --- apps/editor-web/vite.config.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/editor-web/vite.config.ts b/apps/editor-web/vite.config.ts index 1541387..f55f801 100644 --- a/apps/editor-web/vite.config.ts +++ b/apps/editor-web/vite.config.ts @@ -130,6 +130,10 @@ type SubtitleTokenInput = { const jobs = new Map(); const exportJobs = new Map(); const fcpxmlJobs = new Map(); +const edlJobs = new Map(); +const premiereXmlJobs = new Map(); +const aeMarkerJobs = new Map(); +const aafBridgeJobs = new Map(); const RENDER_STATUS_PATH = path.resolve(REPO_ROOT, "data", "render-status.json"); let latestRenderStatus: RenderStatusSnapshot = (() => { @@ -384,7 +388,7 @@ function sanitizeProjectName(raw: string): string { } function projectsDir(): string { - return path.resolve(studioSettings.projectsDir || DEFAULT_SETTINGS.projectsDir); + return path.resolve(studioSettings.projectsDir || DEFAULT_SETTINGS.projectsDir || "data/projects"); } @@ -2616,7 +2620,7 @@ function studioApiPlugin(): Plugin { return; } - const sourceMetadata = probeSourceMetadata(absMedia); + const sourceMetadata = probeFcpxmlMetadata(absMedia); const source = { path: absMedia, name: path.basename(absMedia), @@ -2625,17 +2629,18 @@ function studioApiPlugin(): Plugin { durationSec: sourceMetadata.durationSec, }; - const manifest = buildAafBridgeManifest(keepRanges as KeepRange[], source); - const fcpxml = exportFcpxmlV1(keepRanges as KeepRange[], source, { + const timelineRanges = toTimelineKeepRanges(keepRanges); + const manifest = buildAafBridgeManifest(timelineRanges, source); + const fcpxml = exportFcpxmlV1(timelineRanges, source, { projectName: outputName.replace(/-aaf-bridge\.zip$/, ""), sequenceName: outputName.replace(/-aaf-bridge\.zip$/, ""), eventName: "prune", }); - const edl = exportEdlCmx3600(keepRanges as KeepRange[], source, { + const edl = exportEdlCmx3600(timelineRanges, source, { title: outputName.replace(/\.zip$/, "").toUpperCase().slice(0, 64), reel: source.name, }); - const premiereXml = exportPremiereXml(keepRanges as KeepRange[], source, { + const premiereXml = exportPremiereXml(timelineRanges, source, { projectName: outputName.replace(/-aaf-bridge\.zip$/, ""), sequenceName: outputName.replace(/-aaf-bridge\.zip$/, ""), }); From 943f7723f8b89e8dfee1f76a087de6c6c33894b0 Mon Sep 17 00:00:00 2001 From: enieuwy Date: Wed, 22 Apr 2026 11:07:52 +0800 Subject: [PATCH 4/4] fix(editor-web): apply strict types for status and phase in job polling hooks --- apps/editor-web/src/hooks/useJobPolling.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/editor-web/src/hooks/useJobPolling.ts b/apps/editor-web/src/hooks/useJobPolling.ts index 68a51a3..982f3bb 100644 --- a/apps/editor-web/src/hooks/useJobPolling.ts +++ b/apps/editor-web/src/hooks/useJobPolling.ts @@ -3,14 +3,14 @@ import { fetchJsonSafe, normalizeRunningStatus, startPolling } from "../utils/ap type TranscribeStateLike = { jobId: string | null; - status: string; + status: "idle" | "starting" | "running" | "done" | "error"; log: string[]; error: string | null; transcriptRelPath: string | null; startedAt?: number; mediaDurationSec?: number; transcribedSec?: number; - phase?: string; + phase?: "queued" | "extracting" | "transcribing" | "finalizing" | "done" | "error"; percent?: number | null; etaSec?: number | null; speedLabel?: string | null; @@ -18,7 +18,7 @@ type TranscribeStateLike = { type ExportStateLike = { jobId: string | null; - status: string; + status: "idle" | "starting" | "running" | "done" | "error"; outputPath: string | null; error: string | null; log: string[];