@tikagent/video-editor-cli 0.0.0-stage → 1.5.0-preview.1
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/LICENSE +82 -0
- package/THIRD-PARTY-NOTICES.md +21 -0
- package/dist/bin/tik-video-editor-cli.js +2 -0
- package/dist/lib/asr.js +1 -0
- package/dist/lib/audio.js +1 -0
- package/dist/lib/auth.js +1 -0
- package/dist/lib/browser-zip.js +1 -0
- package/dist/lib/browser.js +1 -0
- package/dist/lib/cli.js +1 -0
- package/dist/lib/doctor.js +1 -0
- package/dist/lib/download.js +1 -0
- package/dist/lib/export-progress.js +1 -0
- package/dist/lib/import-video.js +1 -0
- package/dist/lib/local.js +1 -0
- package/dist/lib/media.js +1 -0
- package/dist/lib/operation.js +1 -0
- package/dist/lib/preset.js +1 -0
- package/dist/lib/processes.js +1 -0
- package/dist/lib/self.js +1 -0
- package/dist/lib/service-urls.js +1 -0
- package/dist/lib/storage.js +1 -0
- package/dist/runtime/LICENSE +21 -0
- package/dist/runtime/analyze.js +1 -0
- package/dist/runtime/app.js +1 -0
- package/dist/runtime/cli/lib/browser-zip.js +1 -0
- package/dist/runtime/cli/lib/browser.js +1 -0
- package/dist/runtime/cli/lib/processes.js +1 -0
- package/dist/runtime/export-audio.js +1 -0
- package/dist/runtime/export-cache.js +1 -0
- package/dist/runtime/export-encoder.js +1 -0
- package/dist/runtime/export-save.js +1 -0
- package/dist/runtime/export-worker.js +1 -0
- package/dist/runtime/favicon.svg +14 -0
- package/dist/runtime/i18n.js +1 -0
- package/dist/runtime/icons/favicon-32.png +0 -0
- package/dist/runtime/icons/tik-logo.png +0 -0
- package/dist/runtime/index.html +320 -0
- package/dist/runtime/library/README.md +12 -0
- package/dist/runtime/library/elements/bokeh-drift.svg +19 -0
- package/dist/runtime/library/elements/light-leak-cool.svg +16 -0
- package/dist/runtime/library/elements/light-leak-warm.svg +22 -0
- package/dist/runtime/library/elements/vhs-scanlines.svg +18 -0
- package/dist/runtime/library/elements/vignette-frame.svg +14 -0
- package/dist/runtime/library/fonts//351/230/277/351/207/214/345/267/264/345/267/264/346/231/256/346/203/240/344/275/223-/344/270/255/351/273/221.woff2 +0 -0
- package/dist/runtime/library/fonts//351/230/277/351/207/214/345/267/264/345/267/264/346/231/256/346/203/240/344/275/223-/345/270/270/350/247/204.woff2 +0 -0
- package/dist/runtime/library/fonts//351/230/277/351/207/214/345/267/264/345/267/264/346/231/256/346/203/240/344/275/223-/347/262/227/344/275/223.woff2 +0 -0
- package/dist/runtime/library/sfx/README.md +13 -0
- package/dist/runtime/library/svg/arrow-bounce.svg +18 -0
- package/dist/runtime/library/svg/arrow-scribble.svg +13 -0
- package/dist/runtime/library/svg/burst-lines.svg +30 -0
- package/dist/runtime/library/svg/checkmark-success.svg +19 -0
- package/dist/runtime/library/svg/confetti-burst.svg +23 -0
- package/dist/runtime/library/svg/countdown-ring.svg +92 -0
- package/dist/runtime/library/svg/equalizer.svg +18 -0
- package/dist/runtime/library/svg/glow-orbs.svg +29 -0
- package/dist/runtime/library/svg/heart-pop.svg +26 -0
- package/dist/runtime/library/svg/loading-dots.svg +16 -0
- package/dist/runtime/library/svg/location-pin.svg +25 -0
- package/dist/runtime/library/svg/lower-third.svg +17 -0
- package/dist/runtime/library/svg/new-badge.svg +18 -0
- package/dist/runtime/library/svg/progress-bar.svg +16 -0
- package/dist/runtime/library/svg/radial-pulse.svg +19 -0
- package/dist/runtime/library/svg/rating-stars.svg +25 -0
- package/dist/runtime/library/svg/rec-indicator.svg +41 -0
- package/dist/runtime/library/svg/sparkles.svg +17 -0
- package/dist/runtime/library/svg/speech-bubble.svg +23 -0
- package/dist/runtime/library/svg/subscribe-bell.svg +29 -0
- package/dist/runtime/library/svg/subscribe-pill.svg +22 -0
- package/dist/runtime/library/svg/swipe-up.svg +46 -0
- package/dist/runtime/library/svg/underline-swoosh.svg +15 -0
- package/dist/runtime/manifest.json +40 -0
- package/dist/runtime/meter-worklet.js +1 -0
- package/dist/runtime/optimized-encoder-worker.js +1 -0
- package/dist/runtime/optimized-export.js +1 -0
- package/dist/runtime/paths.js +1 -0
- package/dist/runtime/project-store.js +1 -0
- package/dist/runtime/ruler-worker.js +1 -0
- package/dist/runtime/server.js +1 -0
- package/dist/runtime/style.css +2126 -0
- package/dist/runtime/timeline.js +1 -0
- package/dist/runtime/waveform.js +1 -0
- package/package.json +37 -4
- package/scripts/build-package.js +69 -0
- package/scripts/next-version.js +56 -0
- package/scripts/pack-runtime.js +26 -0
- package/scripts/release-channel.js +115 -0
- package/README.md +0 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
|
|
2
|
+
<!-- FableCut animated SVG · loops · twinkling sparkles overlay -->
|
|
3
|
+
<style>
|
|
4
|
+
.sp { transform-box: fill-box; transform-origin: center; animation: tw 1.6s ease-in-out infinite; }
|
|
5
|
+
@keyframes tw {
|
|
6
|
+
0%, 100% { transform: scale(0.2) rotate(0deg); opacity: 0; }
|
|
7
|
+
50% { transform: scale(1) rotate(90deg); opacity: 1; }
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
<g fill="#fff8d8">
|
|
11
|
+
<path class="sp" style="--d:0s" d="M120 90 l10 26 26 10 -26 10 -10 26 -10 -26 -26 -10 26 -10z"/>
|
|
12
|
+
<path class="sp" style="--d:0.35s" d="M640 70 l8 20 20 8 -20 8 -8 20 -8 -20 -20 -8 20 -8z"/>
|
|
13
|
+
<path class="sp" style="--d:0.7s" d="M420 180 l12 30 30 12 -30 12 -12 30 -12 -30 -30 -12 30 -12z"/>
|
|
14
|
+
<path class="sp" style="--d:1.05s" d="M200 320 l8 20 20 8 -20 8 -8 20 -8 -20 -20 -8 20 -8z"/>
|
|
15
|
+
<path class="sp" style="--d:1.3s" d="M660 330 l10 24 24 10 -24 10 -10 24 -10 -24 -24 -10 24 -10z"/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="360" viewBox="0 0 500 360">
|
|
2
|
+
<!-- FableCut animated SVG · bubble pops in (~0.5s), then typing dots loop (chat / comment) -->
|
|
3
|
+
<style>
|
|
4
|
+
.bub { transform-box: fill-box; transform-origin: 250px 280px;
|
|
5
|
+
animation: pop 0.5s cubic-bezier(.34,1.56,.64,1) both; }
|
|
6
|
+
@keyframes pop { 0% { transform: scale(0); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
|
|
7
|
+
.td { transform-box: fill-box; transform-origin: center;
|
|
8
|
+
animation: type 1.2s ease-in-out infinite; }
|
|
9
|
+
@keyframes type {
|
|
10
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
11
|
+
30% { transform: translateY(-12px); opacity: 1; }
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
<g class="bub" style="--d:0s">
|
|
15
|
+
<rect x="60" y="50" width="380" height="200" rx="40" fill="#ffffff"/>
|
|
16
|
+
<path d="M180 245 L150 320 L250 245 Z" fill="#ffffff"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#7b6cff">
|
|
19
|
+
<circle class="td" style="--d:0.5s" cx="185" cy="150" r="20"/>
|
|
20
|
+
<circle class="td" style="--d:0.65s" cx="250" cy="150" r="20"/>
|
|
21
|
+
<circle class="td" style="--d:0.8s" cx="315" cy="150" r="20"/>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500">
|
|
2
|
+
<!-- FableCut animated SVG · one-shot pop then repeated ring-swing (notification / subscribe) -->
|
|
3
|
+
<style>
|
|
4
|
+
.badge { transform-box: fill-box; transform-origin: center;
|
|
5
|
+
animation: pop 0.5s cubic-bezier(.34,1.56,.64,1) both; }
|
|
6
|
+
@keyframes pop { 0% { transform: scale(0); } 100% { transform: scale(1); } }
|
|
7
|
+
.bell { transform-box: fill-box; transform-origin: 250px 150px;
|
|
8
|
+
animation: swing 2.2s ease-in-out infinite; }
|
|
9
|
+
@keyframes swing {
|
|
10
|
+
0%, 60%, 100% { transform: rotate(0deg); }
|
|
11
|
+
68% { transform: rotate(16deg); }
|
|
12
|
+
76% { transform: rotate(-13deg); }
|
|
13
|
+
84% { transform: rotate(8deg); }
|
|
14
|
+
92% { transform: rotate(-4deg); }
|
|
15
|
+
}
|
|
16
|
+
.clap { transform-box: fill-box; transform-origin: 250px 150px;
|
|
17
|
+
animation: swing 2.2s ease-in-out infinite; }
|
|
18
|
+
.dot { transform-box: fill-box; transform-origin: center;
|
|
19
|
+
animation: ping 2.2s ease-out infinite; }
|
|
20
|
+
@keyframes ping { 0%,60% { transform: scale(0); opacity: 0; } 70% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }
|
|
21
|
+
</style>
|
|
22
|
+
<circle class="badge" style="--d:0s" cx="250" cy="250" r="180" fill="#ff5d73"/>
|
|
23
|
+
<g class="bell" style="--d:0.5s" fill="#fff">
|
|
24
|
+
<path d="M250 150 c-58 0 -84 44 -84 104 c0 56 -20 74 -30 90 l228 0 c-10 -16 -30 -34 -30 -90 c0 -60 -26 -104 -84 -104z"/>
|
|
25
|
+
<circle cx="250" cy="146" r="16"/>
|
|
26
|
+
</g>
|
|
27
|
+
<path class="clap" style="--d:0.5s" d="M228 356 a22 22 0 0 0 44 0z" fill="#fff"/>
|
|
28
|
+
<circle class="dot" style="--d:0.5s" cx="372" cy="128" r="26" fill="#ffd166"/>
|
|
29
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="160" viewBox="0 0 560 160">
|
|
2
|
+
<!-- FableCut animated SVG · one-shot (~1.4s) · subscribe pill + ringing bell -->
|
|
3
|
+
<style>
|
|
4
|
+
.pill { transform-box: fill-box; transform-origin: center; animation: pop 0.5s cubic-bezier(.34,1.56,.64,1) both; }
|
|
5
|
+
.txt { animation: fadein 0.35s ease both; }
|
|
6
|
+
.bell { transform-box: fill-box; transform-origin: 50% 15%; animation: ring 0.7s ease-in-out both; }
|
|
7
|
+
@keyframes pop { from { transform: scale(0); } to { transform: scale(1); } }
|
|
8
|
+
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
|
|
9
|
+
@keyframes ring { 0%,100% { transform: rotate(0); } 20% { transform: rotate(24deg); }
|
|
10
|
+
45% { transform: rotate(-18deg); } 70% { transform: rotate(10deg); } }
|
|
11
|
+
</style>
|
|
12
|
+
<g class="pill" style="--d:0s">
|
|
13
|
+
<rect x="20" y="30" width="430" height="100" rx="50" fill="#e11d48"/>
|
|
14
|
+
<text class="txt" style="--d:0.35s" x="235" y="97" text-anchor="middle"
|
|
15
|
+
font-family="Arial, sans-serif" font-size="46" font-weight="800" fill="#ffffff"
|
|
16
|
+
letter-spacing="2">SUBSCRIBE</text>
|
|
17
|
+
</g>
|
|
18
|
+
<g class="bell" style="--d:0.6s">
|
|
19
|
+
<path d="M505 45 c-18 0 -30 14 -30 34 v18 l-10 14 h80 l-10 -14 v-18 c0 -20 -12 -34 -30 -34z" fill="#ffd166"/>
|
|
20
|
+
<circle cx="505" cy="118" r="8" fill="#ffd166"/>
|
|
21
|
+
</g>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="200" viewBox="0 0 120 200">
|
|
2
|
+
<!-- FableCut animated SVG · loop · "swipe up" hand gesture
|
|
3
|
+
Bouncing upward arrow/hand for social-media CTAs. -->
|
|
4
|
+
<style>
|
|
5
|
+
.hand {
|
|
6
|
+
transform-box: fill-box;
|
|
7
|
+
transform-origin: center bottom;
|
|
8
|
+
animation: bounce 1s cubic-bezier(.36,0,.66,1) infinite;
|
|
9
|
+
}
|
|
10
|
+
@keyframes bounce {
|
|
11
|
+
0%, 100% { transform: translateY(0); }
|
|
12
|
+
40% { transform: translateY(-30px); }
|
|
13
|
+
60% { transform: translateY(-30px); }
|
|
14
|
+
}
|
|
15
|
+
.arrow {
|
|
16
|
+
stroke: #ffffff;
|
|
17
|
+
stroke-width: 4;
|
|
18
|
+
stroke-linecap: round;
|
|
19
|
+
stroke-linejoin: round;
|
|
20
|
+
fill: none;
|
|
21
|
+
opacity: 0.9;
|
|
22
|
+
}
|
|
23
|
+
.fade-arrow {
|
|
24
|
+
animation: fadeUp 1s ease-in-out infinite;
|
|
25
|
+
animation-delay: var(--d, 0s);
|
|
26
|
+
}
|
|
27
|
+
@keyframes fadeUp {
|
|
28
|
+
0%, 100% { opacity: 0.2; transform: translateY(10px); }
|
|
29
|
+
40% { opacity: 0.9; transform: translateY(-10px); }
|
|
30
|
+
60% { opacity: 0.9; transform: translateY(-10px); }
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
<!-- Trailing chevrons (fade up) -->
|
|
34
|
+
<g class="fade-arrow" style="--d:0s">
|
|
35
|
+
<polyline class="arrow" points="30,140 60,110 90,140"/>
|
|
36
|
+
</g>
|
|
37
|
+
<g class="fade-arrow" style="--d:0.15s">
|
|
38
|
+
<polyline class="arrow" points="30,170 60,140 90,170" opacity="0.5"/>
|
|
39
|
+
</g>
|
|
40
|
+
<!-- Hand / main chevron -->
|
|
41
|
+
<g class="hand">
|
|
42
|
+
<polyline class="arrow" points="20,100 60,55 100,100" stroke-width="5"/>
|
|
43
|
+
<!-- Palm base -->
|
|
44
|
+
<rect x="45" y="95" width="30" height="12" rx="6" fill="#ffffff" opacity="0.85"/>
|
|
45
|
+
</g>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="120" viewBox="0 0 600 120">
|
|
2
|
+
<!-- FableCut animated SVG · one-shot (~0.8s) · hand-drawn underline swoosh.
|
|
3
|
+
Place under a title text clip; tint via the clip's hue slider. -->
|
|
4
|
+
<style>
|
|
5
|
+
.stroke {
|
|
6
|
+
stroke-dasharray: 640; stroke-dashoffset: 640;
|
|
7
|
+
animation: draw 0.8s cubic-bezier(.3,.8,.3,1) both;
|
|
8
|
+
}
|
|
9
|
+
@keyframes draw { to { stroke-dashoffset: 0; } }
|
|
10
|
+
</style>
|
|
11
|
+
<path class="stroke" style="--d:0s" d="M20 70 C 180 40, 420 40, 580 62" fill="none"
|
|
12
|
+
stroke="#ffd166" stroke-width="14" stroke-linecap="round"/>
|
|
13
|
+
<path class="stroke" style="--d:0.18s" d="M60 96 C 220 74, 380 74, 540 90" fill="none"
|
|
14
|
+
stroke="#ffd166" stroke-width="9" stroke-linecap="round" opacity="0.7"/>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.2",
|
|
3
|
+
"name": "fablecut",
|
|
4
|
+
"display_name": "FableCut",
|
|
5
|
+
"version": "1.7.0",
|
|
6
|
+
"description": "Zero-dependency browser video editor that AI agents can drive — JSON timeline, MCP tools, live-reloading UI.",
|
|
7
|
+
"long_description": "FableCut is a Premiere-style non-linear video editor that runs in the browser with zero npm dependencies. An AI agent edits video by patching a single project.json timeline (clips, keyframes, transitions, text animations, filters, speed ramps, adjustment layers) through MCP tools; the open editor UI live-reloads within ~150 ms and exports MP4 via ffmpeg. Includes reference-video analysis (shot cuts, beats, BPM, energy, drop) so agents can remake an edit with new footage.",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Ronak Parmar",
|
|
10
|
+
"url": "https://github.com/ronak-create"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/ronak-create/FableCut"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://ronak-create.github.io/FableCut/",
|
|
17
|
+
"documentation": "https://github.com/ronak-create/FableCut/blob/main/CLAUDE.md",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"keywords": ["video-editor", "nle", "timeline", "canvas", "mcp", "ai-agent", "ffmpeg"],
|
|
20
|
+
"server": {
|
|
21
|
+
"type": "node",
|
|
22
|
+
"entry_point": "mcp-server.js",
|
|
23
|
+
"mcp_config": {
|
|
24
|
+
"command": "node",
|
|
25
|
+
"args": ["${__dirname}/mcp-server.js"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"tools": [
|
|
29
|
+
{ "name": "fablecut_status", "description": "Auto-start the editor server and return its URL plus a project summary" },
|
|
30
|
+
{ "name": "fablecut_docs", "description": "Return the full editor manual (project.json schema, props, recipes), optionally a single section" },
|
|
31
|
+
{ "name": "fablecut_get_project", "description": "Read the timeline JSON (compact mode available for token-efficient planning)" },
|
|
32
|
+
{ "name": "fablecut_set_project", "description": "Replace the timeline JSON with optimistic-concurrency conflict detection" },
|
|
33
|
+
{ "name": "fablecut_patch_project", "description": "Apply targeted, merge-safe ops (add/update/remove clip or media, set project fields)" },
|
|
34
|
+
{ "name": "fablecut_import_media", "description": "Copy a local file into the project's media folder and register it" },
|
|
35
|
+
{ "name": "fablecut_analyze_reference", "description": "Analyze a reference video into an edit blueprint: shot cuts, beats, BPM, energy curve, drop; extracts its music" }
|
|
36
|
+
],
|
|
37
|
+
"compatibility": {
|
|
38
|
+
"runtimes": { "node": ">=18" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function shelfCoeffs(s){const t=.7071752369554196,h=Math.tan(1681.974450955533*Math.PI/s),e=Math.pow(10,.19999219269866736),i=Math.pow(e,.5),a=1+h/t+h*h;return{b0:(e+i*h/t+h*h)/a,b1:2*(h*h-e)/a,b2:(e-i*h/t+h*h)/a,a1:2*(h*h-1)/a,a2:(1-h/t+h*h)/a}}function hpfCoeffs(s){const t=.5003270373238773,h=Math.tan(38.13547087613982*Math.PI/s),e=1+h/t+h*h;return{b0:1/e,b1:-2/e,b2:1/e,a1:2*(h*h-1)/e,a2:(1-h/t+h*h)/e}}function makeBiquad(s){return{b0:s.b0,b1:s.b1,b2:s.b2,a1:s.a1,a2:s.a2,z1:0,z2:0}}function biquadStep(s,t){const h=s.b0*t+s.z1;return s.z1=s.b1*t-s.a1*h+s.z2,s.z2=s.b2*t-s.a2*h,h}class FableCutMeterProcessor extends AudioWorkletProcessor{constructor(s){super();const t=s&&s.processorOptions||{};this._hopBlocks=Math.max(1,t.hopBlocks||8),this._nTracks=Math.max(1,t.nTracks||1),this._ids=t.trackIds||[],this._block=0,this._sumSq=new Float64Array(this._nTracks),this._peak=new Float64Array(this._nTracks),this._sumSqK=new Float64Array(this._nTracks),this._frames=0;const h=shelfCoeffs(sampleRate),e=hpfCoeffs(sampleRate);this._shelfL=[],this._hpfL=[],this._shelfR=[],this._hpfR=[];for(let s=0;s<this._nTracks;s++)this._shelfL.push(makeBiquad(h)),this._hpfL.push(makeBiquad(e)),this._shelfR.push(makeBiquad(h)),this._hpfR.push(makeBiquad(e));const i=128*this._hopBlocks;this._lufsLen=Math.max(1,Math.round(.4*sampleRate/i)),this._lufsRing=[],this._lufsIdx=[],this._lufsFilled=[];for(let s=0;s<this._nTracks;s++)this._lufsRing.push(new Float64Array(this._lufsLen)),this._lufsIdx.push(0),this._lufsFilled.push(0)}process(s,t){const h=t[0],e=h&&h[0],i=h&&(h[1]||h[0]);e&&e.fill(0),i&&i!==e&&i.fill(0);let a=0;for(let t=0;t<this._nTracks;t++){const h=s[t],l=h&&h[0],r=l?h[0]:null,o=l?h[1]||h[0]:null,_=r?r.length:e?e.length:128;a=_;let n=this._sumSq[t],u=this._peak[t],f=this._sumSqK[t];const c=this._shelfL[t],p=this._hpfL[t],k=this._shelfR[t],m=this._hpfR[t];for(let s=0;s<_;s++){const t=r?r[s]:0,h=o?o[s]:0,a=.5*(t+h);n+=a*a;const l=t>=0?t:-t,_=h>=0?h:-h;l>u&&(u=l),_>u&&(u=_);const b=biquadStep(p,biquadStep(c,t)),d=biquadStep(m,biquadStep(k,h));f+=b*b+d*d,e&&(e[s]+=t),i&&i!==e&&(i[s]+=h)}this._sumSq[t]=n,this._peak[t]=u,this._sumSqK[t]=f}if(a&&(this._frames+=a),this._block++,this._block>=this._hopBlocks){const s=Math.max(1,this._frames),t=new Array(this._nTracks),h=new Array(this._nTracks),e=new Array(this._nTracks);for(let i=0;i<this._nTracks;i++){t[i]=Math.sqrt(this._sumSq[i]/s),h[i]=this._peak[i];const a=this._sumSqK[i]/s,l=this._lufsRing[i],r=this._lufsIdx[i];l[r]=a,this._lufsIdx[i]=(r+1)%this._lufsLen,this._lufsFilled[i]<this._lufsLen&&this._lufsFilled[i]++;let o=0;const _=this._lufsFilled[i];for(let s=0;s<_;s++)o+=l[s];const n=o/Math.max(1,_);e[i]=n>1e-12?10*Math.log10(n)-.691:-70,this._sumSq[i]=0,this._peak[i]=0,this._sumSqK[i]=0}this.port.postMessage({type:"meter",trackIds:this._ids,rms:t,peak:h,lufs:e,frames:s}),this._block=0,this._frames=0}return!0}}registerProcessor("fablecut-meter",FableCutMeterProcessor);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";let e,t,s=Promise.resolve();self.onmessage=({data:{id:a,bitmap:i}})=>{s=s.then(async()=>{try{e&&e.width===i.width&&e.height===i.height||(e=new OffscreenCanvas(i.width,i.height),t=e.getContext("2d")),t.drawImage(i,0,0);const s=await e.convertToBlob({type:"image/jpeg",quality:.95});self.postMessage({id:a,blob:s})}catch(e){self.postMessage({id:a,error:e.message})}finally{i.close()}})};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";class OrderedFrameQueue{constructor(e,t=4,a=33554432){this.write=e,this.maxFrames=t,this.maxBytes=a,this.pending=[],this.bytes=0,this.peakBytes=0,this.error=null}async push(e){for(;this.pending.length&&(this.pending.length>=this.maxFrames||this.bytes+e.size>this.maxBytes);)await this.pending[0];if(this.error)throw this.error;this.bytes+=e.size,this.peakBytes=Math.max(this.peakBytes,this.bytes);const t=(this.pending.at(-1)||Promise.resolve()).then(()=>{if(this.error)throw this.error;return this.write(e)}).catch(e=>{this.error=e}).finally(()=>{this.bytes-=e.size,this.pending.shift()});this.pending.push(t)}async finish(){if(await Promise.all(this.pending),this.error)throw this.error}cancel(){this.error=this.error||new Error("cancelled")}}function cachedFrameIndex(e,t){let a=0,s=e.length;for(;a<s;){const r=a+s>>>1;e[r]<=t+1e-5?a=r+1:s=r}return a-1}function sourceBlockPlan(e,t){const a=[];for(const s of e){const e=Math.max(.1,Math.min(8,Number(s.props?.speed)||1)),r=s.start+s.duration;let i=Math.max(0,Math.ceil(s.start*t-1e-7));for(;i/t<r;){const r=i/t,o=s.in+(r-s.start)*e,n=Math.floor(o/5);a.push({clip:s,time:o,block:n,neededAt:r}),i=Math.max(i+1,Math.ceil((s.start+(5*(n+1)-s.in)/e)*t-1e-7))}}return a.sort((e,t)=>e.neededAt-t.neededAt)}function sourceReadAhead(e,t,a){return sourceBlockPlan(e,a).filter(e=>e.neededAt>t).slice(0,3)}class SourceReadAhead{constructor(e,t,{concurrency:a=3,syncPlan:s=async()=>{},key:r=e=>e}={}){this.load=e,this.requested=t,this.concurrency=a,this.syncPlan=s,this.key=r,this.plan=[],this.active=new Map,this.stopped=!1,this.sequence=0,this.readySequence=0,this.syncing=Promise.resolve(),this.signature=""}update(e,t=[]){if(this.stopped)return;const a=JSON.stringify([e.map(this.key),t]);if(a===this.signature)return;this.signature=a,this.plan=e;const s=++this.sequence;this.syncing=this.syncing.catch(()=>{}).then(()=>this.syncPlan(e,t,s)).then(()=>{s===this.sequence&&(this.readySequence=s,this.pump())}).catch(()=>{})}pump(){if(!this.stopped&&this.readySequence===this.sequence)for(const e of this.plan){if(this.active.size>=this.concurrency)break;const t=this.key(e);if(this.active.has(t)||this.requested(e))continue;const a=this.sequence,s=Promise.resolve().then(()=>this.load(e,a)).catch(()=>{}).finally(()=>{this.active.delete(t),this.pump()});this.active.set(t,s)}}async stop(){this.stopped=!0,this.plan=[],await this.syncing,await Promise.allSettled(this.active.values())}}class ExportWaitTracker{constructor(e,{now:t=()=>performance.now(),interval:a=500,threshold:s=250}={}){this.report=e,this.now=t,this.threshold=s,this.active=new Set,this.sequence=0,this.last=null,this.timer=setInterval(()=>this.flush(),a)}async track(e,t){const a={reason:e,started:this.now()};this.active.add(a);try{return await t()}finally{this.active.delete(a)}}flush(){const e=[...this.active].filter(e=>this.now()-e.started>=this.threshold).sort((e,t)=>["source-block","source-image","encoder"].indexOf(e.reason)-["source-block","source-image","encoder"].indexOf(t.reason)||e.started-t.started)[0];(e||null)!==this.last&&(this.last=e||null,Promise.resolve(this.report({sequence:++this.sequence,reason:e?.reason||null,elapsedMs:e?Math.max(0,this.now()-e.started):0})).catch(()=>{this.last===(e||null)&&(this.last=void 0)}))}stop(){clearInterval(this.timer),this.active.clear(),this.flush()}}class FrameReadAhead{constructor(e,t=2){this.load=e,this.concurrency=t,this.plan=[],this.active=new Map,this.stopped=!1}update(e){this.stopped||(this.plan=e,this.pump())}pump(){for(;!this.stopped&&this.active.size<this.concurrency&&this.plan.length;){const e=this.plan.shift();if(this.active.has(e.key))continue;const t=Promise.resolve().then(()=>this.load(e)).catch(()=>{}).finally(()=>{this.active.delete(e.key),this.pump()});this.active.set(e.key,t)}}async stop(){this.stopped=!0,this.plan=[],await Promise.all(this.active.values())}}class SnapshotPipeline{constructor(e,t,a=2){this.capture=e,this.write=t,this.depth=a,this.pending=[]}async push(e){for(;this.pending.length>=this.depth;)await this.drain();const t=await this.capture(e);t.result.catch(()=>{}),this.pending.push(t)}async drain(){const e=this.pending.shift();e&&await this.write(await e.result)}async finish(){for(;this.pending.length;)await this.drain()}async settle(){await Promise.allSettled(this.pending.splice(0).map(e=>e.result))}}function createSnapshotEncoder(e){let t,a=0;const s=new Map,r=()=>{t?.terminate(),t=null;for(const{reject:e}of s.values())e(new Error("snapshot encoder stopped"));s.clear(),e.removeEventListener("abort",r)};if("function"==typeof Worker&&"function"==typeof OffscreenCanvas&&"function"==typeof createImageBitmap)try{t=new Worker("optimized-encoder-worker.js"),t.onmessage=({data:e})=>{const t=s.get(e.id);t&&(s.delete(e.id),e.error?t.reject(new Error(e.error)):t.resolve(e.blob))},t.onerror=e=>{e.preventDefault(),r()},e.addEventListener("abort",r,{once:!0})}catch{r()}return{close:r,async capture(i){if(e.aborted)throw new Error("cancelled");if(!t)return{result:Promise.resolve(await new Promise(e=>i.toBlob(e,"image/jpeg",.95)))};const o=await createImageBitmap(i);if(e.aborted)throw o.close(),new Error("cancelled");const n=a++,c=(async()=>{const a=await createImageBitmap(o);return new Promise((r,i)=>{if(e.aborted||!t)return a.close(),void i(new Error("snapshot encoder stopped"));s.set(n,{resolve:r,reject:i});try{t.postMessage({id:n,bitmap:a},[a])}catch(e){a.close(),s.delete(n),i(e)}})})().catch(async t=>{if(e.aborted)throw t;r();const a=document.createElement("canvas");a.width=o.width,a.height=o.height;try{return a.getContext("2d").drawImage(o,0,0),await new Promise(e=>a.toBlob(e,"image/jpeg",.95))}finally{a.width=a.height=0}}).finally(()=>o.close());return c.catch(()=>{}),{result:c}}}}let optimizedSources=null,optimizedAbort=null,optimizedMaskCanvas=null;function optimizedMaskSource(e){return optimizedMaskCanvas||(optimizedMaskCanvas=document.createElement("canvas")),optimizedMaskCanvas.width!==e.width&&(optimizedMaskCanvas.width=e.width),optimizedMaskCanvas.height!==e.height&&(optimizedMaskCanvas.height=e.height),optimizedMaskCanvas.getContext("2d").drawImage(e,0,0),optimizedMaskCanvas}async function optimizedExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1;const t=performance.now(),a=new AbortController;optimizedAbort=a;const s={engine:"optimized",frames:0,phases:{},samples:{},peakDecodedBytes:0,peakQueueBytes:0},r=(e,t)=>{const a=s.samples[e]||(s.samples[e]={count:0,totalMs:0,maxMs:0,histogram:Array(24).fill(0)});a.count++,a.totalMs+=t,a.maxMs=Math.max(a.maxMs,t),a.histogram[Math.min(23,Math.max(0,Math.ceil(Math.log2(Math.max(1,t)))))]++};let i,o=!1,n=!1;const c=()=>{if(i)throw i;if(renderCancelled||a.signal.aborted)throw new Error("cancelled")},d=async(e,t,s)=>{c();const r=await fetch(e,{method:s||(void 0===t?"GET":"POST"),body:void 0===t?void 0:t instanceof Blob?t:JSON.stringify(t),signal:a.signal}),i=await r.json();if(!r.ok||i.error)throw Object.assign(new Error(i.error||"Export request failed"),{status:r.status});return i};let h,l,p,m,u,f,w,g,y,k,x,v=0,b=new Set;const M=new Map,S=new Map,E=new Map,P=new Set,z=new Set,A=(e,t="")=>projectApi(`/api/export/cache/${e}`)+`&id=${h}${t}`,j=()=>{h&&fetch(A("release"),{method:"POST",keepalive:!0}).catch(()=>{}),l&&fetch(`/api/export/end?id=${l}&discard=1`,{method:"POST",keepalive:!0}).catch(()=>{})},C=()=>{a.abort(),j()},T=e=>{"Escape"===e.key&&(renderCancelled=!0,a.abort()),e.stopImmediatePropagation(),e.preventDefault()};window.addEventListener("pagehide",C),window.addEventListener("keydown",T,!0),els.exportOverlay.classList.remove("hidden"),els.exportProgress.style.width="0%",els.exportTitle.textContent=uiText("Preparing source frames…"),els.exportNote.textContent=uiText("Pre-decoding video and caching frames. You can switch tabs.");try{const j=performance.now()+5e3;for(;;){c();const e=await d(projectApi("/api/project"));if(!runtime.saveTimer&&e.revision===project.revision)break;if(performance.now()>j)throw new Error("Project is not saved or changed externally. Reload/save before exporting.");await new Promise(e=>setTimeout(e,100))}u={...project},Object.assign(project,JSON.parse(JSON.stringify(project)));const C=await d("/api/export/ffmpeg");if(!C.available||!C.ffprobe)throw new Error("Optimized export needs ffmpeg and ffprobe");h=(await d(projectApi("/api/export/cache/begin"),{revision:project.revision})).id,x=new ExportWaitTracker(e=>d(A("wait"),e)),p=setInterval(async()=>{if(!o&&!a.signal.aborted){o=!0;try{if(f=await d(A("status")),"error"===f.state)throw Object.assign(new Error(f.error),{status:400})}catch(e){!n&&e.status>=400&&e.status<500&&(i=e,a.abort())}finally{o=!1}}},15e3);do{if(f=await d(A("status")),"error"===f.state)throw new Error(f.error);"ready"!==f.state&&await new Promise(e=>setTimeout(e,100))}while("ready"!==f.state);const T=new Set(f.clips),I=(e,t)=>{P.add(e.id),s.fallbacks||={},s.fallbacks[e.id]=t},O=(e,t,a=!1,s=0,r)=>{const i=Math.floor(t/5),o=e.id+":"+i,n=!a&&!z.has(o);if(n&&z.add(o),!S.has(o)||n){const t=E.get(o);t?.skipped&&E.delete(o);const n=d(A("block"),{clipId:e.id,block:i,priority:a?"prefetch":"demand",neededAt:s,sequence:r}).then(t=>(t.fallback&&!a&&I(e,t.fallback),S.get(o)===n&&E.set(o,t),t)).catch(t=>a?{skipped:t.message}:(I(e,t.message),{fallback:t.message}));S.set(o,n);for(const t of S.keys())t.startsWith(e.id+":")&&!a&&Number(t.slice(e.id.length+1))<i-1&&(S.delete(t),E.delete(t),z.delete(t))}return S.get(o)},q=(e,t=0)=>{for(const[a,s]of M){if(v+t<=134217728)break;!e.has(a)&&s.img&&(s.img.close(),v-=s.bytes,M.delete(a))}},B=async(e,t,i,o=!1)=>{const n=e.id+":"+Math.floor(t/5);if(o&&!E.has(n))return null;!o&&!E.has(n)&&s.frames>0&&(els.exportTitle.textContent=uiText("Preparing video frames…"),els.exportNote.textContent=uiText("Preparing the next part of your video. Export will continue automatically."));const d=!E.has(n)||E.get(n)?.skipped,h=performance.now(),l=o?E.get(n):await x.track("source-block",()=>O(e,t));if(!o&&d&&r("blockWait",performance.now()-h),l.fallback||l.skipped)return null;const p=cachedFrameIndex(l.times,t);if(p<0||t>l.times.at(-1)+(l.times.length>1?l.times[1]-l.times[0]:.1)+1e-5)return o||I(e,"source time outside cached coverage"),null;const m=l.key+":"+p;o||i.add(m);let u=M.get(m);if(u)M.delete(m),M.set(m,u);else{if(q(i,l.width*l.height*4),o&&v+l.width*l.height*4>134217728)return null;u={bytes:l.width*l.height*4,img:null},v+=u.bytes,s.peakDecodedBytes=Math.max(s.peakDecodedBytes,v),u.promise=(async()=>{let e;try{let t=performance.now();const s=await fetch(A("frame",`&key=${l.key}&index=${p}`),{signal:a.signal});if(!s.ok)throw new Error("Cached source frame unavailable");const i=await s.blob();return r("sourceFetch",performance.now()-t),t=performance.now(),e=await createImageBitmap(i),r("sourceDecode",performance.now()-t),c(),u.img=e,e}catch(t){throw e?.close(),v-=u.bytes,M.delete(m),t}})(),M.set(m,u)}return o?u.promise:x.track("source-image",()=>u.promise)},F=project.fps,R=Math.max(1/F,projDur()),N=Math.max(1,Math.round(R*F)),$=project.clips.filter(e=>"video"===e.kind&&isTrackEnabled(e.track)),W=sourceBlockPlan($.filter(e=>T.has(e.id)),F),D=e=>e.clip.id+":"+e.block;g=new SourceReadAhead((e,t)=>O(e.clip,e.time,!0,e.neededAt,t),e=>P.has(e.clip.id)||S.has(D(e)),{key:D,syncPlan:async(e,t,a)=>{const s=new Set([...e.map(D),...t.map(e=>e.clipId+":"+e.block)]);for(const e of S.keys())z.has(e)||s.has(e)||(S.delete(e),E.delete(e));await d(A("retain"),{sequence:a,blocks:e.map(e=>({clipId:e.clip.id,block:e.block})),current:t})}});const L=new Set,Q=async e=>{const t=$.filter(t=>activeAt(t,e));g.update(W.filter(t=>t.neededAt>e&&!P.has(t.clip.id)).slice(0,3),t.filter(e=>T.has(e.id)&&!P.has(e.id)).map(t=>({clipId:t.id,block:Math.floor(mediaTimeAt(t,e)/5)})));const a=$.filter(t=>t.start+t.duration<=e&&!L.has(t.id));if(a.length){await d(A("drop"),{clipIds:a.map(e=>e.id)});for(const e of a)L.add(e.id)}optimizedSources=new Map;const s=b=new Set;for(const a of t)stealExportVideo(a,e);return releaseFinishedExportVideos(e),await Promise.all(t.map(async t=>{const a=mediaTimeAt(t,e);if(T.has(t.id)&&!P.has(t.id))try{const e=await B(t,a,s);if(e)return void optimizedSources.set(t.id,e)}catch(e){c(),I(t,e.message)}await prepareVideoFrame(t,acquireExportVideo(t,e),a)})),q(s),s};await Q(0),s.phases.prepareMs=performance.now()-t,els.exportTitle.textContent=uiText("Mixing audio…");let J=performance.now();const V=await renderAudioMix(R,a.signal);c(),s.phases.audioMs=performance.now()-J;const U=e.name||project.name||"export";l=(await d(projectApi("/api/export/begin"),{fps:F,name:U,requestId:e.requestId,totalFrames:N,engine:"optimized",cacheId:h})).id,V&&await d(`/api/export/audio?id=${l}`,V),await document.fonts.ready,project.clips.some(e=>isTrackEnabled(e.track)&&e.props?.bgRemove)&&await ensureBgSeg(),m=new OrderedFrameQueue(async e=>{const t=performance.now();await d(`/api/export/frame?id=${l}`,e),r("upload",performance.now()-t)}),w=createSnapshotEncoder(a.signal);const Y=Math.max(1,Math.min(2,Math.floor(67108864/(project.width*project.height*4))));s.snapshotDepth=Y,k=new SnapshotPipeline(async e=>{const t=performance.now(),a=await w.capture(e);return r("snapshot",performance.now()-t),a.result=a.result.then(e=>(r("jpeg",performance.now()-t),e)),a},async e=>{if(!e?.size)throw new Error("Canvas returned an empty frame");const t=performance.now();await x.track("encoder",()=>m.push(e)),r("queueWait",performance.now()-t)},Y),y=new FrameReadAhead(async({clip:e,time:t})=>{if(P.has(e.id)||L.has(e.id))return;const a=performance.now();await B(e,t,b,!0),r("prefetch",performance.now()-a)});const G=performance.now();for(let e=0;e<N;e++){c();const t=e/F;state.time=t,J=performance.now();const a=await Q(t);r("source",performance.now()-J),$.some(a=>a.start>(e-1)/F&&a.start<=t)&&r("cutSourceWait",performance.now()-J),J=performance.now(),await prepareFrameAssets(t),r("assets",performance.now()-J),J=performance.now(),drawFrame(t),r("composite",performance.now()-J),J=performance.now();const i=[];for(let t=1;t<=4&&e+t<N;t++){const a=(e+t)/F;for(const s of $)activeAt(s,a)&&T.has(s.id)&&!P.has(s.id)&&i.push({key:`${s.id}:${e+t}`,clip:s,time:mediaTimeAt(s,a)})}y.update(i),await x.track("encoder",()=>k.push(els.preview)),r("pipeline",performance.now()-J),s.frames++,q(a);const o=(e+1)/N*100;els.exportProgress.style.width=o.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:o.toFixed(0)}),els.exportNote.textContent=uiText("Cached video frames · {count} compatibility fallbacks · You can switch tabs.",{count:new Set([...Object.keys(f.stats.fallbacks),...P]).size})}await x.track("encoder",async()=>{await k.finish(),await m.finish()}),await y.stop(),s.peakQueueBytes=m.peakBytes,s.phases.renderMs=performance.now()-G,f=await d(A("status")),s.cache=f.stats;for(const e of Object.values(s.samples)){let t=0;e.meanMs=e.totalMs/e.count;for(let a=0;a<e.histogram.length;a++)if(t+=e.histogram[a],t>=.95*e.count){e.p95UpperMs=2**a;break}}els.exportTitle.textContent=uiText("Encoding…"),J=performance.now(),s.totalMs=performance.now()-t,n=!0;const K=await d(`/api/export/end?id=${l}`,{metrics:s});if(l=null,s.phases.finalizeMs=performance.now()-J,s.totalMs=performance.now()-t,s.fps=N/(s.totalMs/1e3),!e.requestId){const e=document.createElement("a");e.href=K.src,e.download=decodeURIComponent(K.src.split("/").pop()),e.click(),toast(uiText("Optimized export complete: {seconds}s · {hits} cache hits",{seconds:(s.totalMs/1e3).toFixed(1),hits:s.cache.hits}))}console.info("Optimized export",s)}catch(t){m?.cancel(),a.abort(),await(m?.finish().catch(()=>{}));const s=i?.message||(renderCancelled||"AbortError"===t.name?"cancelled":t.message);e.requestId?await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,error:s})}).catch(()=>{}):"cancelled"!==s&&alert(uiText("Optimized export failed: ")+s)}finally{clearInterval(p),x?.stop(),j(),w?.close(),a.abort(),await(g?.stop()),await(y?.stop()),await(k?.settle()),optimizedAbort=null,optimizedSources=null;for(const e of M.values())e.img&&e.img.close();M.clear(),optimizedMaskCanvas&&(optimizedMaskCanvas.width=0,optimizedMaskCanvas.height=0,optimizedMaskCanvas=null),u&&Object.assign(project,u),state.exporting=!1,state.rendering=!1,window.removeEventListener("pagehide",C),window.removeEventListener("keydown",T,!0),els.exportOverlay.classList.add("hidden"),els.exportNote.textContent=uiText("Rendering your sequence in real time. Keep this tab focused."),runtime.pendingSync&&syncFromServer()}}"undefined"!=typeof module&&(module.exports={ExportWaitTracker,OrderedFrameQueue,cachedFrameIndex,sourceBlockPlan,sourceReadAhead,SourceReadAhead,FrameReadAhead,SnapshotPipeline,createSnapshotEncoder});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const e=require("fs"),r=require("path"),i=__dirname,n=process.env.FABLECUT_DATA_DIR?r.resolve(process.env.FABLECUT_DATA_DIR):i,o=n!==i,t=process.env.FABLECUT_PROJECTS_DIR?r.resolve(process.env.FABLECUT_PROJECTS_DIR):r.join(n,"projects"),s=r.join(n,"library"),c=["sfx","elements","svg","fonts"],a="default";function d(r){try{return e.lstatSync(r),!0}catch(e){if("ENOENT"===e.code||"ENOTDIR"===e.code)return!1;throw e}}function l({dataDir:e=n,projectsDir:i=t}={}){const o=r.join(e,"projects");return{projectsDir:i,legacyDir:("win32"===process.platform?r.resolve(o).toLowerCase()===r.resolve(i).toLowerCase():r.resolve(o)===r.resolve(i))?null:o,hiddenDir:r.join(e,".legacy-projects-hidden")}}function j(i,n){i=u(i);const{legacyDir:o,hiddenDir:t}=l(n);if(!o||!d(r.join(o,i)))return;const s=r.join(t,i);d(s)||e.mkdirSync(s,{recursive:!0})}function u(e){const r=String(e||a).trim().toLowerCase();if(!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(r))throw new Error("project id must be 1-64 lowercase letters, numbers, _ or -");return r}function y(e=a,i){e=u(e);const{projectsDir:n,legacyDir:o,hiddenDir:t}=l(i);let s=r.join(n,e);return d(s)?j(e,i):o&&!d(r.join(t,e))&&d(r.join(o,e))&&(s=r.join(o,e)),{id:e,dir:s,projectFile:r.join(s,"project.json"),mediaDir:r.join(s,"media"),exportsDir:r.join(s,"exports"),analysisDir:r.join(s,"analysis")}}function f(r=a,i){const n=y(r);for(const r of[n.dir,n.mediaDir,n.exportsDir,n.analysisDir])e.mkdirSync(r,{recursive:!0});return e.existsSync(n.projectFile)||e.writeFileSync(n.projectFile,JSON.stringify(function(e="Untitled Project"){return{name:e,width:1280,height:720,fps:30,revision:0,media:[],clips:[]}}(i),null,2)),n}function p(r){return r&&d(r)?e.readdirSync(r,{withFileTypes:!0}).filter(e=>e.isDirectory()&&/^[a-z0-9][a-z0-9_-]{0,63}$/.test(e.name)).map(e=>e.name):[]}function m(e){const{legacyDir:i}=l(e);return p(i).map(r=>y(r,e)).filter(e=>e.dir===r.join(i,e.id))}function D(){return[...new Set([...p(t),...m().map(e=>e.id)])].map(r=>{const i=y(r);let n={};try{n=JSON.parse(e.readFileSync(i.projectFile,"utf8").replace(/^\uFEFF/,""))}catch{}let o=0;try{o=e.statSync(i.projectFile).mtimeMs}catch{}return{id:r,name:n.name||r,revision:n.revision||0,modified:o}}).sort((e,r)=>r.modified-e.modified||e.id.localeCompare(r.id))}module.exports={APP_DIR:i,DATA_DIR:n,SPLIT:o,PROJECTS_DIR:t,LIBRARY_DIR:s,LIBRARY_SUBDIRS:c,DEFAULT_PROJECT_ID:a,normalizeProjectId:u,projectPaths:y,ensureProject:f,listProjects:D,ensureDirs:function(){e.mkdirSync(n,{recursive:!0}),e.mkdirSync(t,{recursive:!0}),function(){if(!e.existsSync(r.join(n,"project.json")))return;const o=["project.json","media","exports","analysis"].map(e=>[r.join(n,e),e]).filter(([r])=>e.existsSync(r));if(!o.length)return;const t=y(a);e.mkdirSync(t.dir,{recursive:!0});for(const[i,n]of o){const o=r.join(t.dir,n);e.existsSync(o)||e.renameSync(i,o)}if(n===i)for(const i of["media","exports"]){const o=r.join(n,i);e.mkdirSync(o,{recursive:!0}),e.closeSync(e.openSync(r.join(o,".gitkeep"),"a"))}}();for(const i of c)e.mkdirSync(r.join(s,i),{recursive:!0});!function(){if(!o)return;const n=r.join(i,"library");if(!e.existsSync(n))return;const t=(i,n)=>{e.mkdirSync(n,{recursive:!0});for(const o of e.readdirSync(i,{withFileTypes:!0})){const s=r.join(i,o.name),c=r.join(n,o.name);o.isDirectory()?t(s,c):e.existsSync(c)||e.copyFileSync(s,c)}};t(n,s)}(),D().length||f(a)},projectRoots:l,hideLegacyProject:j,legacyProjectPaths:m};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const r=require("fs"),e=require("path"),t=require("crypto"),n=require("./paths"),o=require("./timeline");function i(e,t){const n=Date.now()+1e4;let o;for(;void 0===o;)try{o=r.openSync(e,"wx")}catch(r){if("EEXIST"!==r.code)throw r;if(Date.now()>=n)throw new Error(`Timed out waiting for lock: ${e}. If its owner has exited, remove this lock and retry.`);Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,25)}let i=!1;const c=()=>{if(!i){i=!0;try{r.closeSync(o)}finally{r.unlinkSync(e)}}};try{r.writeFileSync(o,String(process.pid));const e=t();return e&&"function"==typeof e.then?e.finally(c):(c(),e)}catch(r){throw c(),r}}function c(r){const e=new Error("No such project: "+r);return e.code="PROJECT_NOT_FOUND",e.status=404,e}function s(e){const t=n.projectPaths(e);try{if(!r.statSync(t.projectFile).isFile())throw new Error("Invalid project file: "+t.id)}catch(r){if("ENOENT"===r.code||"ENOTDIR"===r.code)throw c(t.id);throw r}return t}function a(e){const t=s(e);let n;try{n=r.readFileSync(t.projectFile,"utf8")}catch(r){if("ENOENT"===r.code||"ENOTDIR"===r.code)throw c(t.id);throw r}return o.normalize(JSON.parse(n.replace(/^\uFEFF/,"")))}function l(e,n){const o=e+"."+process.pid+"."+t.randomBytes(8).toString("hex")+".tmp";try{r.writeFileSync(o,JSON.stringify(n,null,2),{flag:"wx"}),r.renameSync(o,e)}finally{r.rmSync(o,{force:!0})}}function u(r,t){const n=s(r);return i(e.join(n.dir,".write.lock"),()=>{const e=a(r),i=o.normalize(t(e));return l(n.projectFile,i),i})}module.exports={withLock:i,context:s,read:a,update:u,save:function(r,e,t=!1){return u(r,r=>{if(!t&&(e.revision||0)<=(r.revision||0))throw function(r){const e=new Error("CONFLICT — stale revision; project changed since it was read");return e.status=409,e.revision=r,e}(r.revision||0);return{...e,revision:Math.max(Number(e.revision||0),Number(r.revision||0)+1)}})},create:function(t,o){t=String(t||"Untitled Project").trim()||"Untitled Project";const c=String(o||t).toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,56)||"project";return i(e.join(n.DATA_DIR,".projects.lock"),()=>{let e=n.normalizeProjectId(c),o=2;for(;;)try{r.mkdirSync(n.projectPaths(e).dir);break}catch(r){if("EEXIST"!==r.code)throw r;e=n.normalizeProjectId(`${c}-${o++}`)}return n.ensureProject(e,t),{id:e,name:t}})},atomicWrite:l};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let cv=null,g=null;function fmt(t,o){t=Math.max(0,t);const l=Math.floor(t/60),e=Math.floor(t%60),n=Math.floor(t%1*(o||30)),g=t=>String(t).padStart(2,"0");return`${g(l)}:${g(e)}:${g(n)}`}function draw(t){if(!g)return;const{w:o,h:l,dpr:e,sl:n,pps:i,markers:a,inPoint:f,outPoint:c,time:s,fps:r,duration:h}=t,d=Math.round(o*e),m=Math.round(l*e);cv.width===d&&cv.height===m||(cv.width=d,cv.height=m),g.setTransform(e,0,0,e,0,0),g.clearRect(0,0,o,l);const T=([.1,.25,.5,1,2,5,10,15,30,60,120,300].find(t=>t*i>=70)||600)/5,u=Math.max(0,Math.floor(n/i/T));g.strokeStyle="#4a4a55",g.fillStyle="#9a9aa6",g.font="10px Consolas, monospace",g.beginPath();for(let t=u;t*T*i<n+o&&t*T<=h;t++){const o=t*T,e=Math.round(o*i-n)+.5,a=t%5==0;g.moveTo(e,a?8:17),g.lineTo(e,l),a&&g.fillText(fmt(Math.round(1e3*o)/1e3,r).slice(0,5),e+4,12)}if(g.stroke(),null!=f&&null!=c&&c>f){const t=f*i-n,e=c*i-n;g.fillStyle="#00000055",t>0&&g.fillRect(0,0,Math.min(o,t),l),e<o&&g.fillRect(Math.max(0,e),0,o-Math.max(0,e),l)}for(const t of a||[]){const e=t.t*i-n;e<-6||e>o+6||(g.fillStyle="#ffd166",g.beginPath(),g.moveTo(e,l-9),g.lineTo(e+4,l-5),g.lineTo(e,l-1),g.lineTo(e-4,l-5),g.closePath(),g.fill())}const v=l-1,p=v-.75*(l-4),M=(p+v)/2;if(g.globalCompositeOperation="difference",null!=f){const t=f*i-n;t>=-10&&t<=o+10&&(g.fillStyle="#5eead4",g.beginPath(),g.moveTo(t,p),g.lineTo(t,v),g.lineTo(t+8,M),g.closePath(),g.fill())}if(null!=c){const t=c*i-n;t>=-10&&t<=o+10&&(g.fillStyle="#fb923c",g.beginPath(),g.moveTo(t,p),g.lineTo(t,v),g.lineTo(t-8,M),g.closePath(),g.fill())}g.globalCompositeOperation="source-over";const P=s*i-n;P>=-8&&P<=o+8&&(g.fillStyle="#ff4d6a",g.beginPath(),g.moveTo(P-6,12),g.lineTo(P+6,12),g.lineTo(P+6,19),g.lineTo(P,25),g.lineTo(P-6,19),g.closePath(),g.fill())}self.onmessage=t=>{const o=t.data;o&&("init"===o.type?(cv=o.canvas,g=cv.getContext("2d")):"draw"===o.type&&draw(o))};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const e=require("http"),r=require("fs"),t=require("path"),o=require("os"),{spawn:n,fork:i,spawnSync:a}=require("./cli/lib/processes"),{writeFrame:s,stopProcess:c,run:d}=require("./export-encoder"),{createExportSaver:p,resolveExportSource:l,validVideoName:m}=require("./export-save"),f=p(),{analyze:u}=require("./analyze"),h=require("./project-store"),{ExportCache:g}=require("./export-cache"),{Waveforms:w}=require("./waveform"),{ExportAudio:S}=require("./export-audio"),{pipeline:y}=require("node:stream/promises"),{APP_DIR:v,DATA_DIR:x,PROJECTS_DIR:b,LIBRARY_DIR:j,LIBRARY_SUBDIRS:I,DEFAULT_PROJECT_ID:P,normalizeProjectId:E,projectPaths:T,listProjects:O,ensureDirs:D,projectRoots:C}=require("./paths"),k=v,q=new S({libraryDir:j}),R=process.env.HOST||"127.0.0.1",$=new Set(["localhost","127.0.0.1","[::1]","::1",R.toLowerCase()]);for(const e of(process.env.FABLECUT_ALLOWED_HOSTS||"").split(","))e.trim()&&$.add(e.trim().toLowerCase());function z(e){if(!e)return!1;const r=e.replace(/^(\[[^\]]*\]|[^:]+)(:\d+)?$/,"$1").toLowerCase();return $.has(r)}let N=!1;try{N=0===a("ffmpeg",["-version"],{stdio:"ignore"}).status}catch{}let _=!1;try{_=0===a("ffprobe",["-version"],{stdio:"ignore"}).status}catch{}const A=new g({libraryDir:j}),U=new w({libraryDir:j}),F={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".json":"application/json",".mp4":"video/mp4",".webm":"video/webm",".mov":"video/quicktime",".mkv":"video/x-matroska",".m4v":"video/mp4",".mp3":"audio/mpeg",".wav":"audio/wav",".ogg":"audio/ogg",".m4a":"audio/mp4",".aac":"audio/aac",".flac":"audio/flac",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".ico":"image/x-icon",".ttf":"font/ttf",".otf":"font/otf",".woff":"font/woff",".woff2":"font/woff2"};D();const J=new Set;function L(e,r="change"){for(const t of J)e&&t.projectId!==e||t.res.write(`data: ${r}\n\n`)}const H=new Map;function W(e){clearTimeout(H.get(e)),H.set(e,setTimeout(()=>L(e),150))}const M=new Map;function B(e){const r=M.get(e);if(M.delete(e),r)for(const e of r.watchers)e.close()}function G(e){const t=T(e),o=M.get(e);if(o?.dir===t.dir)return;B(e);const n=[],i={dir:t.dir,watchers:n},a=(t,o)=>{try{const a=r.watch(t,o);a.on("error",()=>{M.get(e)===i&&B(e),W(e)}),n.push(a)}catch{}};a(t.dir,(r,t)=>{"project.json"===t&&W(e)}),a(t.mediaDir,()=>W(e)),n.length&&M.set(e,i)}for(const e of O())G(e.id);let V=null;for(const e of[b,C().legacyDir].filter(Boolean))try{r.watch(e,(e,r)=>{const t=r&&String(r),o=t&&/^[a-z0-9][a-z0-9_-]{0,63}$/.test(t)?[t]:[...M.keys()];for(const e of o){B(e);try{G(e)}catch{}W(e)}clearTimeout(V),V=setTimeout(()=>L(null,"projects"),150)}).on("error",()=>{for(const[r,o]of M)t.dirname(o.dir)===e&&B(r);L(null,"projects")})}catch{}for(const e of I)try{r.watch(t.join(j,e),()=>L(null))}catch{}function Y(e,r,t){const o=JSON.stringify(t);e.writeHead(r,{"Content-Type":"application/json","Cache-Control":"no-store"}),e.end(o)}function K(e){return new Promise((r,t)=>{const o=[];e.on("data",e=>o.push(e)),e.on("end",()=>r(Buffer.concat(o))),e.on("error",t)})}function X(e,o){let n=t.join(e,o);const i=t.extname(o),a=t.basename(o,i);let s=1;for(;r.existsSync(n);)n=t.join(e,`${a}_${s++}${i}`);return n}function Q(e){const r=E(e.searchParams.get("project")||P),t=h.context(r);return G(r),t}function Z(e,r){return"/projects/"+encodeURIComponent(e)+"/media/"+encodeURIComponent(r)}const ee=new Set([".mp4",".mov",".m4v"]),re=new Map,te=new Map,oe=new Map;function ne(){const e=new Set;for(const[r,t]of oe){const o=te.get(r)?.state;o&&["complete","error","cancelled"].includes(o)||e.add(t.projectId)}for(const r of re.values())r.projectId&&e.add(r.projectId);return[...e]}function ie(e){const r=oe.get(e);if(!r||!r.cleanupDone||r.child&&!r.childClosed)return;clearTimeout(r.timer),oe.delete(e);const t=te.get(e);t&&te.set(e,{...t,workerDone:!0}),queueMicrotask(ae)}function ae(){let e=[...oe.values()].filter(e=>e.child).length;for(const[r,t]of oe){if(e>=2)break;t.child||t.stopping||(se(r,t),t.child&&e++)}}function se(e,r){const{projectId:o,options:n}=r;try{const a=i(t.join(k,"export-worker.js"),[],{stdio:["ignore","ignore","pipe","ipc"]});r.child=a,de(e,{state:"starting",projectId:o});let s="";a.stderr.on("data",e=>{s=(s+e).slice(-2e3)}),a.on("message",r=>{const t=te.get(e);(["starting","preparing-browser","preparing-media"].includes(t?.state)||"error"===r.state)&&de(e,{...r,projectId:o})}),a.on("error",r=>de(e,{state:"error",projectId:o,error:r.message})),a.once("close",()=>{r.childClosed=!0,de(e,{state:"error",projectId:o,error:"Export worker exited: "+s.slice(-800)}),ie(e)}),r.timer=setTimeout(()=>de(e,{state:"error",projectId:o,error:"Export timed out"}),1e3*n.timeoutSeconds);const c=fe.address(),d="0.0.0.0"===R?"127.0.0.1":"::"===R?"[::1]":R.includes(":")&&!R.startsWith("[")?`[${R}]`:R;a.send({id:e,projectId:o,engine:n.engine,name:n.name,browser:n.browser,url:`http://${d}:${c.port}/`})}catch(r){de(e,{state:"error",projectId:o,error:r.message})}}function ce(e,r){const t=e.searchParams.get("exportJob");if(!t)return;const o=oe.get(t);if(!o||o.stopping||o.projectId!==r.id)throw new Error("no such export job");return o.snapshot}function de(e,r){if(!e||!/^[a-f0-9]{32}$/.test(e))return;const t=te.get(e);if((!t||!["complete","error","cancelled"].includes(t.state))&&("error"===r.state&&(r={...r,error:`${r.error} (stage=${t?.state||"starting"}, frames=${t?.frames||0})`}),te.set(e,{...t,...r,updated:Date.now()}),["complete","error","cancelled"].includes(r.state)&&function(e){const r=oe.get(e);if(!r||r.stopping)return;r.stopping=!0,r.recordingUpload?.abort(),clearTimeout(r.timer);const t=[];for(const e of r.cacheIds||[])t.push(A.release(e,r.projectId));for(const[r,o]of re)o.requestId===e&&t.push(le(r));r.child&&!r.childClosed&&r.child.kill();const o=()=>{r.cleanupDone=!0,ie(e)};t.some(Boolean)?Promise.allSettled(t).then(o):o()}(e),te.size>100)){const e=[...te].sort((e,r)=>e[1].updated-r[1].updated).slice(0,20);for(const[r]of e)oe.has(r)||te.delete(r)}}function pe(e,i,a,s,c,d){const p=Date.now().toString(36)+Math.random().toString(36).slice(2,7),l=r.mkdtempSync(t.join(o.tmpdir(),"fablecut-")),m=t.join(l,"video.mp4"),f=n("ffmpeg",[...c?["-filter_threads","1"]:[],"-y","-f","image2pipe","-vcodec","mjpeg","-framerate",String(e),"-i","-","-vf","scale=in_range=full:in_color_matrix=bt601:out_range=tv:out_color_matrix=bt709","-c:v","libx264","-preset","fast","-crf","18","-pix_fmt","yuv420p","-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv",m],{stdio:["pipe","ignore","pipe"]});let u="";f.stderr.on("data",e=>{u=(u+e).slice(-2e3)});const h={controller:new AbortController,proc:f,dir:l,videoPath:m,name:(i||"export").replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").slice(0,120).trim()||"export",projectId:a,requestId:s,cacheId:c,totalFrames:Number.isSafeInteger(d)&&d>0?d:void 0,wav:null,frames:0,frameBytes:0,err:()=>u,done:new Promise(e=>f.on("close",e))};re.set(p,h);const g=e=>{h.cleaning||(de(s,{state:"error",projectId:a,error:e.message}),le(p))};return f.on("error",g),f.stdin.on("error",g),f.once("exit",(e,r)=>{h.finalizing&&0===e||g(new Error(`ffmpeg exited (${r||e}): ${h.err()}`))}),de(s,{state:"rendering",projectId:a,frames:0,totalFrames:h.totalFrames}),p}function le(e){const t=re.get(e);if(t)return t.cleaning||(t.controller.abort(),t.cleaning=(async()=>{await Promise.allSettled([c(t.proc,t.done),t.muxDone,t.cacheId&&A.release(t.cacheId,t.projectId)]),t.output&&!t.committed&&await r.promises.rm(t.output,{force:!0}),await r.promises.rm(t.dir,{recursive:!0,force:!0})})().catch(e=>console.error("Export cleanup failed:",e.message)).finally(()=>re.delete(e))),t.cleaning}function me(e,o,n){r.stat(n,(i,a)=>{if(i||!a.isFile())return o.writeHead(404),void o.end("Not found");"HEAD"===e.method&&o.setHeader("ETag",`W/"${a.size}-${a.mtimeMs}"`);const s=F[t.extname(n).toLowerCase()]||"application/octet-stream",c=e.headers.range;if(c){const t=/bytes=(\d*)-(\d*)/.exec(c);let i=t&&t[1]?parseInt(t[1]):0,d=t&&t[2]?parseInt(t[2]):a.size-1;if(i=Math.min(i,a.size-1),d=Math.min(d,a.size-1),o.writeHead(206,{"Content-Type":s,"Accept-Ranges":"bytes","Content-Range":`bytes ${i}-${d}/${a.size}`,"Content-Length":d-i+1}),"HEAD"===e.method)return void o.end();r.createReadStream(n,{start:i,end:d}).pipe(o)}else{if(o.writeHead(200,{"Content-Type":s,"Content-Length":a.size,"Accept-Ranges":"bytes","Cache-Control":"no-cache"}),"HEAD"===e.method)return void o.end();r.createReadStream(n).pipe(o)}})}setInterval(()=>{for(const[e,r]of re)r.cacheId&&!A.sessions.has(r.cacheId)&&(de(r.requestId,{state:"error",projectId:r.projectId,error:"Optimized export session expired"}),le(e))},15e3).unref();const fe=e.createServer(async(e,o)=>{if(!function(e){if(!z(e.headers.host))return!1;const r=e.headers.origin;if(r)try{return z(new URL(r).host)}catch{return!1}return!0}(e))return void Y(o,403,{error:"forbidden: request must come from this machine (bad Host or Origin header)"});const n=new URL(e.url,"http://localhost"),i=decodeURIComponent(n.pathname);if(i.split(/[\\/]/).some(e=>e.startsWith(".")))return o.writeHead(403),void o.end();if("/api/status"===i&&"GET"===e.method)return void Y(o,200,{service:"fablecut",pid:process.pid,version:ue(),dataDir:r.realpathSync(x)});if("/api/export/active"===i&&"GET"===e.method){const e=ne(),r=n.searchParams.get("project");return void Y(o,200,{active:r?e.includes(r):e.length>0,projects:e})}if("/api/server/stop"===i&&"POST"===e.method){if(re.size>0||ne().length>0)return void Y(o,409,{error:"Export in progress"});const e=JSON.stringify({ok:!0,stopped:!0});return o.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-store","Content-Length":Buffer.byteLength(e)}),void o.end(e,()=>{we(),fe.close(()=>process.exit(0))})}if("/api/projects"===i&&"GET"===e.method)return void Y(o,200,O());if("/api/projects"===i&&"POST"===e.method){try{const r=JSON.parse((await K(e)).toString("utf8")||"{}"),t=String(r.name||"Untitled Project").trim()||"Untitled Project",{id:n}=h.create(t,r.id);G(n),L(null,"projects"),Y(o,201,{id:n,name:t})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/project"===i&&"GET"===e.method){try{const e=Q(n);Y(o,200,ce(n,e)||h.read(e.id))}catch(e){Y(o,"PROJECT_NOT_FOUND"===e.code?404:500,{error:String(e)})}return}if("/api/project"===i&&"PUT"===e.method){try{const r=Q(n),t=await K(e),i=JSON.parse(t.toString("utf8"));Y(o,200,{ok:!0,revision:h.save(r.id,i,"1"===n.searchParams.get("force")).revision})}catch(e){Y(o,e.status||400,{error:String(e),revision:e.revision})}return}if("/api/media"===i&&"GET"===e.method){try{const e=Q(n);Y(o,200,r.readdirSync(e.mediaDir).filter(o=>r.statSync(t.join(e.mediaDir,o)).isFile()).map(o=>({name:o,src:Z(e.id,o),size:r.statSync(t.join(e.mediaDir,o)).size})))}catch(e){Y(o,500,{error:String(e)})}return}if("/api/library"===i&&"GET"===e.method){const e=n.searchParams.get("dir");if(!I.includes(e))return void Y(o,400,{error:"dir must be one of "+I.join("|")});try{const n=t.join(j,e),i=[],a=(o,n)=>{for(const s of r.readdirSync(o)){const c=t.join(o,s),d=n?n+"/"+s:s,p=r.statSync(c);p.isDirectory()?a(c,d):i.push({name:s,rel:d,size:p.size,src:"/library/"+e+"/"+d.split("/").map(encodeURIComponent).join("/")})}};a(n,""),Y(o,200,i)}catch(e){Y(o,500,{error:String(e)})}return}if("/api/upload"===i&&"POST"===e.method){try{const i=Q(n);let a=function(e){return e.replace(/[^\w.\- ()\[\]]+/g,"_").slice(0,120)||"file"}(n.searchParams.get("name")||"upload.bin"),s=t.join(i.mediaDir,a),c=1;const p=t.extname(a),l=t.basename(a,p),m=await K(e);for(;;)try{r.writeFileSync(s,m,{flag:"wx"});break}catch(e){if("EEXIST"!==e.code)throw e;s=t.join(i.mediaDir,`${l}_${c++}${p}`)}await async function(e){if(!N||!ee.has(t.extname(e).toLowerCase()))return;const o=e+".fs"+t.extname(e);try{await d("ffmpeg",["-y","-i",e,"-c","copy","-movflags","+faststart",o]),r.rmSync(e),r.renameSync(o,e)}catch{try{r.rmSync(o)}catch{}}}(s),Y(o,200,{ok:!0,src:Z(i.id,t.basename(s))})}catch(e){Y(o,500,{error:String(e)})}return}if(i.startsWith("/api/export/cache/")){try{if(!N||!_)throw new Error("Optimized export needs ffmpeg and ffprobe");const r=Q(n),t=n.searchParams.get("id");if("/api/export/cache/begin"===i&&"POST"===e.method){const t=JSON.parse((await K(e)).toString()),i=ce(n,r),a=oe.get(n.searchParams.get("exportJob"));if(a&&!a.child)throw new Error("Export job has not started");const s=A.create(r,t.revision,i,{managed:!!a});a&&(a.cacheIds||=new Set).add(s),Y(o,200,{id:s})}else if("/api/export/cache/status"===i&&"GET"===e.method)Y(o,200,A.status(t,r.id));else if("/api/export/cache/block"===i&&"POST"===e.method){const n=JSON.parse((await K(e)).toString());Y(o,200,await A.block(t,r.id,n.clipId,n.block,{priority:n.priority,neededAt:n.neededAt,sequence:n.sequence}))}else if("/api/export/cache/retain"===i&&"POST"===e.method){const n=JSON.parse((await K(e)).toString());A.retain(t,r.id,n.sequence,n.blocks,n.current),Y(o,200,{ok:!0})}else if("/api/export/cache/wait"===i&&"POST"===e.method)A.reportWait(t,r.id,JSON.parse((await K(e)).toString())),Y(o,200,{ok:!0});else if("/api/export/cache/frame"===i&&"GET"===e.method)me(e,o,A.frame(t,r.id,n.searchParams.get("key"),Number(n.searchParams.get("index"))));else if("/api/export/cache/drop"===i&&"POST"===e.method){const n=JSON.parse((await K(e)).toString());A.drop(t,r.id,n.clipIds),Y(o,200,{ok:!0})}else"/api/export/cache/release"===i&&"POST"===e.method?(await A.release(t,r.id),Y(o,200,{ok:!0})):Y(o,404,{error:"unknown cache operation"})}catch(e){Y(o,400,{error:String(e.message||e)})}return}if("/api/export/source-audio"===i&&"GET"===e.method){const e=new AbortController,t=()=>e.abort();let i;o.once("close",t);try{if(!N)throw Object.assign(new Error("Audio export needs ffmpeg"),{status:503});if(i=await q.prepare(Q(n),n.searchParams.get("src"),e.signal),e.signal.aborted)return;if(!i)return o.writeHead(204),void o.end();o.writeHead(200,{"Content-Type":"audio/wav","Cache-Control":"no-store"}),await y(r.createReadStream(i.file),o,{signal:e.signal})}catch(e){o.destroyed||o.headersSent?o.destroyed||o.destroy():Y(o,e.status||400,{error:String(e.message||e)})}finally{o.removeListener("close",t),i&&await i.cleanup()}return}if(("/api/waveform"===i||"/api/audio-info"===i)&&"GET"===e.method){try{if(!N||!_)throw Object.assign(new Error("Waveforms need ffmpeg and ffprobe"),{status:503});const e=Q(n),r=n.searchParams.get("src");Y(o,200,"/api/waveform"===i?await U.peaks(e,r):{channels:await U.channels(e,r)})}catch(e){Y(o,e.status||400,{error:String(e.message||e)})}return}if("/api/export/save"===i&&"POST"===e.method){const r=["127.0.0.1","::1","::ffff:127.0.0.1"].includes(e.socket.remoteAddress);let i=!1;try{i=new URL(e.headers.origin).origin===`http://${e.headers.host}`}catch{}if(!r||!i)return e.resume(),void Y(o,403,{error:"System folder selection requires a local, same-origin editor page",browserSave:!0});const a=new AbortController,s=()=>a.abort();o.once("close",s);try{const r=Q(n),i=n.searchParams.get("src");let s,c;if(i)s=await l(r,i),c=t.basename(s),e.resume();else if(c=n.searchParams.get("name"),!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw Object.assign(new Error("Expected a completed video"),{status:400});const d=await f({source:s,name:c,stream:e,signal:a.signal});e.resume(),o.destroyed||Y(o,200,d)}catch(r){e.resume(),o.destroyed||Y(o,r.status||500,{error:r.message,browserSave:!0===r.browserSave||501===r.status})}finally{o.off("close",s)}return}if("/api/export/start"===i&&"POST"===e.method){try{const r=Q(n),t=JSON.parse((await K(e)).toString("utf8")||"{}"),i=t.engine||"optimized";if(!["fast","optimized","realtime"].includes(i))throw new Error("Unknown export engine");if("realtime"!==i&&(!N||"optimized"===i&&!_))throw new Error("Export needs ffmpeg"+("optimized"===i?" and ffprobe":""));if(void 0!==t.browser&&("string"!=typeof t.browser||!t.browser))throw new Error("browser must be a nonempty executable path");const a=void 0===t.timeoutSeconds?3600:t.timeoutSeconds;if(!Number.isFinite(a)||a<=0||a>86400)throw new Error("timeoutSeconds must be between 0 and 86400");const s=h.read(r.id);if(void 0!==t.revision&&t.revision!==s.revision)return void Y(o,409,{error:"Project changed; save/reload before exporting"});if(!s.clips?.length)throw new Error("Timeline is empty");const c=t.requestId??require("crypto").randomBytes(16).toString("hex");if("string"!=typeof c||!/^[a-f0-9]{32}$/.test(c))throw new Error("Invalid export request id");if(te.has(c))return void Y(o,409,{error:"Export request id already used"});oe.set(c,{projectId:r.id,snapshot:s,options:{engine:i,name:String(t.name||s.name||"export"),browser:t.browser,timeoutSeconds:a}}),de(c,{state:"queued",projectId:r.id,engine:i}),ae(),Y(o,202,{id:c,engine:i})}catch(e){Y(o,400,{error:String(e.message||e)})}return}if("/api/export/cancel"===i&&"POST"===e.method){try{const e=Q(n),r=n.searchParams.get("id");if(te.get(r)?.projectId!==e.id)return void Y(o,404,{error:"no such export request"});de(r,{state:"cancelled",projectId:e.id}),Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/ffmpeg"===i&&"GET"===e.method)return void Y(o,200,{available:N,ffprobe:_});if("/api/export/status"===i&&"GET"===e.method){try{const e=Q(n),r=te.get(n.searchParams.get("id"));if(!r||r.projectId!==e.id)return void Y(o,404,{error:"no such export request"});let t;if(!["complete","error","cancelled","finalizing"].includes(r.state)){const e=oe.get(n.searchParams.get("id")),r=e?.cacheIds||[...re.values()].filter(e=>e.requestId===n.searchParams.get("id")).map(e=>e.cacheId);for(const e of r){const r=A.sessions.get(e)?.wait;if(r){t={reason:r.reason,elapsedMs:Math.max(0,Date.now()-r.since)};break}}}Y(o,200,{...r,...t?{wait:t}:{}})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/recording"===i&&"POST"===e.method){let i,a;try{const s=Q(n),c=n.searchParams.get("exportJob");if(a=oe.get(c),!a?.child||a.stopping||a.projectId!==s.id||"realtime"!==a.options.engine)return void Y(o,404,{error:"No active recording for this project"});if(a.recordingUpload)return void Y(o,409,{error:"Recording upload already started"});if(n.searchParams.has("frames")){const e=Number(n.searchParams.get("frames")),r=Number(n.searchParams.get("totalFrames"));if(!Number.isSafeInteger(e)||!Number.isSafeInteger(r)||e<0||r<=0||e>r)throw new Error("Invalid recording progress");de(c,{state:"rendering",frames:e,totalFrames:r})}else{const o=n.searchParams.get("name");if(!m(o)||!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw new Error("Invalid recording");a.recordingUpload=new AbortController,de(c,{state:"finalizing"}),r.mkdirSync(s.exportsDir,{recursive:!0}),i=t.join(t.dirname(s.projectFile),".recording-"+c+".part");const d=r.createWriteStream(i,{flags:"wx"});if(await y(e,d,{signal:a.recordingUpload.signal}),a.recordingUpload.signal.throwIfAborted(),!d.bytesWritten)throw new Error("Empty recording");const p=X(s.exportsDir,o);r.renameSync(i,p),i=null,de(c,{state:"complete",src:"/projects/"+s.id+"/exports/"+encodeURIComponent(t.basename(p))})}Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:e.message})}finally{i&&r.rmSync(i,{force:!0})}return}if("/api/export/report"===i&&"POST"===e.method){try{const r=Q(n),t=JSON.parse((await K(e)).toString("utf8")||"{}"),i=te.get(t.requestId);i&&"error"===i.state||de(t.requestId,{state:"error",projectId:r.id,error:String(t.error||"browser export failed")}),Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/begin"===i&&"POST"===e.method){if(!N)return void Y(o,400,{error:"ffmpeg not found on PATH"});try{const r=Q(n),t=JSON.parse((await K(e)).toString("utf8")||"{}"),i=te.get(t.requestId);if(i&&(["complete","error","cancelled"].includes(i.state)||i.projectId!==r.id))throw new Error("Export request is no longer active");"optimized"===t.engine&&A.get(t.cacheId,r.id),Y(o,200,{id:pe(t.fps||30,t.name,r.id,t.requestId,"optimized"===t.engine?t.cacheId:void 0,t.totalFrames)})}catch(e){Y(o,500,{error:String(e)})}return}if("/api/export/frame"===i&&"POST"===e.method){const r=re.get(n.searchParams.get("id"));if(!r)return void Y(o,404,{error:"no such export session"});try{const t=await K(e);if(t.length<4||255!==t[0]||216!==t[1])throw new Error("export frame is not a valid JPEG");await s(r,t),r.frames++,r.frameBytes+=t.length,de(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),Y(o,200,{ok:!0})}catch(e){de(r.requestId,{state:"error",projectId:r.projectId,error:e.message}),le(n.searchParams.get("id")),Y(o,500,{error:String(e)})}return}if("/api/export/audio"===i&&"POST"===e.method){const i=re.get(n.searchParams.get("id"));if(!i)return void Y(o,404,{error:"no such export session"});try{i.wav=t.join(i.dir,"audio.wav"),r.writeFileSync(i.wav,await K(e)),Y(o,200,{ok:!0})}catch(e){Y(o,500,{error:String(e)})}return}if("/api/export/end"===i&&"POST"===e.method){const i=n.searchParams.get("id"),a=re.get(i);if(!a)return void Y(o,404,{error:"no such export session"});let s;try{if(n.searchParams.get("discard"))return await le(i),void Y(o,200,{ok:!0});if(a.finalizing||a.cleaning)throw new Error("Export session is already ending");const c=Date.now(),p=await K(e),l=p.length?JSON.parse(p.toString()).metrics:void 0;if(!a.frames)throw new Error("export received no video frames");if(a.finalizing=!0,de(a.requestId,{state:"finalizing",projectId:a.projectId,frames:a.frames,totalFrames:a.totalFrames}),a.proc.stdin.end(),0!==await a.done)throw new Error("ffmpeg encode failed: "+a.err());if(a.controller.signal.aborted)throw new Error("export cancelled");const m=h.context(a.projectId);r.mkdirSync(m.exportsDir,{recursive:!0}),s=a.output=X(m.exportsDir,a.name.replace(/\.mp4$/i,"")+".mp4");const f=["-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv"];if(a.wav&&r.existsSync(a.wav)?a.muxDone=d("ffmpeg",["-y","-i",a.videoPath,"-i",a.wav,"-c:v","copy","-c:a","aac","-b:a","192k",...a.cacheId?[]:["-shortest"],...f,"-movflags","+faststart",s],a.controller.signal):a.muxDone=d("ffmpeg",["-y","-i",a.videoPath,"-c","copy",...f,"-movflags","+faststart",s],a.controller.signal),await a.muxDone,a.controller.signal.aborted)throw new Error("export cancelled");l&&"object"==typeof l&&(l.phases||={},l.phases.finalizeMs=Date.now()-c,l.totalMs=(Number(l.totalMs)||0)+l.phases.finalizeMs,l.fps=a.frames/(l.totalMs/1e3),l.serverRssBytes=process.memoryUsage().rss),a.committed=!0,de(a.requestId,{state:"complete",projectId:m.id,metrics:l,src:"/projects/"+encodeURIComponent(m.id)+"/exports/"+encodeURIComponent(t.basename(s))}),await le(i),Y(o,200,{ok:!0,metrics:l,src:"/projects/"+encodeURIComponent(m.id)+"/exports/"+encodeURIComponent(t.basename(s))})}catch(e){const r=`${String(e)} (frames=${a.frames}, bytes=${a.frameBytes})`;de(a.requestId,{state:"error",projectId:a.projectId,error:r}),await le(i),Y(o,500,{error:r})}return}if("/api/analyze"===i&&"GET"===e.method){let e;try{e=Q(n)}catch(e){return void Y(o,400,{error:String(e)})}const i=decodeURIComponent(n.searchParams.get("src")||""),a=t.join(e.analysisDir,t.basename(i,t.extname(i))+".json");if(!i||!r.existsSync(a))return void Y(o,404,{error:"no cached analysis for that src — POST /api/analyze first"});try{Y(o,200,JSON.parse(r.readFileSync(a,"utf8")))}catch(e){Y(o,500,{error:String(e)})}return}if("/api/analyze"===i&&"POST"===e.method){if(!N)return void Y(o,400,{error:"ffmpeg not found on PATH"});try{const i=Q(n),a=JSON.parse((await K(e)).toString("utf8")||"{}"),s=t.basename(decodeURIComponent(a.src||"")),c=t.join(i.mediaDir,s);if(!s||!r.existsSync(c))return void Y(o,404,{error:"src must name an existing file under /media/"});const d=await u(c,{threshold:a.threshold,music:!1!==a.music,musicDir:i.mediaDir,srcUrl:Z(i.id,s)});d.music&&(d.music.src=Z(i.id,d.music.name)),r.writeFileSync(t.join(i.analysisDir,t.basename(s,t.extname(s))+".json"),JSON.stringify(d,null,2)),Y(o,200,d)}catch(e){Y(o,500,{error:String(e)})}return}if("/api/events"===i){let r;try{r=Q(n)}catch(e){return void Y(o,400,{error:String(e)})}o.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive"}),o.write("data: hello\n\n");const t={res:o,projectId:r.id};return J.add(t),void e.on("close",()=>J.delete(t))}const a=/^\/projects\/([a-z0-9_-]+)\/(media|exports)\/(.+)$/.exec(i);if(a){try{const n=T(a[1]);if(!r.existsSync(n.projectFile))throw new Error("no such project");const i="media"===a[2]?n.mediaDir:n.exportsDir;me(e,o,t.join(i,t.basename(a[3])))}catch{o.writeHead(404),o.end("Not found")}return}if(i.startsWith("/media/")||i.startsWith("/exports/")){try{const r=h.context(P),n=i.startsWith("/media/")?r.mediaDir:r.exportsDir;me(e,o,t.join(n,t.basename(i)))}catch{o.writeHead(404),o.end("Not found")}return}if(i.startsWith("/library/")){const r=t.normalize(t.join(j,i.slice(9)));return r.startsWith(j+t.sep)?void me(e,o,r):(o.writeHead(403),void o.end())}let c="/"===i?"/index.html":i;if(c=t.normalize(t.join(k,c)),!c.startsWith(k+t.sep))return o.writeHead(403),void o.end();me(e,o,c)});function ue(){const e=process.env.FABLECUT_VERSION;return"string"==typeof e&&e.trim()?e.trim():""}function he(e){const r="0.0.0.0"===R?"127.0.0.1":"::"===R?"::1":R;return`http://${r.includes(":")&&!r.startsWith("[")?`[${r}]`:r}:${e}`}function ge(){return t.join(x,"server.json")}function we(){try{JSON.parse(r.readFileSync(ge(),"utf8")).pid===process.pid&&r.rmSync(ge(),{force:!0})}catch{}}process.on("exit",we);const Se=function(){if("auto"===process.env.PORT)return{port:7777,fallback:!0};const e=Number(process.env.PORT||7777);return(!Number.isInteger(e)||e<0||e>65535)&&(console.error("Invalid PORT"),process.exit(1)),{port:e,fallback:"1"===process.env.FABLECUT_PORT_FALLBACK&&0!==e}}();!function e(t,o){const n=r=>{o&&r&&"EADDRINUSE"===r.code&&0!==t?e(0,!1):(console.error(r&&r.message?r.message:r),process.exit(1))};fe.once("error",n),fe.listen(t,R,()=>{fe.removeListener("error",n);const e=fe.address().port;!function(e){const t={pid:process.pid,host:R,port:e,url:he(e)+"/",version:ue(),dataDir:r.realpathSync(x),startedAt:(new Date).toISOString()},o=ge(),n=o+".tmp";r.writeFileSync(n,JSON.stringify(t)+"\n"),r.renameSync(n,o)}(e),console.log(`\n FableCut running → ${he(e)}/\n`),["127.0.0.1","localhost","::1"].includes(R)||console.log(` ⚠ WARNING: HOST=${R} exposes the editor (and its file APIs) to the network.\n`),console.log(` projects : ${b} (${O().length})`),console.log(` library : ${j} (${I.join(", ")})`),x!==v&&console.log(` app files : ${v}`),console.log(` ffmpeg : ${N?"found (fast export + faststart remux on)":"not found (real-time export only)"}\n`)})}(Se.port,Se.fallback);
|