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/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[]; 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/apps/editor-web/vite.config.ts b/apps/editor-web/vite.config.ts index ed6c602..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 = (() => { @@ -383,6 +387,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 || "data/projects"); +} + + 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 +1201,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 +1892,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 +1907,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 +1925,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"}`; } }); }); @@ -2607,7 +2620,7 @@ function studioApiPlugin(): Plugin { return; } - const sourceMetadata = probeSourceMetadata(absMedia); + const sourceMetadata = probeFcpxmlMetadata(absMedia); const source = { path: absMedia, name: path.basename(absMedia), @@ -2616,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$/, ""), }); 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",