@tikagent/video-editor-cli 1.5.0-preview.2 → 1.5.0-preview.3

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.
@@ -0,0 +1 @@
1
+ !function(e){const t="object"==typeof module&&module.exports,a=function(){"use strict";const e=(e,t,a)=>Math.min(a,Math.max(t,e)),t=(e,t)=>null!=e&&""!==e&&Number.isFinite(+e)?+e:t,a=e=>Math.max(2,2*Math.round(e/2)),o="object"==typeof process&&process&&"darwin"===process.platform&&"function"==typeof require?parseInt(require("os").release(),10)||22:null,r="object"==typeof process&&process&&"darwin"===process.platform?process.arch:null,n={mp4:{label:"MP4",ext:".mp4",mov:!0},mov:{label:"QuickTime MOV",ext:".mov",mov:!0},mkv:{label:"Matroska MKV",ext:".mkv"},webm:{label:"WebM",ext:".webm"},mxf:{label:"MXF (OP1a)",ext:".mxf"}},s={h264:{label:"H.264",encoders:["libx264","h264_videotoolbox","h264_nvenc","h264_qsv","h264_amf"],containers:["mp4","mov","mkv"],rate:["crf","vbr","cbr"],crf:[0,51,18],pixFmts:["yuv420p","yuv422p","yuv444p","yuv420p10le","yuv422p10le"]},hevc:{label:"H.265 / HEVC",encoders:["libx265","hevc_videotoolbox","hevc_nvenc","hevc_qsv","hevc_amf"],containers:["mp4","mov","mkv"],rate:["crf","vbr","cbr"],crf:[0,51,22],pixFmts:["yuv420p","yuv420p10le","yuv422p10le","yuv444p"]},av1:{label:"AV1",encoders:["libsvtav1","libaom-av1","av1_nvenc","av1_qsv","av1_amf"],containers:["mp4","mkv","webm"],rate:["crf","vbr","cbr"],crf:[0,63,30],pixFmts:["yuv420p","yuv420p10le"]},vp9:{label:"VP9",encoders:["libvpx-vp9"],containers:["webm","mkv","mp4"],rate:["crf","vbr","cbr"],crf:[0,63,31],pixFmts:["yuv420p","yuv420p10le"]},prores:{label:"Apple ProRes",encoders:["prores_ks"],containers:["mov"],rate:["fixed"],profiles:{proxy:"Proxy",lt:"LT",standard:"422",hq:"422 HQ",4444:"4444","4444xq":"4444 XQ"},defaultProfile:"hq",pixFmts:["yuv422p10le","yuv444p10le"]},dnxhr:{label:"Avid DNxHR",encoders:["dnxhd"],containers:["mov","mxf"],rate:["fixed"],profiles:{lb:"LB",sq:"SQ",hq:"HQ",hqx:"HQX 10-bit",444:"444 10-bit"},defaultProfile:"hq",pixFmts:["yuv422p","yuv422p10le","yuv444p10le"]},mpeg2:{label:"XDCAM HD422 (MPEG-2 50 Mb/s)",encoders:["mpeg2video"],containers:["mxf","mov"],rate:["fixed"],pixFmts:["yuv422p"]},ffv1:{label:"FFV1 (lossless)",encoders:["ffv1"],containers:["mkv"],rate:["fixed"],pixFmts:["yuv422p10le","yuv444p10le","yuv420p"]}},i={yuv420p:"nv12",yuv420p10le:"p010le",yuv444p:"yuv444p"},l={libx264:{label:"x264 (software)"},h264_videotoolbox:{label:"Apple Silicon VideoToolbox",hw:"videotoolbox",pix:["yuv420p"]},h264_nvenc:{label:"NVIDIA NVENC",hw:"nvenc",pix:["yuv420p","yuv444p","yuv420p10le"]},h264_qsv:{label:"Intel Quick Sync",hw:"qsv",pix:["yuv420p"]},h264_amf:{label:"AMD AMF",hw:"amf",pix:["yuv420p"]},libx265:{label:"x265 (software)"},hevc_videotoolbox:{label:"Apple Silicon VideoToolbox",hw:"videotoolbox",pix:["yuv420p","yuv420p10le"]},hevc_nvenc:{label:"NVIDIA NVENC",hw:"nvenc",pix:["yuv420p","yuv420p10le","yuv444p"]},hevc_qsv:{label:"Intel Quick Sync",hw:"qsv",pix:["yuv420p","yuv420p10le"]},hevc_amf:{label:"AMD AMF",hw:"amf",pix:["yuv420p","yuv420p10le"]},libsvtav1:{label:"SVT-AV1 (software)"},"libaom-av1":{label:"libaom (software, slow)"},av1_nvenc:{label:"NVIDIA NVENC",hw:"nvenc",pix:["yuv420p","yuv420p10le"]},av1_qsv:{label:"Intel Quick Sync",hw:"qsv",pix:["yuv420p","yuv420p10le"]},av1_amf:{label:"AMD AMF",hw:"amf",pix:["yuv420p"]},"libvpx-vp9":{label:"libvpx (software)"},prores_ks:{label:"prores_ks"},dnxhd:{label:"dnxhd"},mpeg2video:{label:"mpeg2video"},ffv1:{label:"ffv1"}},c={aac:{label:"AAC",encoder:"aac",bitrate:[64,512,320],containers:["mp4","mov","mkv"]},opus:{label:"Opus",encoder:"libopus",bitrate:[32,512,192],containers:["webm","mkv","mp4"]},mp3:{label:"MP3",encoder:"libmp3lame",bitrate:[64,320,320],containers:["mp4","mov","mkv"]},pcm16:{label:"PCM 16-bit",encoder:"pcm_s16le",containers:["mov","mkv","mxf"]},pcm24:{label:"PCM 24-bit",encoder:"pcm_s24le",containers:["mov","mkv","mxf"]},flac:{label:"FLAC (lossless)",encoder:"flac",containers:["mkv"]},none:{label:"No audio",containers:["mp4","mov","mkv","webm","mxf"]}},p=[44100,48e3],u={bt709:{label:"Rec. 709 (limited)",matrix:"bt709",primaries:"bt709",trc:"bt709",range:"tv",scale:"bt709"},"bt709-full":{label:"Rec. 709 (full range)",matrix:"bt709",primaries:"bt709",trc:"bt709",range:"pc",scale:"bt709"},"bt601-625":{label:"Rec. 601 PAL (limited)",matrix:"bt470bg",primaries:"bt470bg",trc:"smpte170m",range:"tv",scale:"bt470"},"bt601-525":{label:"Rec. 601 NTSC (limited)",matrix:"smpte170m",primaries:"smpte170m",trc:"smpte170m",range:"tv",scale:"smpte170m"}},d=[23.976,24,25,29.97,30,48,50,59.94,60];function h(e){const t=+e;for(const e of[24,30,60,120])if(Math.abs(t-1e3*e/1001)<.01)return 1e3*e+"/1001";return String(Math.round(1e3*t)/1e3)}const m=["fast","balanced","best"],f=(e,t)=>({codec:"h264",rate:"crf",crf:e,keyint:2,pixFmt:"yuv420p",speed:"balanced",...t}),b={youtube:{label:"YouTube",group:"Platforms",width:1920,height:1080,container:"mp4",video:f(18,{keyint:.5}),audio:{codec:"aac",bitrate:384,sampleRate:48e3,channels:2},filename:"{project}_youtube_{res}",note:"16:9 · H.264 High · CRF 18 · AAC 384k"},"youtube-shorts":{label:"YouTube Shorts",group:"Platforms",width:1080,height:1920,container:"mp4",video:f(18,{keyint:.5}),audio:{codec:"aac",bitrate:384,sampleRate:48e3,channels:2},filename:"{project}_shorts_{res}",limits:{duration:180,vertical:!0},note:"9:16 · up to 3 min"},"instagram-reels":{label:"Instagram Reels",group:"Platforms",width:1080,height:1920,container:"mp4",video:f(19,{maxrate:25e3,keyint:1}),audio:{codec:"aac",bitrate:256,sampleRate:48e3,channels:2},filename:"{project}_reel_{res}",limits:{duration:180,vertical:!0,fps:60},note:"9:16 · ≤ 60 fps · capped at 25 Mb/s"},tiktok:{label:"TikTok",group:"Platforms",width:1080,height:1920,container:"mp4",video:f(19,{maxrate:2e4,keyint:1}),audio:{codec:"aac",bitrate:256,sampleRate:48e3,channels:2},filename:"{project}_tiktok_{res}",limits:{duration:600,vertical:!0,fps:60},note:"9:16 · ≤ 60 fps"},x:{label:"X (Twitter)",group:"Platforms",width:1920,height:1080,container:"mp4",video:f(20,{maxrate:25e3,keyint:1}),audio:{codec:"aac",bitrate:192,sampleRate:48e3,channels:2},filename:"{project}_x_{res}",limits:{duration:140,size:536870912,fps:60},note:"≤ 2 min 20 s and 512 MB on a free account"},linkedin:{label:"LinkedIn",group:"Platforms",width:1920,height:1080,container:"mp4",video:f(20,{maxrate:2e4,keyint:1}),audio:{codec:"aac",bitrate:192,sampleRate:48e3,channels:2},filename:"{project}_linkedin_{res}",limits:{duration:900,size:5*1024**3,fps:60},note:"≤ 15 min and 5 GB"},master:{label:"Master",group:"Delivery",width:null,height:null,container:"mp4",video:f(16,{speed:"best",keyint:2}),audio:{codec:"aac",bitrate:320,sampleRate:48e3,channels:2},filename:"{project}_master_{res}",note:"Project size · H.264 CRF 16 · AAC 320k"},broadcast:{label:"Broadcast",group:"Delivery",width:1920,height:1080,container:"mov",video:{codec:"prores",profile:"hq",pixFmt:"yuv422p10le"},audio:{codec:"pcm24",sampleRate:48e3,channels:2,loudness:-23,truePeak:-1},filename:"{project}_broadcast_{res}_{fps}",limits:{fpsList:[25,29.97,50,59.94]},note:"ProRes 422 HQ · PCM 24-bit 48 kHz · Rec. 709 · −23 LUFS (EBU R128)"},web:{label:"Web",group:"Delivery",width:1280,height:720,container:"mp4",video:f(23,{speed:"balanced"}),audio:{codec:"aac",bitrate:128,sampleRate:48e3,channels:2},filename:"{project}_web_{res}",note:"Small, plays everywhere · fast start"},archive:{label:"Archive",group:"Delivery",width:null,height:null,container:"mkv",video:{codec:"ffv1",pixFmt:"yuv422p10le"},audio:{codec:"pcm24",sampleRate:48e3,channels:2},filename:"{project}_archive_{date}",note:"Lossless FFV1 · PCM 24-bit · source resolution"},custom:{label:"Custom",group:"Custom",width:null,height:null,container:"mp4",video:f(18),audio:{codec:"aac",bitrate:320,sampleRate:48e3,channels:2},filename:"{project}_{res}",note:"Your own settings"}},v=Object.keys(b),x={youtube:-14,"youtube-shorts":-14,"instagram-reels":-14,tiktok:-14,x:-14,linkedin:-14,broadcast:-23};function g(e,t,a){return e.includes(t)?t:a}function $(e,t){return String(e??"").replace(/[\u0000-\u001f\u007f]+/g," ").trim().slice(0,t)}function _(e,t){const a=(s[e]||s.h264).encoders;return t?a.filter(e=>t.has(e)):a.slice()}function y(n,i={}){const h=[],f=i.presets||{};let v=n;"string"==typeof v&&(v={preset:v}),v=v&&"object"==typeof v?v:{};const x="string"==typeof v.preset?v.preset:null,y=x&&(b[x]||f[x])||null;x&&!y&&h.push(`unknown preset "${x}" — using Custom`);const k=y||b.custom,M=v.video&&"object"==typeof v.video?v.video:{},S={...k.video||{}};if(M.codec&&M.codec!==S.codec)for(const e of["crf","pixFmt","profile","encoder","rate"])delete S[e];const F={...S,...M},R={...k.audio||{},...v.audio||{}},A=e=>Object.prototype.hasOwnProperty.call(v,e),P={preset:y?x:"custom"};let q=A("width")?v.width:k.width,j=A("height")?v.height:k.height;if(q=null==q||""===q?null:t(q,null),j=null==j||""===j?null:t(j,null),null==q!=(null==j)&&(h.push("width and height go together — using the picture's own size"),q=j=null),null!=q){const t=a(e(q,16,8192)),o=a(e(j,16,8192));t===q&&o===j||h.push(`resolution ${q}×${j} → ${t}×${o} (even, 16…8192)`),q=t,j=o}P.width=q,P.height=j;let C=A("fps")?v.fps:k.fps;if(C=null==C||""===C||"project"===C?null:t(C,null),null!=C){const t=e(C,1,120),a=d.find(e=>Math.abs(e-t)<.02);P.fps=a??Math.round(1e3*t)/1e3}else P.fps=null;P.reframe=g(["fill","fit","stretch"],A("reframe")?v.reframe:k.reframe,"fill");const L=s[F.codec]?F.codec:"h264";F.codec&&!s[F.codec]&&h.push(`unknown codec "${F.codec}" — using H.264`);const H=s[L],E=_(L,i.available);let T=F.encoder&&H.encoders.includes(F.encoder)?F.encoder:null;F.encoder&&!T&&h.push(`encoder "${F.encoder}" does not make ${H.label}`),T&&i.available&&!i.available.has(T)&&(h.push(`this ffmpeg has no ${T}`),T=null),T||(T=E[0]||H.encoders[0]),i.available&&!E.length&&h.push(`this ffmpeg has no ${H.label} encoder — the export will fail`);const D=l[T]||{};let z=A("container")?v.container:k.container;H.containers.includes(z)||(z&&h.push(`${H.label} does not go in ${String(z).toUpperCase()} — using ${H.containers[0].toUpperCase()}`),z=H.containers[0]),P.container=z;const N={codec:L,encoder:T};if(N.rate=H.rate.includes(F.rate)?F.rate:H.rate[0],F.rate&&F.rate!==N.rate&&"fixed"!==H.rate[0]&&h.push(`rate "${F.rate}" is not offered for ${H.label}`),"fixed"!==N.rate){const[a,n,s]=H.crf;if(N.crf=Math.round(e(t(F.crf,s),a,n)),N.bitrate=Math.round(e(t(F.bitrate,12e3),100,4e5)),N.maxrate=+F.maxrate>0?Math.round(e(+F.maxrate,100,8e5)):null,N.keyint=Math.round(100*e(t(F.keyint,2),0,20))/100,N.speed=g(m,F.speed,"balanced"),"libsvtav1"===T&&"cbr"===N.rate&&(N.rate="vbr",h.push("SVT-AV1 has no CBR here — using VBR")),"videotoolbox"===D.hw){const e=i.arch||r||"arm64",t=i.darwin??o??22;"crf"===N.rate&&"arm64"!==e?(N.rate="vbr",h.push("VideoToolbox constant quality (-q:v) needs Apple Silicon — using VBR")):"cbr"===N.rate&&("arm64"!==e||t<22)&&(N.rate="vbr",h.push("VideoToolbox CBR needs macOS 13+ on Apple Silicon — using VBR"))}}H.profiles&&(N.profile=H.profiles[F.profile]?String(F.profile):H.defaultProfile);let O=H.pixFmts.includes(F.pixFmt)?F.pixFmt:H.pixFmts[0];"prores"===L&&(O=/^4444/.test(N.profile)?"yuv444p10le":"yuv422p10le"),"dnxhr"===L&&(O="444"===N.profile?"yuv444p10le":"hqx"===N.profile?"yuv422p10le":"yuv422p"),D.pix&&!D.pix.includes(O)&&(h.push(`${D.label} cannot encode ${O} — using ${D.pix[0]}`),O=D.pix[0]),M.pixFmt&&M.pixFmt!==O&&!h.some(e=>e.includes(O))&&h.push(`pixel format ${M.pixFmt} → ${O} for ${H.label}${N.profile?" "+H.profiles[N.profile]:""}`),N.pixFmt=O,P.video=N;let U=c[R.codec]?R.codec:"aac";if(!c[U].containers.includes(z)){const e=Object.keys(c).find(e=>"none"!==e&&c[e].containers.includes(z))||"none";h.push(`${c[U].label} audio does not go in ${z.toUpperCase()} — using ${c[e].label}`),U=e}const V=c[U],B={codec:U};if("none"!==U){V.bitrate&&(B.bitrate=Math.round(e(t(R.bitrate,V.bitrate[2]),V.bitrate[0],V.bitrate[1]))),B.sampleRate=g(p,+R.sampleRate,48e3),"opus"===U&&(B.sampleRate=48e3),B.channels=1===+R.channels?1:2;const a=null==R.loudness||""===R.loudness||"off"===R.loudness?null:+R.loudness;B.loudness=Number.isFinite(a)?Math.round(10*e(a,-36,-5))/10:null,null!=B.loudness&&(B.truePeak=Math.round(10*e(t(R.truePeak,-1),-9,0))/10)}P.audio=B,P.color=u[A("color")?v.color:k.color]?A("color")?v.color:k.color:"bt709",P.filename=$(A("filename")?v.filename:k.filename,160)||"{project}_{res}";const I=v.metadata&&"object"==typeof v.metadata?v.metadata:{};P.metadata={};for(const e of w){const t=$(I[e],"description"===e?2e3:300);t&&(P.metadata[e]=t)}return{spec:P,notes:h}}const w=["title","artist","description","copyright","date","keywords"];function k(e,t,o,r){const n={x:0,y:0,w:0|t,h:0|o};let s=r&&r.w>0&&r.h>0?{x:0|r.x,y:0|r.y,w:0|r.w,h:0|r.h}:n;s!==n&&e.width&&Math.abs(e.width/e.height-s.w/s.h)>.01&&(s=n);const i=e.width||Math.max(2,s.w-(1&s.w)),l=e.height||Math.max(2,s.h-(1&s.h));let c={...s},p=i,u=l,d=0,h=0;const m=i/l,f=s.w/s.h;return e.width&&Math.abs(m-f)>.002&&("fill"===e.reframe?f>m?(c.w=Math.round(s.h*m),c.x=s.x+Math.round((s.w-c.w)/2)):(c.h=Math.round(s.w/m),c.y=s.y+Math.round((s.h-c.h)/2)):"fit"===e.reframe&&(f>m?u=a(i/f):p=a(l*f),d=i-p>>1,h=l-u>>1,d-=1&d,h-=1&h)),c.w=Math.max(2,c.w-(1&c.w)),c.h=Math.max(2,c.h-(1&c.h)),{crop:0===c.x&&0===c.y&&c.w===t&&c.h===o?null:c,scaleW:p,scaleH:u,outW:i,outH:l,padX:d,padY:h,upscale:p>1.01*c.w||u>1.01*c.h,from:{w:c.w,h:c.h}}}const M={libx264:{fast:["-preset","veryfast"],balanced:["-preset","medium"],best:["-preset","slow"]},libx265:{fast:["-preset","fast"],balanced:["-preset","medium"],best:["-preset","slow"]},videotoolbox:{fast:["-prio_speed","1"],balanced:[],best:["-prio_speed","0"]},nvenc:{fast:["-preset","p3"],balanced:["-preset","p5"],best:["-preset","p7"]},qsv:{fast:["-preset","veryfast"],balanced:["-preset","medium"],best:["-preset","veryslow"]},amf:{fast:["-quality","speed"],balanced:["-quality","balanced"],best:["-quality","quality"]},"libvpx-vp9":{fast:["-deadline","good","-cpu-used","4"],balanced:["-deadline","good","-cpu-used","2"],best:["-deadline","good","-cpu-used","1"]},"libaom-av1":{fast:["-cpu-used","8"],balanced:["-cpu-used","6"],best:["-cpu-used","4"]},libsvtav1:{fast:["-preset","10"],balanced:["-preset","8"],best:["-preset","5"]}},S={proxy:"0",lt:"1",standard:"2",hq:"3",4444:"4","4444xq":"5"};function F(e,t,a){const o=u[e.color]||u.bt709,r=[];return t.crop&&r.push(`crop=${t.crop.w}:${t.crop.h}:${t.crop.x}:${t.crop.y}`),r.push(`scale=${t.scaleW}:${t.scaleH}:flags=lanczos:in_range=full:in_color_matrix=${a}:out_range=${o.range}:out_color_matrix=${o.scale}`),r.push(`setparams=color_primaries=${o.primaries}:color_trc=${o.trc}:colorspace=${o.matrix}:range=${"pc"===o.range?"pc":"tv"}`),(t.padX||t.padY||t.scaleW!==t.outW||t.scaleH!==t.outH)&&r.push(`pad=${t.outW}:${t.outH}:${t.padX}:${t.padY}:color=black`),r.join(",")}function R(e,t){const a=e.audio;if(null==a.loudness||!t)return"";const o=Math.min(20,Math.max(11,Math.ceil(t.input_lra+1)));return`loudnorm=I=${a.loudness}:TP=${a.truePeak??-1}:LRA=${o}:measured_I=${t.input_i}:measured_TP=${t.input_tp}:measured_LRA=${t.input_lra}:measured_thresh=${t.input_thresh}:offset=${t.target_offset}:linear=true`}function A(e){const t=Math.floor(e/60),a=e-60*t;return`${t}:${a<10?"0":""}${a.toFixed(2)}`}function P(e){return String(e).padStart(2,"0")}function q(e,t,a={}){const o=a.date instanceof Date?a.date:new Date,r=a.width||t.width||0,n=a.height||t.height||0,s=a.fps||t.fps||0,i=r&&n?r>n?`${n}p`:r===n?`${r}sq`:`${r}x${n}`:"source",l={project:a.project||"export",preset:t.preset||"custom",w:r,h:n,width:r,height:n,res:i,fps:s?String(Math.round(100*s)/100).replace(".","_"):"",codec:t.video.codec,date:`${o.getFullYear()}-${P(o.getMonth()+1)}-${P(o.getDate())}`,time:`${P(o.getHours())}${P(o.getMinutes())}`,range:"in-out"===a.range?"inout":"full",n:null!=a.index?String(a.index):"1"};let c=String(e||"{project}").replace(/\{(\w+)\}/g,(e,t)=>t in l?String(l[t]):e);return c=c.replace(/[\\/:*?"<>|\u0000-\u001f]+/g,"_").replace(/\s+/g," ").trim().replace(/^\.+/,""),c.slice(0,120)||"export"}function j(e){return e>=1e3?Math.round(e/100)/10+" Mb/s":`${e} kb/s`}function C(e,t={}){const a=e.video,o=e.audio,r=s[a.codec],n=e.width||t.width,i=e.height||t.height,l=[n&&i?`${n}×${i}`:"source size",`${e.fps||t.fps||"project"} fps`];let p=r.label.replace(/ \(.+\)$/,"");return a.profile&&r.profiles&&(p+=" "+r.profiles[a.profile]),"crf"===a.rate?p+=` CRF ${a.crf}`+(a.maxrate?` ≤ ${j(a.maxrate)}`:""):"vbr"===a.rate?p+=` VBR ${j(a.bitrate)}`:"cbr"===a.rate&&(p+=` CBR ${j(a.bitrate)}`),l.push(p),l.push("none"===o.codec?"no audio":`${c[o.codec].label}${o.bitrate?" "+o.bitrate+"k":""}${null!=o.loudness?` · ${o.loudness} LUFS`:""}`),l.push(e.container.toUpperCase()),l.join(" · ")}const L={prores:{proxy:45,lt:102,standard:147,hq:220,4444:330,"4444xq":500},dnxhr:{lb:45,sq:145,hq:220,hqx:220,444:440}};function H(e,t){const a=e.video,o=e.audio,r=t.duration||0,n=e.width||t.width||1920,s=e.height||t.height||1080,i=e.fps||t.fps||30;let l=null;if("vbr"===a.rate||"cbr"===a.rate?l=a.bitrate:"mpeg2"===a.codec?l=5e4:L[a.codec]&&(l=1e3*L[a.codec][a.profile]*(n*s*i)/62208e3),null==l)return null;const c="none"===o.codec?0:o.bitrate||("pcm24"===o.codec?24:16)*o.sampleRate*o.channels/1e3;return Math.round(1e3*(l+c)/8*r)}function E(e){e=Math.round(e);const t=Math.floor(e/60);return t?`${t} min${e%60?` ${e%60} s`:""}`:`${e} s`}function T(e){return e>=1024**3?`${(e/1024**3).toFixed(1)} GB`:e>=1048576?`${(e/1048576).toFixed(e>=104857600?0:1)} MB`:`${Math.max(1,Math.round(e/1024))} KB`}function D(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40)}return{CONTAINERS:n,CODECS:s,ENCODERS:l,AUDIO:c,COLORS:u,PRESETS:b,PRESET_IDS:v,DEFAULT_PRESET:"master",FPS_CHOICES:d,RESOLUTIONS:[[3840,2160,"4K UHD"],[2560,1440,"1440p"],[1920,1080,"1080p"],[1280,720,"720p"],[854,480,"480p"],[4096,2160,"DCI 4K"],[2160,3840,"4K vertical"],[1080,1920,"9:16 vertical"],[720,1280,"9:16 720"],[1080,1350,"4:5 portrait"],[1080,1080,"1:1 square"]],SPEEDS:m,SAMPLE_RATES:p,META_KEYS:w,FILENAME_TOKENS:["project","preset","res","w","h","fps","codec","date","time","range","n"],normalize:y,encodersFor:_,reframe:k,ffmpegArgs:function(e,t){const a=h(t.fps),o=["-y","-hide_banner"],r="rgba"===t.input;t.dry?o.push("-f","lavfi","-i",`color=c=black:s=${t.canvasW}x${t.canvasH}:r=${a}:d=0.1,format=${r?"rgba":"yuvj420p"}`):r?o.push("-thread_queue_size","8","-f","rawvideo","-pix_fmt","rgba","-s",`${t.canvasW}x${t.canvasH}`,"-framerate",a,"-i","-"):o.push("-thread_queue_size","64","-f","image2pipe","-framerate",a,"-c:v","mjpeg","-i","-");const s="none"!==e.audio.codec&&(t.dry?!1!==t.hasAudio:!!t.wavPath);s&&(t.dry?o.push("-f","lavfi","-i","anullsrc=r=48000:cl=stereo"):o.push("-i",t.wavPath)),o.push("-map","0:v:0"),s&&o.push("-map","1:a:0"),o.push("-vf",F(e,t.frame,r?"bt709":"bt601")),o.push(...function(e,t){const a=e.video,o=a.encoder,r=l[o]||{},n=["-c:v",o],s=e=>`${Math.round(e)}k`;if("prores"===a.codec)return n.concat(["-profile:v",S[a.profile]||"3","-vendor","apl0","-pix_fmt",a.pixFmt]);if("dnxhr"===a.codec)return n.concat(["-profile:v",`dnxhr_${a.profile}`,"-pix_fmt",a.pixFmt]);if("ffv1"===a.codec)return n.concat(["-level","3","-g","1","-slices","16","-slicecrc","1","-coder","1","-context","1","-pix_fmt",a.pixFmt]);if("mpeg2"===a.codec)return n.concat(["-pix_fmt","yuv422p","-b:v","50M","-minrate","50M","-maxrate","50M","-bufsize","17825792","-rc_init_occupancy","17825792","-g","12","-bf","2","-intra_vlc","1","-non_linear_quant","1","-dc","10","-qmin","1","-qmax","12","-lmin","1*QP2LAMBDA","-rc_max_vbv_use","1","-rc_min_vbv_use","1"]);n.push(...(M[r.hw||o]||{})[a.speed]||[]);const c=a.bitrate,p=a.maxrate;if("crf"===a.rate)"libx264"===o||"libx265"===o||"libsvtav1"===o?n.push("-crf",String(a.crf)):"libvpx-vp9"===o||"libaom-av1"===o?n.push("-crf",String(a.crf),"-b:v","0"):"nvenc"===r.hw?n.push("-rc","vbr","-cq",String(a.crf),"-b:v","0"):"videotoolbox"===r.hw?n.push("-q:v",String(Math.max(1,Math.min(100,Math.round(100*(51-a.crf)/51))))):"qsv"===r.hw?n.push("-global_quality",String(a.crf)):"amf"===r.hw&&n.push("-rc","cqp","-qp_i",String(a.crf),"-qp_p",String(a.crf),..."h264"===a.codec?["-qp_b",String(a.crf)]:[]),p&&"amf"!==r.hw&&("libsvtav1"===o?n.push("-svtav1-params",`mbr=${p}`):n.push("-maxrate",s(p),"-bufsize",s(2*p)));else if("vbr"===a.rate){const e=p||Math.round(1.5*c);"nvenc"===r.hw&&n.push("-rc","vbr"),"amf"===r.hw&&n.push("-rc","vbr_peak"),n.push("-b:v",s(c),"-maxrate",s(e),"-bufsize",s(2*e))}else"cbr"===a.rate&&("nvenc"===r.hw&&n.push("-rc","cbr"),"amf"===r.hw&&n.push("-rc","cbr"),"videotoolbox"===r.hw&&n.push("-constant_bit_rate","1"),n.push("-b:v",s(c),"-minrate",s(c),"-maxrate",s(c),"-bufsize",s(c)),"libx264"===o&&n.push("-x264-params","nal-hrd=cbr"));if(a.keyint>0){const e=Math.max(1,Math.round(a.keyint*t));n.push("-g",String(e)),"libx264"!==o&&"libx265"!==o||n.push("-keyint_min",String(Math.min(e,Math.max(1,Math.round(t/2)))))}return"libvpx-vp9"!==o&&"libaom-av1"!==o||n.push("-row-mt","1"),"libx265"===o&&n.push("-x265-params","log-level=error"),"hevc"!==a.codec||"mp4"!==e.container&&"mov"!==e.container||n.push("-tag:v","hvc1"),n.push("-pix_fmt",r.hw&&i[a.pixFmt]||a.pixFmt),n}(e,+t.fps));const p=u[e.color]||u.bt709;o.push("-colorspace",p.matrix,"-color_primaries",p.primaries,"-color_trc",p.trc,"-color_range",p.range);const d=+t.duration>0?(+t.duration).toFixed(6):null;s&&!t.dry&&o.push("-af",[t.loudnorm?R(e,t.loudnorm):"",d?`apad=whole_dur=${d},atrim=0:${d}`:"apad"].filter(Boolean).join(",")),o.push(...s?function(e){const t=e.audio;if("none"===t.codec)return["-an"];const a=["-c:a",c[t.codec].encoder];return t.bitrate&&a.push("-b:a",`${t.bitrate}k`),a.push("-ar",String(t.sampleRate),"-ac",String(t.channels)),a}(e):["-an"]);for(const[t,a]of Object.entries(e.metadata||{}))"description"===t?o.push("-metadata",`description=${a}`,"-metadata",`comment=${a}`):o.push("-metadata",`${t}=${a}`);return n[e.container].mov&&o.push("-movflags","+faststart"+(e.metadata&&e.metadata.keywords?"+use_metadata_tags":"")),t.dry?o.push("-t","0.1"):s&&!d&&o.push("-shortest"),o.push(t.outPath),o},videoFilter:F,frameInput:function(e){const t=e.video;return"ffv1"===t.codec||/422|444|10le/.test(t.pixFmt)?"rgba":"jpeg"},jpegQuality:function(e){const t=e.video;return"crf"===t.rate?t.crf<=16?.98:t.crf<=20?.95:.9:.95},fpsRational:h,renderFilename:q,projectFilename:function(e,t={}){const a=k(e,t.width||1920,t.height||1080,t.exportFrame||null);return q(e.filename,e,{project:t.name,width:a.outW,height:a.outH,fps:e.fps||t.fps})},describe:C,estimateSize:H,checks:function(e,t={}){const a=[],o=b[e.preset],r=o&&o.limits||{},n=k(e,t.canvasW||1920,t.canvasH||1080,t.region),s=e.fps||t.fps||30;n.upscale&&a.push(`Upscaled: the part of the picture used is ${n.from.w}×${n.from.h} and the output ${n.outW}×${n.outH} — a larger${e.width&&e.height>e.width?" or vertical":""} project size keeps it sharp`),e.width&&"fill"===e.reframe&&n.crop&&t.canvasW&&(n.crop.w<(t.region?t.region.w:t.canvasW)-2||n.crop.h<(t.region?t.region.h:t.canvasH)-2)&&a.push(`Cropped to ${n.crop.w}×${n.crop.h} from the middle of the picture — set an export frame (⬒ Frame) to choose the part, or use Fit to keep everything`),r.vertical&&e.width&&e.width>e.height&&a.push(`${o.label} expects a vertical picture; this output is landscape`),r.duration&&t.duration>r.duration+.05&&a.push(`${o.label}: ${E(t.duration)} is longer than ${E(r.duration)} — check the platform's current limit`),r.fps&&s>r.fps+.01&&a.push(`${o.label} takes up to ${r.fps} fps; this output is ${s}`),r.fpsList&&!r.fpsList.some(e=>Math.abs(e-s)<.02)&&a.push(`Broadcast usually wants ${r.fpsList.join(" / ")} fps; this output is ${s}`);const i=H(e,{duration:t.duration,width:n.outW,height:n.outH,fps:s});return r.size&&i&&i>r.size&&a.push(`${o.label} takes files up to ${T(r.size)}; this one will be about ${T(i)}`),"mpeg2"!==e.video.codec||1920===n.outW&&1080===n.outH||a.push("XDCAM HD422 is 1920×1080 — other sizes may be refused by playout servers"),a},presetGroups:function(e={}){const t=[],a=(e,a,o)=>{let r=t.find(t=>t.group===e);r||t.push(r={group:e,items:[]}),r.items.push({id:a,label:o.label||a,note:o.note||""})};for(const e of v)"Custom"!==b[e].group&&a(b[e].group,e,b[e]);for(const[t,o]of Object.entries(e))a("Saved",t,o);return a("Custom","custom",b.custom),t},presetId:D,savedPresets:function(e){const t={};for(const{file:a,data:o}of e||[]){if(!o||"object"!=typeof o)continue;const e=D(o.id||String(a||"").replace(/\.json$/i,""));if(!e||b[e])continue;const{spec:r}=y({...o,preset:"custom"});t[e]={...r,preset:e,label:$(o.label||e,60),note:C(r)}}return t},parseEncoders:function(e){const t=new Set;for(const a of String(e||"").split(/\r?\n/)){const e=/^\s*[VAS][.A-Z]{5}\s+(\S+)/.exec(a);e&&"="!==e[1]&&t.add(e[1])}return t},PLATFORM_LOUDNESS:x,LOUDNESS_TARGETS:[[-14,"−14 LUFS · streaming and social"],[-16,"−16 LUFS · podcast, dialogue"],[-23,"−23 LUFS · broadcast (EBU R128)"],[-24,"−24 LUFS · broadcast (ATSC A/85)"]],LOUDNESS_ARGS:e=>["-hide_banner","-nostats","-i",e,"-map","0:a:0","-af","ebur128=peak=true:framelog=quiet","-f","null","-"],LOUDNORM_ARGS:(e,t)=>["-hide_banner","-nostats","-i",e,"-map","0:a:0","-af",`loudnorm=I=${t.audio.loudness}:TP=${t.audio.truePeak??-1}:LRA=11:print_format=json`,"-f","null","-"],parseLoudnorm:function(e){const t=/\{[^{}]*"input_i"[^{}]*\}/.exec(String(e||"").split(/Parsed_loudnorm/).pop()||"");if(!t)return null;try{const e=JSON.parse(t[0]),a={};for(const t of["input_i","input_tp","input_lra","input_thresh","target_offset"])a[t]=Number(e[t]);return Object.values(a).every(Number.isFinite)&&a.input_i>-70?a:null}catch{return null}},loudnormFilter:R,parseEbur128:function(e){const t=String(e||""),a=[...t.matchAll(/\bI:\s+(-?[\d.]+|-inf)\s+LUFS/g)].pop(),o=[...t.matchAll(/True peak:\s*[\r\n]+\s*Peak:\s+(-?[\d.]+|-inf)\s+dBFS/g)].pop(),r=e=>e&&"-inf"!==e[1]?+e[1]:null,n={lufs:r(a),truePeak:r(o)};return a&&("-inf"===a[1]||+a[1]<=-69.95)&&(n.lufs=null,n.silent=!0),n},verify:function(e,t,a={}){const o=[],r=(e,t)=>o.push({ok:!!e,text:t}),n=t&&t.streams||[],s=n.find(e=>"video"===e.codec_type),i=n.find(e=>"audio"===e.codec_type),l=a.frame||{},p=(e,t,a)=>Math.abs(e-t)<=a,d=e=>{const[t,a]=String(e||"0/1").split("/").map(Number);return a?t/a:0},h={h264:"h264",hevc:"hevc",av1:"av1",vp9:"vp9",prores:"prores",dnxhr:"dnxhd",mpeg2:"mpeg2video",ffv1:"ffv1"};if(s){l.outW&&r(s.width===l.outW&&s.height===l.outH,`${s.width}×${s.height}${s.width===l.outW&&s.height===l.outH?"":` (asked ${l.outW}×${l.outH})`}`);const t=d(s.avg_frame_rate)||d(s.r_frame_rate);a.fps&&r(p(t,a.fps,.01),`${Math.round(1e3*t)/1e3} fps${p(t,a.fps,.01)?"":` (asked ${a.fps})`}`);const o=+s.nb_frames;a.frames&&Number.isFinite(o)&&o>0&&r(o===a.frames,`${o} frames${o===a.frames?"":` (rendered ${a.frames})`}`),r(s.codec_name===h[e.video.codec],`${s.codec_name}${s.codec_name===h[e.video.codec]?"":` (asked ${e.video.codec})`}`);const n=u[e.color];n&&s.color_space&&"unknown"!==s.color_space&&r(s.color_space===n.matrix,`color ${s.color_space}${s.color_space===n.matrix?"":` (asked ${n.matrix})`}`)}else r(!1,"no video stream in the file");if("none"===e.audio.codec)r(!i,i?"has an audio stream (asked none)":"no audio");else if(i){const t=c[e.audio.codec];r(i.codec_name===t.encoder||"libopus"===t.encoder&&"opus"===i.codec_name||"libmp3lame"===t.encoder&&"mp3"===i.codec_name,`${i.codec_name} ${Math.round(+i.sample_rate/100)/10} kHz ${1===+i.channels?"mono":2===+i.channels?"stereo":i.channels+" ch"}`),+i.sample_rate!==e.audio.sampleRate&&r(!1,`sample rate ${i.sample_rate} (asked ${e.audio.sampleRate})`),+i.channels!==e.audio.channels&&r(!1,`${i.channels} channel${1===+i.channels?"":"s"} (asked ${e.audio.channels})`)}else r(!a.hasAudio,a.hasAudio?"no audio stream":"no audio (the timeline is silent)");const m=+(t&&t.format&&t.format.duration);a.duration&&Number.isFinite(m)&&r(p(m,a.duration,Math.max(.05,1.5/(a.fps||30))),`${A(m)}${p(m,a.duration,Math.max(.05,1.5/(a.fps||30)))?"":` (timeline ${A(a.duration)})`}`);const f=a.loud;if(!a.loudWanted||!i||f&&null!=f.lufs||f&&f.silent||r(!1,"loudness could not be measured"),f&&f.silent&&i&&r(null==e.audio.loudness,"silent"+(null!=e.audio.loudness?` (target ${e.audio.loudness} LUFS)`:"")),f&&null!=f.lufs){const t=e.audio.loudness,a=f.truePeak;if(null!=t)r(p(f.lufs,t,1),`${f.lufs.toFixed(1)} LUFS${p(f.lufs,t,1)?"":` (target ${t})`}`),null!=a&&r(a<=(e.audio.truePeak??-1)+.5,`${a.toFixed(1)} dBTP${a<=(e.audio.truePeak??-1)+.5?"":` (ceiling ${e.audio.truePeak??-1})`}`);else{const t=x[e.preset],o=null!=t&&f.lufs>t+1?` — ${b[e.preset].label} plays it ${(f.lufs-t).toFixed(1)} dB quieter (set Loudness to ${t} LUFS to match)`:"";r(!0,`${f.lufs.toFixed(1)} LUFS${null!=a?` · ${a.toFixed(1)} dBTP`:""}${o}`),null!=a&&a>0&&r(!1,`true peak ${a.toFixed(1)} dBTP — over 0, it will clip after encoding`)}}const v=b[e.preset]&&b[e.preset].limits||{};return a.size&&r(!(v.size&&a.size>v.size),`${T(a.size)}${v.size&&a.size>v.size?` (over ${b[e.preset].label}'s ${T(v.size)})`:""}`),{ok:!o.filter(e=>!e.ok).length,checks:o,line:o.map(e=>(e.ok?"":"⚠ ")+e.text).join(" · ")}},fmtBytes:T,fmtDur:E}}();t?module.exports=a:e.FableCutDelivery=a}("undefined"!=typeof self?self:this);
@@ -0,0 +1 @@
1
+ "use strict";const e=require("fs"),r=require("os"),t=require("path"),{spawn:i}=require("./cli/lib/processes"),o=t.join(__dirname,"encoding-profiles.json"),n="delivery",s={matrix:"bt709",primaries:"bt709",trc:"bt709",range:"tv"},a={label:"Delivery · H.264 balanced",description:"Built-in fallback — good quality and compatibility.",jpegQuality:.95,extension:".mp4",color:{...s},args:["-c:v","libx264","-preset","fast","-crf","18","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k","-movflags","+faststart","-shortest"]},l=new Set(["-colorspace","-color_primaries","-color_trc","-color_range"]);let c=null,u=-1;function f(e){const r=e&&"object"==typeof e?e:{},t=(e,r)=>{const t=String(e??"").trim().toLowerCase();return/^[a-z0-9._-]+$/.test(t)?t:r};let i=t(r.range,s.range);"full"===i&&(i="pc"),"limited"===i&&(i="tv"),"tv"!==i&&"pc"!==i&&(i=s.range);const o=t(r.matrix??r.colorspace,s.matrix);return{matrix:o,primaries:t(r.primaries,o),trc:t(r.trc??r.transfer,o),range:i}}function p(e,r){const t=r&&"object"==typeof r?r:{},i=Array.isArray(t.args)?t.args.map(String):String(t.args||"").split(/\s+/).filter(Boolean);let o=String(t.extension||a.extension).trim().toLowerCase();o&&!o.startsWith(".")&&(o=`.${o}`),/^\.[a-z0-9]+$/.test(o)||(o=a.extension);const n=Number(t.jpegQuality);return{id:e,label:String(t.label||e),description:String(t.description||t.desc||""),jpegQuality:n>=.1&&n<=1?n:a.jpegQuality,extension:o,color:f(t.color),args:i}}function g(r){const i=function(){try{return e.statSync(o).mtimeMs}catch{return 0}}();if(c&&!r&&i===u)return c;let s=null;const l=[];try{e.existsSync(o)?s=JSON.parse(e.readFileSync(o,"utf8").replace(/^\uFEFF/,"")):l.push(`${t.basename(o)} not found — using the built-in profile`)}catch(e){l.push(`${t.basename(o)} could not be parsed (${e.message}) — using the built-in profile`)}const f={};if(s?.profiles&&"object"==typeof s.profiles)for(const[e,r]of Object.entries(s.profiles))f[e]=p(e,r);for(const[e,r]of Object.entries(f))r.args.length||l.push(`profile "${e}" has no args — ffmpeg will pick its own defaults`);Object.keys(f).length||(f[n]=p(n,a));let g="string"==typeof s?.default?s.default:n;if(!f[g]){const e=Object.keys(f)[0];s?.default&&l.push(`default "${s.default}" is not a defined profile — using ${e}`),g=e}return c={default:g,profiles:f,issues:l},u=i,c}function m(e,r=120){const t=(e.args||[]).join(" ")||"(no args — ffmpeg defaults)";return t.length>r?t.slice(0,r-1)+"…":t}function d(e,r){const t=e.color||s,i=function(e,r){const t=e||s;return`scale=in_range=full:in_color_matrix=${"bt601"===r?"bt601":"bt709"}:out_range=${t.range}:out_color_matrix=${t.matrix},setparams=color_primaries=${t.primaries}:color_trc=${t.trc}`}(t,r),o=function(e){const r=[];for(let t=0;t<e.length;t++)l.has(e[t])?t++:r.push(e[t]);return r}((e.args||[]).slice()),n=o.indexOf("-vf");return n>=0&&null!=o[n+1]?o[n+1]=`${i},${o[n+1]}`:o.unshift("-vf",i),o.push(...function(e){const r=e||s;return["-colorspace",r.matrix,"-color_primaries",r.primaries,"-color_trc",r.trc,"-color_range",r.range]}(t)),o}module.exports={PROFILES_FILE:o,DEFAULT_COLOR:s,loadEncodeProfiles:g,invalidateEncodeProfiles:function(){c=null,u=-1},resolveProfile:function(e){const r=g(),t=e||r.default,i=r.profiles[t];if(!i)throw new Error(`Unknown encoding profile "${t}"`);return i},listProfilesPublic:function(e){const r=g(),i={default:r.default,profiles:{},file:t.basename(o),issues:r.issues||[]};for(const[t,o]of Object.entries(r.profiles)){const r={label:o.label,description:o.description,jpegQuality:o.jpegQuality,extension:o.extension,color:o.color,summary:m(o)};i.profiles[t]=e?{...r,args:o.args}:r}return i},profileSummary:m,buildExportArgs:function(e,{fps:r,wavPath:t,outPath:i,pixelFormat:o,width:n,height:s}={}){const a=["-y","-hide_banner"],l="rgba"===o;if(l){const e=Math.max(2,0|n),t=Math.max(2,0|s);a.push("-thread_queue_size","8","-f","rawvideo","-pix_fmt","rgba","-s",`${e}x${t}`,"-framerate",String(r),"-i","-")}else a.push("-thread_queue_size","64","-f","image2pipe","-framerate",String(r),"-c:v","mjpeg","-i","-");return t&&a.push("-i",t),a.push(...d(e,l?"bt709":"bt601"),i),a},dryRunProfile:function(o,{fps:n=30,hasAudio:s=!0}={}){const a=e.mkdtempSync(t.join(r.tmpdir(),"fablecut-dry-")),l=t.join(a,`probe${o.extension}`),c=["-y","-hide_banner","-f","lavfi","-i",`color=c=black:s=256x144:r=${n}:d=0.1`];return s&&c.push("-f","lavfi","-i","anullsrc=r=48000:cl=stereo"),c.push("-t","0.1",...d(o,"bt709"),l),new Promise(r=>{let t,o,n="",s=!1,l=!1;const u=t=>{if(!s){s=!0,clearTimeout(o);try{e.rmSync(a,{recursive:!0,force:!0})}catch{}r(t)}};try{t=i("ffmpeg",c,{stdio:["ignore","ignore","pipe"]})}catch(e){return void u({ok:!1,error:e.message||String(e)})}o=setTimeout(()=>{l=!0;try{t.kill("SIGKILL")}catch{}},15e3),t.on("error",e=>u({ok:!1,error:e.message||String(e)})),t.stderr.on("data",e=>{n=(n+e).slice(-4e3)}),t.on("close",e=>{if(l)return void u({ok:!1,error:"ffmpeg dry-run timed out"});if(0===e)return void u({ok:!0});const r=n.trim().split("\n").map(e=>e.trim()).filter(Boolean),t=r.filter(e=>/error|invalid|unknown|not found|unrecognized|failed|minimum|unsupported|no such/i.test(e)&&!/^Conversion failed/i.test(e)),i=t.length?t.slice(0,2).concat(r.slice(-1)):r.slice(-3);u({ok:!1,error:[...new Set(i)].join(" · ")||`ffmpeg exited ${e}`})})})}};
@@ -0,0 +1,284 @@
1
+ {
2
+ "default": "delivery",
3
+ "profiles": {
4
+ "draft": {
5
+ "label": "草稿 · H.264 快速",
6
+ "description": "快速预览:较小文件,较快编码。",
7
+ "jpegQuality": 0.85,
8
+ "extension": ".mp4",
9
+ "color": {
10
+ "matrix": "bt709",
11
+ "primaries": "bt709",
12
+ "trc": "bt709",
13
+ "range": "tv"
14
+ },
15
+ "args": [
16
+ "-c:v",
17
+ "libx264",
18
+ "-preset",
19
+ "veryfast",
20
+ "-crf",
21
+ "23",
22
+ "-pix_fmt",
23
+ "yuv420p",
24
+ "-c:a",
25
+ "aac",
26
+ "-b:a",
27
+ "128k",
28
+ "-movflags",
29
+ "+faststart",
30
+ "-shortest"
31
+ ]
32
+ },
33
+ "delivery": {
34
+ "label": "交付 · H.264 均衡",
35
+ "description": "默认软件编码,兼顾画质、大小与兼容性。",
36
+ "jpegQuality": 0.95,
37
+ "extension": ".mp4",
38
+ "color": {
39
+ "matrix": "bt709",
40
+ "primaries": "bt709",
41
+ "trc": "bt709",
42
+ "range": "tv"
43
+ },
44
+ "args": [
45
+ "-c:v",
46
+ "libx264",
47
+ "-preset",
48
+ "fast",
49
+ "-crf",
50
+ "18",
51
+ "-pix_fmt",
52
+ "yuv420p",
53
+ "-c:a",
54
+ "aac",
55
+ "-b:a",
56
+ "192k",
57
+ "-movflags",
58
+ "+faststart",
59
+ "-shortest"
60
+ ]
61
+ },
62
+ "hq": {
63
+ "label": "高质量 · H.264 4:2:2 10-bit",
64
+ "description": "用于中间母版及调色交换,编码较慢、文件较大;部分浏览器和 QuickTime 无法播放。",
65
+ "jpegQuality": 0.98,
66
+ "extension": ".mp4",
67
+ "color": {
68
+ "matrix": "bt709",
69
+ "primaries": "bt709",
70
+ "trc": "bt709",
71
+ "range": "tv"
72
+ },
73
+ "args": [
74
+ "-c:v",
75
+ "libx264",
76
+ "-preset",
77
+ "slow",
78
+ "-crf",
79
+ "16",
80
+ "-pix_fmt",
81
+ "yuv422p10le",
82
+ "-c:a",
83
+ "aac",
84
+ "-b:a",
85
+ "256k",
86
+ "-movflags",
87
+ "+faststart",
88
+ "-shortest"
89
+ ]
90
+ },
91
+ "broadcast1080i50": {
92
+ "label": "广播 · 1080i50 MOV",
93
+ "description": "工程使用 1920×1080、25 fps,滤镜生成广播交付所需的隔行输出。",
94
+ "jpegQuality": 0.98,
95
+ "extension": ".mov",
96
+ "color": {
97
+ "matrix": "bt709",
98
+ "primaries": "bt709",
99
+ "trc": "bt709",
100
+ "range": "tv"
101
+ },
102
+ "args": [
103
+ "-c:v",
104
+ "libx264",
105
+ "-preset",
106
+ "fast",
107
+ "-crf",
108
+ "18",
109
+ "-pix_fmt",
110
+ "yuv420p",
111
+ "-g",
112
+ "10",
113
+ "-maxrate",
114
+ "60M",
115
+ "-bufsize",
116
+ "70M",
117
+ "-x264opts",
118
+ "open_gop=0:interlaced=1:me=hex:subme=2:cabac=1:no-deblock:analyse=all:8x8dct=1:bframes=2:weightp=0:sliced-threads:level=41",
119
+ "-vf",
120
+ "scale=1920:1080:interl=-1,fps=50,tinterlace=interleave_top:bypass_il",
121
+ "-c:a",
122
+ "aac",
123
+ "-b:a",
124
+ "384k",
125
+ "-ar",
126
+ "48000",
127
+ "-strict",
128
+ "-2",
129
+ "-shortest"
130
+ ]
131
+ },
132
+ "prores422": {
133
+ "label": "ProRes 422 HQ · MOV",
134
+ "description": "用于中间母版和剪辑交换,文件较大。",
135
+ "jpegQuality": 1,
136
+ "extension": ".mov",
137
+ "color": {
138
+ "matrix": "bt709",
139
+ "primaries": "bt709",
140
+ "trc": "bt709",
141
+ "range": "tv"
142
+ },
143
+ "args": [
144
+ "-c:v",
145
+ "prores_ks",
146
+ "-profile:v",
147
+ "3",
148
+ "-pix_fmt",
149
+ "yuv422p10le",
150
+ "-c:a",
151
+ "pcm_s16le",
152
+ "-shortest"
153
+ ]
154
+ },
155
+ "nvenc-h264": {
156
+ "label": "NVIDIA · H.264",
157
+ "description": "使用 NVIDIA NVENC 编码,需可用显卡及包含 NVENC 的 ffmpeg。",
158
+ "jpegQuality": 0.95,
159
+ "extension": ".mp4",
160
+ "color": {
161
+ "matrix": "bt709",
162
+ "primaries": "bt709",
163
+ "trc": "bt709",
164
+ "range": "tv"
165
+ },
166
+ "args": [
167
+ "-c:v",
168
+ "h264_nvenc",
169
+ "-preset",
170
+ "p5",
171
+ "-tune",
172
+ "hq",
173
+ "-rc",
174
+ "vbr",
175
+ "-cq",
176
+ "19",
177
+ "-b:v",
178
+ "0",
179
+ "-pix_fmt",
180
+ "yuv420p",
181
+ "-c:a",
182
+ "aac",
183
+ "-b:a",
184
+ "192k",
185
+ "-movflags",
186
+ "+faststart",
187
+ "-shortest"
188
+ ]
189
+ },
190
+ "nvenc-hevc": {
191
+ "label": "NVIDIA · HEVC",
192
+ "description": "使用 NVIDIA NVENC 编码 HEVC,需可用显卡及包含 NVENC 的 ffmpeg。",
193
+ "jpegQuality": 0.95,
194
+ "extension": ".mp4",
195
+ "color": {
196
+ "matrix": "bt709",
197
+ "primaries": "bt709",
198
+ "trc": "bt709",
199
+ "range": "tv"
200
+ },
201
+ "args": [
202
+ "-c:v",
203
+ "hevc_nvenc",
204
+ "-preset",
205
+ "p5",
206
+ "-tune",
207
+ "hq",
208
+ "-rc",
209
+ "vbr",
210
+ "-cq",
211
+ "21",
212
+ "-b:v",
213
+ "0",
214
+ "-pix_fmt",
215
+ "yuv420p",
216
+ "-tag:v",
217
+ "hvc1",
218
+ "-c:a",
219
+ "aac",
220
+ "-b:a",
221
+ "192k",
222
+ "-movflags",
223
+ "+faststart",
224
+ "-shortest"
225
+ ]
226
+ },
227
+ "videotoolbox-h264": {
228
+ "label": "Apple Silicon · H.264",
229
+ "description": "使用 VideoToolbox 硬件编码,需 Apple Silicon、macOS 及包含 VideoToolbox 的 ffmpeg。",
230
+ "jpegQuality": 0.95,
231
+ "extension": ".mp4",
232
+ "color": {
233
+ "matrix": "bt709",
234
+ "primaries": "bt709",
235
+ "trc": "bt709",
236
+ "range": "tv"
237
+ },
238
+ "args": [
239
+ "-c:v",
240
+ "h264_videotoolbox",
241
+ "-q:v",
242
+ "65",
243
+ "-pix_fmt",
244
+ "yuv420p",
245
+ "-c:a",
246
+ "aac",
247
+ "-b:a",
248
+ "192k",
249
+ "-movflags",
250
+ "+faststart",
251
+ "-shortest"
252
+ ]
253
+ },
254
+ "videotoolbox-hevc": {
255
+ "label": "Apple Silicon · HEVC",
256
+ "description": "使用 VideoToolbox 硬件编码 HEVC,需 Apple Silicon、macOS 及包含 VideoToolbox 的 ffmpeg。",
257
+ "jpegQuality": 0.95,
258
+ "extension": ".mp4",
259
+ "color": {
260
+ "matrix": "bt709",
261
+ "primaries": "bt709",
262
+ "trc": "bt709",
263
+ "range": "tv"
264
+ },
265
+ "args": [
266
+ "-c:v",
267
+ "hevc_videotoolbox",
268
+ "-q:v",
269
+ "58",
270
+ "-pix_fmt",
271
+ "yuv420p",
272
+ "-tag:v",
273
+ "hvc1",
274
+ "-c:a",
275
+ "aac",
276
+ "-b:a",
277
+ "192k",
278
+ "-movflags",
279
+ "+faststart",
280
+ "-shortest"
281
+ ]
282
+ }
283
+ }
284
+ }
@@ -0,0 +1 @@
1
+ !function(t){"use strict";const e="object"==typeof module&&module.exports?require("./mp4-demux"):t.FableCutMp4;class s{static async open(t,i){if("function"!=typeof VideoDecoder||!e)return null;const r=async(e,s)=>{const r=new AbortController,o=()=>r.abort();i?.aborted&&r.abort(),i?.addEventListener("abort",o,{once:!0});const n=setTimeout(o,15e3);try{const i=await fetch(t,{signal:r.signal,headers:{Range:`bytes=${e}-${s-1}`}});if(206!==i.status){try{await(i.body?.cancel())}catch{}return null}const o=new Uint8Array(await i.arrayBuffer());if(o.length!==s-e)throw new Error("invalid range response length");return{bytes:o,headers:i.headers}}finally{clearTimeout(n),i?.removeEventListener("abort",o)}};try{const t=await r(0,1);if(!t)return null;const o=Number((t.headers.get("content-range")||"").split("/")[1]);if(!Number.isSafeInteger(o)||o<8)return null;const n=async(t,e)=>{if(!Number.isSafeInteger(t)||!Number.isSafeInteger(e)||t<0||e>o||e<=t||e-t>67108864)throw new Error("invalid MP4 byte range");const s=await r(t,e);if(!s)throw new Error("range request refused");return s.bytes},h=await e.findMoov(n,o),a=h&&e.parseMovie(h);if(!a?.samples.length||a.samples.some(t=>!Number.isSafeInteger(t.offset)||!Number.isSafeInteger(t.size)||t.size<1||t.offset<0||t.offset+t.size>o))return null;const c={codec:a.codec,codedWidth:a.width,codedHeight:a.height};a.description&&(c.description=a.description),a.displayWidth!==a.width&&Object.assign(c,{displayAspectWidth:a.displayWidth,displayAspectHeight:a.displayHeight});const u=await VideoDecoder.isConfigSupported(c).catch(()=>null);return u?.supported?new s(n,o,a,c,i):null}catch{return null}}constructor(t,s,i,r,o){Object.assign(this,{read:t,size:s,cfg:r,signal:o,samples:i.samples,order:e.presentationOrder(i.samples)}),this.dec=null,this.pos=0,this.out=[],this.cur=null,this.win=null,this.err=null,this.wake=null}restart(t){this.closed=!1,this.dropQueue(),this.dec&&"closed"!==this.dec.state&&this.dec.close(),this.err=null,this.dec=new VideoDecoder({output:t=>{this.closed?t.close():(this.out.push(t),this.poke())},error:t=>{this.err=t,this.poke()}}),this.dec.ondequeue=()=>this.poke(),this.dec.configure(this.cfg),this.pos=t}poke(){const t=this.wake;this.wake=null,t&&t()}dropQueue(){for(const t of this.out)t.close();this.out=[]}async bytes(t){const e=this.win;if(!e||t.offset<e.at||t.offset+t.size>e.at+e.data.length){const e=Math.min(this.size,t.offset+Math.max(t.size,4<<20));this.win={at:t.offset,data:await this.read(t.offset,e)}}const s=t.offset-this.win.at;return this.win.data.subarray(s,s+t.size)}async frameAt(t){const s=Date.now()+15e3,i=e.frameAt(this.samples,this.order,t),r=Math.round(1e6*this.samples[i].pts);if(this.cur&&this.cur.timestamp===r)return this.cur;const o=e.syncBefore(this.samples,i),n=this.out.length>0&&this.out[0].timestamp>r;(!this.dec||"closed"===this.dec.state||this.cur&&this.cur.timestamp>r||i<this.pos&&n||o>this.pos)&&this.restart(o);const h=this.samples.length;for(;;){if(this.signal?.aborted)throw new Error("cancelled");if(Date.now()>s)throw new Error("VideoDecoder frame timed out");for(;this.out.length&&this.out[0].timestamp<r;)this.out.shift().close();if(this.out.length)return this.cur&&this.cur.close(),this.cur=this.out.shift(),this.cur;if(this.err)throw this.err;let t=!1;for(;this.pos<h&&this.dec.decodeQueueSize<8&&this.out.length<3;){const e=this.samples[this.pos++];this.dec.decode(new EncodedVideoChunk({type:e.key?"key":"delta",timestamp:Math.round(1e6*e.pts),duration:Math.round(1e6*e.dur),data:await this.bytes(e)})),t=!0}if(!t&&this.pos>=h&&0===this.dec.decodeQueueSize){if(await this.dec.flush().catch(t=>{this.err=this.err||t}),this.pos=h+1,!this.out.length){if(this.err)throw this.err;return this.cur}}else await new Promise(t=>{this.wake=t,setTimeout(t,50)})}}close(){this.closed=!0,this.poke(),this.dropQueue(),this.cur&&(this.cur.close(),this.cur=null),this.dec&&"closed"!==this.dec.state&&this.dec.close(),this.dec=null,this.win=null}}"object"==typeof module&&module.exports?module.exports={ExportFrameSource:s}:t.FableCutExportDecode={ExportFrameSource:s}}("undefined"!=typeof self?self:this);
@@ -0,0 +1 @@
1
+ "use strict";const e=require("node:fs"),t=require("node:path"),r=require("node:os"),{spawn:o}=require("./cli/lib/processes"),{writeFrame:s,stopProcess:i}=require("./export-encoder"),a=require("./encode-profiles"),n=require("./delivery");async function h(e,t,r=6e4,s="ffmpeg"){if(t?.aborted)throw new Error("export cancelled");return new Promise((i,a)=>{const n=o(s,e,{stdio:["ignore","pipe","pipe"]});let h,l="",c="";const p=()=>{h||=new Error("export cancelled"),n.kill("SIGKILL")},f=setTimeout(()=>{h||=new Error(s+" timed out"),n.kill("SIGKILL")},r);t?.addEventListener("abort",p,{once:!0}),n.stdout.on("data",e=>{l+=e,l.length>8388608&&(h=new Error("probe output too large"),n.kill("SIGKILL"))}),n.stderr.on("data",e=>{c=(c+e).slice(-16e3)}),n.on("error",e=>{h=e}),n.once("close",e=>{clearTimeout(f),t?.removeEventListener("abort",p),h||0!==e?a(h||new Error(c.slice(-2e3)||s+" failed")):i({stdout:l,stderr:c})})})}let l;function c(){return l||=h(["-hide_banner","-encoders"]).then(e=>n.parseEncoders(e.stdout)).catch(e=>{throw l=null,e})}function p(r,o,s,i){e.mkdirSync(o,{recursive:!0});const a=(n=s,String(n||"export").replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").slice(0,120).trim()||"export").replace(new RegExp(i.replace(".","\\.")+"$","i"),"");var n;for(let s=0;;s++){const n=t.join(o,a+(s?"_"+s:"")+i);try{return e.copyFileSync(r,n,e.constants.COPYFILE_EXCL),n}catch(e){if("EEXIST"!==e.code)throw e}}}function f(e){let t=0,r=0;for(;t<e.length;){if(255!==e[t++]||216!==e[t++])throw new Error("export frame is not a valid JPEG");let o=!1;for(;t<e.length;){if(255!==e[t++])throw new Error("invalid JPEG marker");for(;255===e[t];)t++;const s=e[t++];if(void 0===s)throw new Error("truncated JPEG frame");if(217===s){o=!0,r++;break}if(1===s||s>=208&&s<=215)continue;if(0===s||216===s||t+2>e.length)throw new Error("invalid JPEG marker");const i=e.readUInt16BE(t);if(i<2||t+i>e.length)throw new Error("truncated JPEG frame");if(t+=i,218===s)for(;t<e.length;){if(255!==e[t]){t++;continue}let r=t+1;for(;255===e[r];)r++;if(!(0===e[r]||e[r]>=208&&e[r]<=215))break;t=r+1}}if(!o)throw new Error("truncated JPEG frame")}if(!r)throw new Error("empty JPEG batch");return r}class d{static async create(e,t){const r=new d(e,t);try{return await r.prepare(),r}catch(e){throw await r.close(),e}}constructor(o,s){this.options=o,this.context=s,this.controller=new AbortController,this.frames=0,this.frameBytes=0,this.dir=e.mkdtempSync(t.join(r.tmpdir(),"fablecut-delivery-")),this.outputs=[],this.tail=Promise.resolve(),this.started=!1,this.lastTouch=Date.now()}async prepare(){const e=this.options;if(!Number.isFinite(e.fps)||e.fps<=0||e.fps>240)throw new Error("export fps required (0 < fps <= 240)");if(![e.width,e.height].every(e=>Number.isSafeInteger(e)&&e>=2&&e<=16384))throw new Error("export width/height required");if(void 0!==e.totalFrames&&(!Number.isSafeInteger(e.totalFrames)||e.totalFrames<1))throw new Error("invalid totalFrames");if(this.mode=e.mode||"jpeg",!["jpeg","annexb"].includes(this.mode))throw new Error("unknown export mode");if("annexb"===this.mode&&(e.delivery||e.deliveries||e.profile))throw new Error("WebCodecs encoding does not accept delivery/profile settings");const r=e.deliveries||(e.delivery?[{delivery:e.delivery}]:[null]);if(!Array.isArray(r)||!r.length||r.length>8)throw new Error("one render supports 1 to 8 outputs");const o=r.some(Boolean)||"auto"===e.profile?await c():null;for(const[s,i]of r.entries()){const r={name:i?.name||e.name,path:t.join(this.dir,"output-"+s),stderr:""};if(i){const{spec:t,notes:s}=n.normalize(i.delivery??i,{available:o});if(t.fps&&Math.abs(t.fps-e.fps)>.001)throw new Error("outputs in one render must share fps");r.spec=t,r.notes=s,r.frame=n.reframe(t,e.width,e.height),r.extension=n.CONTAINERS[t.container].ext}else if("annexb"===this.mode)r.extension=".mp4";else{let t=e.profile;"auto"===t&&(t=o.has("h264_videotoolbox")?"videotoolbox-h264":o.has("h264_nvenc")?"nvenc-h264":"delivery"),r.profile=a.resolveProfile(t);const s=await a.dryRunProfile(r.profile);if(s.ok||"auto"!==e.profile){if(!s.ok)throw new Error(s.error)}else r.profile=a.resolveProfile("delivery");r.extension=r.profile.extension}r.path+=r.extension,this.outputs.push(r)}this.input=this.outputs.some(e=>e.spec?"rgba"===n.frameInput(e.spec):/422|444|10le/.test(e.profile?.args.join(" ")||""))?"rgba":"jpeg",this.jpegQuality=Math.max(...this.outputs.map(e=>e.spec?n.jpegQuality(e.spec):e.profile?.jpegQuality||.95));for(const r of this.outputs.filter(e=>e.spec))await h(n.ffmpegArgs(r.spec,{fps:e.fps,input:this.input,canvasW:e.width,canvasH:e.height,frame:r.frame,hasAudio:e.hasAudio,dry:!0,outPath:t.join(this.dir,"probe"+r.extension)}),this.controller.signal)}async audio(r){if(this.lastTouch=Date.now(),this.started)throw new Error("audio must arrive before video frames");if(this.controller.signal.aborted)throw new Error("export cancelled");this.wav=t.join(this.dir,"audio.wav"),await e.promises.writeFile(this.wav,r);for(const e of this.outputs)if(null!=e.spec?.audio.loudness){const t=await h(n.LOUDNORM_ARGS(this.wav,e.spec),this.controller.signal,6e5);if(e.loudnorm=n.parseLoudnorm(t.stderr),!e.loudnorm)throw new Error("could not measure audio loudness")}}start(){this.started=!0;const e=this.options,t=e.totalFrames/e.fps;for(const r of this.outputs){let s;if("annexb"===this.mode)s=["-y","-hide_banner","-f","h264","-r",String(e.fps),"-i","-",...this.wav?["-i",this.wav]:[],"-c:v","copy",...this.wav?["-c:a","aac","-b:a","192k"]:["-an"],"-bsf:v","h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1:video_full_range_flag=0","-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv","-movflags","+faststart",r.path];else if(r.spec)s=n.ffmpegArgs(r.spec,{fps:e.fps,input:this.input,canvasW:e.width,canvasH:e.height,frame:r.frame,duration:t,wavPath:this.wav,outPath:r.path,loudnorm:r.loudnorm});else{const t={...r.profile,args:r.profile.args.filter(e=>"-shortest"!==e)};s=a.buildExportArgs(t,{fps:e.fps,wavPath:this.wav,outPath:r.path,pixelFormat:this.input,width:e.width,height:e.height})}"optimized"===e.engine&&"jpeg"===this.mode&&s.splice(0,0,"-filter_threads","1");const i=r.proc=o("ffmpeg",s,{stdio:["pipe","ignore","pipe"]});r.controller=this.controller,r.err=()=>r.stderr,i.stderr.on("data",e=>{r.stderr=(r.stderr+e).slice(-4e3)}),i.stdin.on("error",()=>{}),i.on("error",e=>{r.failed||=e.message}),r.done=new Promise(e=>i.once("close",t=>{0!==t&&(r.failed||=r.stderr||"encoder failed"),e(t)}))}}write(e,t){this.lastTouch=Date.now();const r=this.tail.then(async()=>{if(this.controller.signal.aborted)throw new Error("export cancelled");if(this.ending)throw new Error("export session is ending");if("jpeg"===this.mode){const r="rgba"===this.input?e.length/(this.options.width*this.options.height*4):f(e);if(!Number.isSafeInteger(r)||r<1||r>64)throw new Error("invalid frame batch");if(void 0!==t&&t!==r)throw new Error("frame batch count mismatch");t=r}else if(!Number.isSafeInteger(t)||t<1||t>64)throw new Error("Annex-B upload requires frame count");if(this.options.totalFrames&&this.frames+t>this.options.totalFrames)throw new Error("too many video frames");if(this.started||this.start(),await Promise.all(this.outputs.filter(e=>!e.failed).map(t=>s(t,e).catch(e=>{t.failed=e.message}))),this.outputs.every(e=>e.failed))throw new Error(this.outputs.map(e=>e.failed).join("; "));this.frames+=t,this.frameBytes+=e.length});return this.tail=r.catch(()=>{}),r}async verify(t,r){try{const o=JSON.parse((await h(["-v","error","-show_streams","-show_format","-of","json",r],this.controller.signal,12e4,"ffprobe")).stdout),s=o.streams.find(e=>"video"===e.codec_type),i=o.streams.find(e=>"audio"===e.codec_type);if(s&&!Number(s.nb_frames)){const e=JSON.parse((await h(["-v","error","-select_streams","v:0","-count_frames","-show_entries","stream=nb_read_frames","-of","json",r],this.controller.signal,12e4,"ffprobe")).stdout);s.nb_frames=e.streams?.[0]?.nb_read_frames}if(t.spec){const s=!!this.wav&&"none"!==t.spec.audio.codec;let i=null;if(s)try{i=n.parseEbur128((await h(n.LOUDNESS_ARGS(r),this.controller.signal,12e4)).stderr)}catch{}return n.verify(t.spec,o,{frame:t.frame,fps:this.options.fps,frames:this.frames,duration:this.frames/this.options.fps,hasAudio:!!this.wav,loudWanted:s,loud:i,size:e.statSync(r).size})}const l=[],c=(e,t)=>l.push({ok:!!e,text:t});c(s,"视频流"),c(Number(s?.nb_frames)===this.frames,`帧数 ${s?.nb_frames}/${this.frames}`);const[p,f=1]=String(s?.avg_frame_rate).split("/").map(Number);c(Math.abs(p/f-this.options.fps)<.01,"帧率"),c(!this.wav||i,"音轨");const d=t.profile?.color||a.DEFAULT_COLOR;c(s?.color_space===d.matrix&&s.color_primaries===d.primaries&&s.color_transfer===d.trc,d.matrix+" 色彩标记");const u=Number(o.format?.duration);c(Number.isFinite(u)&&Math.abs(u-this.frames/this.options.fps)<=Math.max(.05,1.5/this.options.fps),"成片时长");const m=t.profile?.args[t.profile.args.indexOf("-c:v")+1],w="annexb"===this.mode||/^(libx264|h264_)/.test(m||"")?"h264":/^(libx265|hevc_)/.test(m||"")?"hevc":"prores_ks"===m?"prores":null;return w&&c(s?.codec_name===w,"视频编码 "+s?.codec_name),{ok:l.every(e=>e.ok),checks:l,line:l.map(e=>(e.ok?"":"⚠ ")+e.text).join(" · ")}}catch(e){return(e=>({ok:!1,checks:[{ok:!1,text:e.message}],line:e.message}))(e)}}async end(){if(this.lastTouch=Date.now(),await this.tail,!this.frames)throw new Error("export received no video frames");if(this.options.totalFrames&&this.frames!==this.options.totalFrames)throw new Error("export frame count mismatch");this.ending=!0;for(const e of this.outputs)e.proc.stdin.end();await Promise.all(this.outputs.map(e=>e.done));const e=[];for(const r of this.outputs){if(this.controller.signal.aborted)throw new Error("export cancelled");if(r.failed)e.push({error:r.failed,name:r.name});else try{const o=await this.verify(r,r.path);if(this.controller.signal.aborted)throw new Error("export cancelled");r.published=p(r.path,this.context.exportsDir,r.name,r.extension),e.push({src:this.context.url(t.basename(r.published)),report:o,name:t.basename(r.published),notes:r.notes})}catch(t){e.push({error:t.message,name:r.name})}}if(!e.some(e=>e.src))throw new Error(e.map(e=>e.error).join("; "));return this.committed=!0,{src:e.find(e=>e.src).src,outputs:e,report:e[0].report}}close(){return this.closing||(this.controller.abort(),this.closing=(async()=>{if(await Promise.allSettled(this.outputs.filter(e=>e.proc).map(e=>i(e.proc,e.done))),!this.committed)for(const t of this.outputs)t.published&&await e.promises.rm(t.published,{force:!0});await e.promises.rm(this.dir,{recursive:!0,force:!0})})()),this.closing}}module.exports={DeliverySession:d,capture:h,encoders:c,jpegCount:f};
@@ -0,0 +1 @@
1
+ "use strict";let exportDecodeState=null,exportDecodedFrames=new Map;function exportDecodeBegin(e,t="auto"){exportDecodeEnd(),"video"!==t&&"cache"!==t&&(exportDecodeState={signal:e,sources:new Map})}function exportDecodeEnd(){if(exportDecodeState)for(const e of exportDecodeState.sources.values())e?.close();exportDecodeState=null,exportDecodedFrames.clear()}async function decodeExportVideo(e,t){const r=exportDecodeState;if(!r||evalProps(e,t).bgRemove)return!1;for(const[e,o]of r.sources){const a=project.clips.find(t=>t.id===e);(!a||a.start+a.duration<=t)&&(o?.close(),r.sources.delete(e),exportDecodedFrames.delete(e))}if(!r.sources.has(e.id)){const t=getMedia(e.mediaId)?.src,o=t?await FableCutExportDecode.ExportFrameSource.open(t,r.signal):null;if(r.signal.aborted)throw o?.close(),new Error("cancelled");r.sources.set(e.id,o)}const o=r.sources.get(e.id);if(!o)return!1;try{const r=await o.frameAt(mediaTimeAt(e,t));if(!r)throw new Error("empty decoded frame");return exportDecodedFrames.set(e.id,r),!0}catch(t){if(o.close(),r.sources.set(e.id,null),exportDecodedFrames.delete(e.id),r.signal.aborted)throw t;return console.warn("视频解码回退到浏览器寻址:",t.message),!1}}function exportWindow(e={}){const t=e.fps||project.fps;let r=0,o=projDur();if("in-out"===e.range){const e=playRange();r=Math.min(o,Math.max(0,e.start)),o=Math.min(o,Math.max(r,e.end))}const a=Math.max("in-out"===e.range?0:1,Math.round((o-r)*t));if(!a)throw new Error("export range is empty");return{fps:t,start:r,frames:a,dur:a/t}}async function renderExportAudio(e,t,r){const o=await renderAudioMix(e+t,r);if(!o||!e)return o;const a=await o.arrayBuffer(),s=new DataView(a),n=s.getUint32(24,!0),i=s.getUint16(32,!0),c=Math.round(e*n)*i,p=Math.min(Math.round(t*n)*i,a.byteLength-44-c),d=new Uint8Array(44+Math.max(0,p));d.set(new Uint8Array(a,0,44)),d.set(new Uint8Array(a,44+c,Math.max(0,p)),44);const u=new DataView(d.buffer);return u.setUint32(4,d.length-8,!0),u.setUint32(40,Math.max(0,p),!0),new Blob([d],{type:"audio/wav"})}function exportBeginOptions(e,t,r,o="fast"){return{...t,totalFrames:t.frames,width:project.width,height:project.height,mode:"webcodecs"===o?"annexb":"jpeg",engine:o,name:e.name||project.name||"export",requestId:e.requestId,group:e.group||0,profile:e.profile,deliveries:e.deliveries?.length?e.deliveries:void 0,hasAudio:!!r}}class BatchFrameUploader{constructor(e){this.parts=[],this.bytes=0,this.frames=0,this.queue=new OrderedFrameQueue(t=>e(t.body,t.frames))}async push(e,t=1){this.parts.length&&this.bytes+e.size>33554432&&await this.flush(),this.parts.push(e),this.bytes+=e.size,this.frames+=t,(this.parts.length>=8||this.bytes>=524288)&&await this.flush()}async flush(){if(!this.parts.length)return;const e={body:new Blob(this.parts),size:this.bytes,frames:this.frames};this.parts=[],this.bytes=0,this.frames=0,await this.queue.push(e)}async finish(){await this.flush(),await this.queue.finish()}cancel(){this.parts=[],this.bytes=0,this.frames=0,this.queue.cancel()}get peakBytes(){return this.queue.peakBytes}}function createExportPipeline(e,t,r,o=()=>{}){const a=new BatchFrameUploader(r),s=createSnapshotEncoder(t,e.jpegQuality??.95),n=Math.max(1,Math.min(2,Math.floor(67108864/(project.width*project.height*4)))),i=new SnapshotPipeline(async t=>{const r=performance.now();if("rgba"===e.input){const e=t.getContext("2d").getImageData(0,0,t.width,t.height).data;return o("snapshot",performance.now()-r),{result:Promise.resolve(new Blob([e]))}}const a=await s.capture(t);return o("snapshot",performance.now()-r),a.result=a.result.then(e=>(o("jpeg",performance.now()-r),e)),a},async e=>{if(!e?.size)throw new Error("Canvas returned an empty frame");const t=performance.now();await a.push(e),o("queueWait",performance.now()-t)},n);return{queue:a,pipeline:i,encoder:s,depth:n}}function waitExportTask(e,t,r,o){return new Promise((a,s)=>{let n=!1;const i=(e,r)=>{n||(n=!0,clearTimeout(p),t.removeEventListener("abort",c),e?s(e):a(r))},c=()=>i(new Error("cancelled")),p=setTimeout(()=>i(new Error(o+" timed out")),r);t.addEventListener("abort",c,{once:!0}),t.aborted&&c(),Promise.resolve(e).then(e=>i(null,e),e=>i(e))})}async function runExportPlan(e,t,r){try{if(!new URLSearchParams(location.search).has("exportJob"))return("webcodecs"===r?webCodecsExport:"optimized"===r?optimizedExport:fastExport)({requestId:e,name:t});const o=await fetch(projectApi("/api/export/options"));if(!o.ok)throw new Error("could not load export settings");const a=await o.json(),s=a.groups||[{}];for(const[o,n]of s.entries()){const s={...a,...n,requestId:e,name:t,group:o};"webcodecs"===r?await webCodecsExport(s):await("optimized"===r?optimizedExport:fastExport)(s);const i=await fetch(projectApi("/api/export/status")+"&id="+e).then(e=>e.json());if(["error","cancelled"].includes(i.state))break}}catch(t){await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e,error:t.message}),signal:AbortSignal.timeout(15e3)}).catch(()=>{})}}
@@ -1 +1 @@
1
- "use strict";!function(e){const t={"Thumbnails {loaded}/{total}":"缩略图 {loaded}/{total}","{failed} thumbnails unavailable":"{failed} 张缩略图无法加载","Project not found":"项目不存在","Project could not be loaded":"项目加载失败","Invalid project data":"工程数据格式无效","Invalid project link. Open the project again from its original entry.":"项目链接无效,请从原入口重新打开项目。","Project request failed ({status})":"工程读取请求失败({status})","Project ID: {id}":"项目 ID:{id}","This project is not in the current or legacy storage folder. It may have been moved or deleted. Restore the project folder, then retry.":"当前及旧存储目录中均未找到此工程,可能已被移动或删除。请恢复工程文件夹后重试。","The project file could not be read or the editor service is unavailable. Check the file or service, then retry.":"工程文件无法读取,或编辑器服务暂时不可用。请检查文件或服务后重试。","Error details":"错误详情","Retry loading":"重新加载","Media missing":"媒体丢失","Media unreadable":"媒体无法读取","Check again":"重新检测","The source file was deleted or moved. Restore it to its original location, then retry.":"源文件已被删除或移动,请恢复到原位置后重新检测。","The source file could not be loaded. Check the file or connection, then retry.":"源文件无法加载,请检查文件或网络连接后重新检测。","Snapping on (N)":"吸附已开启(N)","Snapping off (N)":"吸附已关闭(N)","Zoom out (-)":"缩小时间轴(-)","Zoom in (+)":"放大时间轴(+)","Timeline zoom":"时间轴缩放","Insert before {clip}":"插到「{clip}」之前","Insert after {clip}":"插到「{clip}」之后","Move to {track} · {seconds}s":"移到{track} · {seconds}秒","Moves together":"一起移动","Moves together: {tracks}":"同时移动:{tracks}","Adjusted to an available position":"位置受限,已预览可用落点","Drop on a matching track":"请拖入同类型轨道或轨道间隙","Trimmed {seconds}s":"裁掉 {seconds} 秒","Restored {seconds}s":"恢复 {seconds} 秒","Duration {seconds}s":"剩余时长 {seconds} 秒","Following clips move together":"后续片段随裁剪联动","Linked media stays in sync":"关联音视频保持同步","Source or clip limit reached":"已到裁剪边界(素材范围/最短时长/重叠限制)","Clip actions":"片段操作","Timeline actions":"时间轴操作","Selected {count} clips · linked media follows":"已选 {count} 个片段 · 编辑包含关联音视频","Shared gap on enabled tracks":"闭合各启用轨道的公共空隙","No shared gap: {track} contains a clip":"没有公共空隙:{track}此处有片段","Move the playhead inside the clip first":"请先将播放头移到片段内部","Go to clip start":"跳到此片段片头","Zoom to selected clips":"放大所选片段","Split at playhead":"在播放头处分割","Trim start to playhead":"裁片头到播放头(当前片段)","Trim end to playhead":"裁片尾到播放头(当前片段)","Delete selected clips":"删除所选片段","Close shared gap":"闭合公共空隙","Timeline changed. Open the menu again.":"时间轴已更新,请重新打开菜单。","+ New track":"+ 新建轨道","Place here":"放置到此处","Drop media here to create a track":"拖入素材以创建轨道","+ Adjust":"+ 调整层","+ Import":"+ 导入","+ Text":"+ 文字","+click bin":"+单击素材区","+click clip":"+单击片段","+wheel":"+滚轮","AI bg remove":"AI 去背景","Add / remove from selection":"加入/移出选区","Add / remove marker at playhead":"在播放头处添加/移除标记","Add text at the playhead":"在播放头处添加文字","Add an adjustment layer (grades everything below it)":"添加调整图层(影响下方所有图层)","Add at playhead":"添加到播放头位置","Add transition at playhead (in / out by half)":"在播放头处添加转场(按前后半段选择入场/出场)","Adjustment layer":"调整图层",Align:"水平对齐",Animation:"动画",Assets:"素材","Assets panel":"素材面板","Inspector panel":"属性面板","Connection status":"连接状态","Settings button":"设置入口","Project switcher":"项目切换","Layout controls":"布局切换","Monitor toolbar":"预览工具栏","Audio meter":"音量表","Go to start / end":"跳至开头/末尾","Fit timeline":"适应时间轴","Clip {number}":"片段{number}","Visible modules":"显示模块","Hidden modules keep their functions and shortcuts.":"隐藏模块仍保留原有功能和快捷键。","Audio / Time":"音频/时间","Audio Hold — while paused, loop one frame of audio at the playhead. Play/Stop turns it off.":"音频保持:暂停时循环播放播放头处一帧音频。播放/停止会关闭此功能。","Audio hold":"音频保持","Audio {number}":"音频{number}","Video {number}":"视频{number}",Undo:"撤销",Redo:"重做","Undo (Ctrl/Cmd+Z)":"撤销(Ctrl/Cmd+Z)","Redo (Ctrl/Cmd+Shift+Z)":"重做(Ctrl/Cmd+Shift+Z)","Back 1 frame (←)":"后退一帧(←)","Background removal unavailable — couldn't load MediaPipe (offline?). Using chroma key still works.":"去背景不可用:无法加载 MediaPipe,请检查网络。仍可使用色度抠像。","Bg color":"背景色",Blend:"混合模式",Bold:"粗体","Box W/H":"文本框宽/高","Buffering…":"缓冲中…","Cached video frames · {count} compatibility fallbacks · You can switch tabs.":"使用缓存帧 · {count} 个片段使用兼容模式 · 可切换标签页。",Cancel:"取消","Canvas aspect preset":"画布宽高比预设",Channel:"声道","Channel {number}":"声道 {number}","Choose…":"选择…","Clear IN / OUT marker":"清除入点/出点标记","Clear keyframes":"清除关键帧","Click to switch RMS / LUFS / Peak":"单击切换 RMS/LUFS/峰值","Click: keyframe graph · Ctrl-click: reset":"单击:关键帧曲线 · Ctrl+单击:重置","Clip — {kind}":"片段 — {kind}",Close:"关闭","Close gap":"闭合空隙","Close gap at playhead (enabled tracks)":"闭合播放头处的空隙(已启用轨道)","Close gap at playhead (⇧G)":"闭合播放头处的空隙(⇧G)","Close graph":"关闭曲线","Closed {seconds}s gap":"已闭合 {seconds} 秒空隙",Collapse:"折叠",Color:"颜色","Compact tracks (no thumbnails)":"紧凑轨道(无缩略图)",Content:"内容","Couldn't create project: ":"无法创建项目:","Couldn't import — unsupported or unreadable file type":"无法导入:文件类型不受支持或无法读取","Couldn't play ":"无法播放:","Create a project":"创建项目","Crop L/R %":"左右裁剪 %","Crop T/B %":"上下裁剪 %","Ctrl-click to reset":"Ctrl+单击重置","Custom · {fps} fps":"自定义 · {fps} fps","Custom · {width}×{height}":"自定义 · {width}×{height}","Cycle RMS → LUFS → Peak":"循环切换 RMS → LUFS → 峰值",Delete:"删除","Delete folder (keeps media)":"删除文件夹(保留素材)","Delete selected (Del)":"删除所选片段(Del)","Delete selected clip(s), or clear focused transition":"删除所选片段,或清除当前转场",Direction:"文字方向","Disable chroma key":"关闭色度抠像","Disable gradient":"关闭渐变","Disable track":"禁用轨道","Drag empty area":"拖动空白区域","Drag to adjust duration":"拖动调整时长","Drag to resize · double-click to reset":"拖动调整大小 · 双击重置","Drop files into":"将文件放入","Drop here to move to root":"拖到此处移至根目录","Drop video, audio or images here":"将视频、音频或图片拖到此处",Elements:"装饰素材","Enable track":"启用轨道","Encoding…":"编码中…",Expand:"展开",Export:"导出","Export preview":"成片预览","Export progress":"导出进度","Play exported video":"播放成片","Video duration {seconds}s":"成片时长 {seconds} 秒","Preparing video…":"正在准备视频…","Rendering video…":"正在合成视频…","Rendering clip {current}/{total}":"正在合成第 {current}/{total} 个片段"," · About {seconds}s remaining":" · 预计剩余 {seconds} 秒","Finalizing video…":"正在封装视频,即将完成…","Video complete. Your file is ready.":"合成完成,文件已就绪",Save:"保存","Saving…":"保存中…","Choose a folder to save your video.":"请选择成片保存文件夹","Saved: {name}":"已保存:{name}","Save cancelled. Click Save to choose a folder again.":"已取消保存,可点击「保存」重新选择文件夹","Unable to save. Click Save to try again.":"保存失败,请点击「保存」重试","Click Save to choose a location in your browser.":"请点击「保存」,通过浏览器选择保存位置","Check your browser downloads to confirm the file was saved.":"已请求浏览器下载,请在下载列表中确认保存结果","Retry export":"重试导出","Cancel this export?":"确定取消导出?","The video is still exporting. Cancelling will stop this task.":"视频仍在导出中,取消后将停止本次任务。","Continue export":"继续导出","Confirm cancellation":"确定取消","Quick export":"快捷合成","Quick export in progress. You can switch chats.":"正在后台快捷合成,可以切换聊天","Background recording failed. You can record in this page while it stays open.":"后台录制失败,可在当前页面录制。录制期间请保持页面在前台。","Record in this page":"在当前页面录制","Recording failed. Keep this page open to try again.":"当前页面录制失败,可重新录制。录制期间请保持页面在前台。","Export interrupted. Try quick export to export again.":"合成任务中断,可尝试快捷合成重新导出","Quick export in progress. Keep this page open and in the foreground.":"快捷合成中,请保持当前页面,不要关闭窗口或切换页面","The exported file is unavailable. Please retry.":"未获取到成片文件,请重试。","Unable to check export progress. Please retry.":"暂时无法查询导出进度,请重试。","Unable to preview. Open the downloaded video to watch it.":"暂时无法预览,请打开已下载的视频观看。","Export failed: ":"导出失败:","Exporting…":"导出中…","Exporting… {percent}%":"导出中… {percent}%","Tik — Video Editor":"Tik — 视频编辑器",Family:"字体","CLI Fast (ffmpeg)":"CLI 快速(ffmpeg)","CLI Optimized (ffmpeg + frame cache)":"CLI 优化(ffmpeg + 帧缓存)","Fast (ffmpeg)":"快速(ffmpeg)","Filter / Color":"滤镜/调色","Find gap":"查找空隙","Find gap (G)":"查找空隙(G)","Find next shared gap (wraps; respects IN/OUT)":"查找下一处共同空隙(循环查找,遵循入点/出点)",Fit:"适应","Flip H":"水平翻转","Flip V":"垂直翻转",Font:"字体","Font size":"字号","Forward 1 frame (→)":"前进一帧(→)","Queued for export…":"正在排队,等待导出…","Starting background export…":"正在启动后台导出…","CLI renders in a separate process. Closing this page does not stop export.":"CLI 在独立进程中渲染,关闭此页面后仍继续导出。","CLI pre-decodes and caches frames in a separate process. You can close this page.":"CLI 在独立进程中预解码并缓存视频帧,关闭此页面后仍继续导出。","CLI renders frames in a separate process with ffmpeg. You can close this page.":"CLI 在独立进程中逐帧渲染并交由 ffmpeg 编码,关闭此页面后仍继续导出。","Frame-accurate ffmpeg encode. Keeps rendering if you switch tabs.":"使用 ffmpeg 逐帧编码,切换标签页后仍继续渲染。","Glow color":"发光颜色","Glow uses the text color":"发光使用文字颜色","Go to end (End)":"跳到结尾(End)","Go to previous / next keyframe":"跳到上一/下一关键帧","Go to start (Home)":"跳到开头(Home)","Google font":"Google 字体","Google fonts":"Google 字体","Gradient bottom color":"渐变底部颜色","Height in px (0 = no box — hug content)":"高度(像素);0 表示按内容自适应","IN and OUT must be at different times":"入点与出点不能相同","IN must be before OUT":"入点必须早于出点","Import media files":"导入素材文件","Imported {added}, skipped {skipped}":"已导入 {added} 个,跳过 {skipped} 个","Imported, but cannot replace {kind} with {replacement}":"已导入,但无法用{replacement}替换{kind}片段",In:"入场",Inspector:"属性","Interface language":"界面语言",Italic:"斜体","Jump to start / end":"跳到开头/结尾","Key color":"抠像颜色","Keyboard shortcuts":"键盘快捷键","Keying / Cut-out":"抠像/去背景",L:"L",Language:"语言","Large tracks":"大轨道",Layout:"布局",Left:"左声道","Length (s)":"时长(秒)","Library fonts":"本地字体库",Limit:"限区播放","Limit (toolbar)":"限区播放(工具栏)","Limit playback to IN/OUT (Home/End jump to markers; playhead outside = override)":"限制在入点/出点之间播放(Home/End 跳到标记;播放头在范围外时不限制)","Link timeline and Project bin selection":"联动时间线与项目素材选择","Linked {count} audio channels from {name}":"已关联 {name} 的 {count} 个声道",Load:"加载",'Loading Google font "{name}"…':"正在加载 Google 字体“{name}”…",M:"M","Marquee-select clips":"框选片段","Max size":"最大字号","Media replaced":"已替换素材","Media replaced — trimmed to fit shorter source":"已替换素材,并按较短素材裁剪","Media used by this project — right-click for New folder":"本项目使用的素材,右键可新建文件夹","MediaRecorder is not supported in this browser.":"当前浏览器不支持 MediaRecorder。","Medium tracks":"中等轨道","Missing media":"素材缺失","Mixing audio…":"混音中…","Mode: {mode} — click to switch":"模式:{mode} — 单击切换","Motion FX":"运动特效","Move playhead over the selected clip":"请将播放头移到所选片段上",Name:"名称","Needs the server + ffmpeg and ffprobe on PATH.":"需要运行本地服务,且 PATH 中包含 ffmpeg 和 ffprobe。","Needs the server + ffmpeg on PATH.":"需要运行本地服务,且 PATH 中包含 ffmpeg。","New folder":"新建文件夹","New subfolder":"新建子文件夹","No assets yet.":"暂无素材。","No clips on the timeline use this media":"时间线上没有使用此素材的片段","No enabled tracks":"没有已启用的轨道","No gap on {track} — disable the track or move the playhead":"轨道 {track} 没有空隙,请禁用该轨道或移动播放头","No gaps found":"未找到空隙","No gaps in IN/OUT range":"入点/出点范围内没有空隙","No keyframes":"没有关键帧","No next keyframe":"没有下一关键帧","No other {kind} in bin":"素材区没有其他{kind}素材","No previous keyframe":"没有上一关键帧","Nothing to close at playhead":"播放头处没有可闭合的空隙","Nothing to split at IN/OUT":"入点/出点处没有可分割的片段","Nothing to trim":"没有可裁剪的片段","OUT must be after IN":"出点必须晚于入点","Open project":"打开项目","Optimized (ffmpeg + frame cache)":"优化(ffmpeg + 帧缓存)","Optimized export complete: {seconds}s · {hits} cache hits":"优化导出完成:{seconds} 秒 · 命中缓存 {hits} 次","Optimized export failed: ":"优化导出失败:",Out:"出场","Overlapping audio":"重叠音频","Overlay art from library/elements":"来自 library/elements 的叠加素材","Pick a style — hover to preview it live":"选择样式,悬停可实时预览","Play / Pause (Space)":"播放/暂停(空格)","Play / pause":"播放/暂停","Play stops at OUT; Home/End use IN/OUT":"播放到出点停止;Home/End 跳到入点/出点","Plays the timeline once and records it. Keep the tab focused.":"播放并录制一遍时间线,请保持当前标签页处于前台。","Position X":"位置 X","Position Y":"位置 Y","Pre-decodes video frames and caches them. First export needs preparation; you can switch tabs.":"预解码并缓存视频帧,首次导出需要准备,可切换标签页。","Pre-decoding video and caching frames. You can switch tabs.":"正在预解码视频并缓存帧,可切换标签页。","Preferences are stored in this browser only.":"偏好设置仅保存在当前浏览器中。","Preparing source frames…":"准备素材帧…","Preparing the next part of your video. Export will continue automatically.":"正在准备下一段视频,完成后将自动继续导出。","Preparing video frames…":"准备视频帧…",Preset:"预设",Preview:"预览","Preview speed — click to cycle 1×/1.5×/2×/4×. While playing: L faster · J slower · K play/pause and reset to 1×. Preview only, not export.":"预览速度:单击切换 1×/1.5×/2×/4×。播放时 L 加速、J 减速、K 播放/暂停并恢复 1×。仅影响预览。","Program Monitor":"预览监视器",Project:"项目",Media:"媒体","Project frame rate":"项目帧率","Project name":"项目名称","Project was updated externally — your last change may need redoing.":"项目已被外部更新,可能需要重新应用刚才的修改。","Random style":"随机样式","Realtime (in-browser)":"实时(浏览器内录制)","Remove (and its clips)":"移除素材及其片段","Rendering frames → ffmpeg. You can switch tabs; export continues.":"正在渲染并交由 ffmpeg 编码,切换标签页后仍继续导出。","Rendering your sequence in real time. Keep this tab focused.":"正在实时渲染时间线,请保持当前标签页处于前台。","Rendering… {percent}%":"渲染中… {percent}%","Replace this clip's media — keeps position, trim, keyframes and effects":"替换片段素材,保留位置、裁剪、关键帧和特效","Reset monitor view to fit stage":"重置预览缩放以适应窗口","Restore default layout":"恢复默认布局",Right:"右声道",S:"S",SVG:"SVG","Scale to fit":"缩放以适应","Select a clip first":"请先选择片段","Select a clip to edit its":"选择片段以编辑","Select a clip to zoom to":"请选择要缩放显示的片段","Select all / deselect":"全选/取消选择","Selecting a timeline clip highlights its media in Project, and the reverse. Off by default.":"选择时间线片段时高亮项目中的对应素材,反之亦然。默认关闭。",Selection:"选择","Set IN / OUT work-area marker":"设置工作区入点/出点标记","Set IN and OUT markers first (I / O)":"请先设置入点和出点标记(I / O)","Set an IN or OUT marker first (I / O)":"请先设置入点或出点标记(I / O)","Set keyframe at playhead":"在播放头处设置关键帧",Settings:"设置","Show / hide keyframe graph":"显示/隐藏关键帧曲线",Shuffle:"随机","Shuttle preview —":"变速预览 —",Snap:"吸附","Sound FX":"音效","Sound effects from library/sfx":"来自 library/sfx 的音效",Source:"素材源","Split at playhead (S)":"在播放头处分割(S)","Split clip(s) at playhead":"在播放头处分割片段","Split clips at IN/OUT markers":"在入点/出点标记处分割片段","Start (s)":"开始(秒)","Step 1 frame (⇧ = 1 second)":"前进/后退一帧(⇧ = 一秒)","Stroke col.":"描边颜色",System:"系统字体",Text:"文本","Text style":"文本样式","Then drag media onto the timeline below.":"然后将素材拖到下方时间线上。","Timeline is empty — add some clips first.":"时间线为空,请先添加片段。","Timeline track height":"时间线轨道高度","Toggle safe-area guides":"显示/隐藏安全区域参考线","Toggle snapping":"开关吸附","Toggle snapping (N)":"开关吸附(N)","Track {tracks} is disabled and will be omitted from the export.":"轨道 {tracks} 已禁用,不会包含在导出中。","Tracks {tracks} are disabled and will be omitted from the export.":"轨道 {tracks} 已禁用,不会包含在导出中。",Transform:"变换",Transition:"转场",Trim:"裁剪","Trim clips to IN/OUT (remove heads/tails)":"按入点/出点裁剪片段(移除首尾)","Trim clips to IN/OUT (⇧T)":"按入点/出点裁剪片段(⇧T)","Trim selected in / out to playhead":"将所选片段的入点/出点裁至播放头","Type any Google Font name…":"输入 Google 字体名称…","Undo / redo":"撤销/重做","Unsupported file type: ":"不支持的文件类型:","Untitled Project":"未命名项目",Uppercase:"全部大写","V-align":"垂直对齐","Vector animations from library/svg":"来自 library/svg 的矢量动画","Video preview: {name}: {error}":"视频预览:{name}:{error}",Weight:"字重","Width in px (0 = no box — hug content)":"宽度(像素);0 表示按内容自适应",Zoom:"缩放","Zoom timeline":"缩放时间线","Zoom timeline at cursor":"以光标为中心缩放时间线","Zoom timeline to fit":"缩放以显示完整时间线","Zoom timeline to fit (⇧Z)":"缩放以显示完整时间线(⇧Z)","Zoom to IN–OUT work area":"缩放以显示入点至出点的工作区","Zoom to selected clip(s)":"缩放以显示所选片段","faster /":"加速/",flat:"纯色","loading timed out":"加载超时",off:"关闭","or click":"或单击","play/pause + reset to 1×":"播放/暂停并恢复 1×","slower (1× · 1.5× · 2× · 4×);":"减速(1× · 1.5× · 2× · 4×);","transform, effects & audio.":"变换、特效与音频。","{count} clips selected — drag moves them together, Del deletes all. Fields below edit the primary (white-outlined) clip.":"已选 {count} 个片段,拖动可一起移动,Del 删除全部。下方属性仅编辑主选片段(白色边框)。","{name} has fewer channels — removed {count} linked audio clips":"{name} 的声道更少,已移除 {count} 个关联音频片段","{name}: added {tracks} audio tracks and linked {channels} channels":"{name}:已添加 {tracks} 条音轨并关联 {channels} 个声道","{name}: {count} audio channels, only {max} channels supported — extra channels dropped":"{name}:共 {count} 个声道,单个素材最多支持 {max} 个声道,已舍弃多余声道","— this list live-updates.":"— 列表会自动更新。","⊡ Layout":"⊡ 布局","▦ Safe":"▦ 安全区","⚪ local session":"⚪ 本地会话","✂ Split":"✂ 分割","⬇ Export":"⬇ 导出","+ Project":"+ 项目","📂 Browse file…":"📂 浏览文件…","🟢 connected":"🟢 已连接",video:"视频",audio:"音频",image:"图片",svg:"矢量动画",text:"文字",adjust:"调整层",none:"无",fade:"淡入淡出","slide-left":"向左滑入","slide-right":"向右滑入","slide-up":"向上滑入","slide-down":"向下滑入",zoom:"缩放",wipe:"向左擦除","wipe-right":"向右擦除","wipe-up":"向上擦除","wipe-down":"向下擦除",iris:"圆形展开",spin:"旋转",blur:"模糊",whip:"甩镜",glitch:"故障",pop:"弹出",typewriter:"打字机","word-pop":"逐词弹出","word-slide":"逐词滑入",karaoke:"卡拉 OK","letter-pop":"逐字弹出",wave:"波浪",bounce:"弹跳",shake:"抖动","clip-reveal":"裁切揭示","zoom-in":"放大入场","font-cut":"字体切换","rise-mask":"遮罩上升",normal:"正常",multiply:"正片叠底",screen:"滤色",overlay:"叠加",lighter:"加亮","soft-light":"柔光","hard-light":"强光","color-dodge":"颜色减淡",darken:"变暗",lighten:"变亮",difference:"差值",contain:"完整显示",cover:"填满画布",stretch:"拉伸",left:"左对齐",center:"居中",right:"右对齐",justify:"两端对齐",top:"顶部",middle:"中部",bottom:"底部",auto:"自动",ltr:"从左到右",rtl:"从右到左",cinematic:"电影感","teal-orange":"青橙",noir:"黑白电影",vintage:"复古",faded:"褪色",warm:"暖色",cold:"冷色",dreamy:"梦幻",retro:"怀旧","bw-soft":"柔和黑白",cyberpunk:"赛博朋克",sunset:"日落",midnight:"午夜",Plain:"简约",Impact:"冲击",Elegant:"优雅","Kinetic cut":"动态切换",Neon:"霓虹",Handwritten:"手写","Serif drop":"衬线落入",Subtitle:"字幕","Bold rise":"粗体上升",Luxury:"典雅",Scale:"缩放",Rotation:"旋转",Opacity:"不透明度",Brightness:"亮度",Contrast:"对比度",Saturation:"饱和度",Hue:"色相",Temperature:"色温",Tint:"色调",Blur:"模糊",Grayscale:"灰度",Sepia:"褐色",Invert:"反相",Vignette:"暗角",CornerRadius:"圆角",Shake:"抖动",ShakeSpeed:"抖动速度",RgbSplit:"色差分离",Grain:"颗粒",ChromaTolerance:"抠像容差",ChromaSoftness:"边缘柔化",Volume:"音量",Speed:"速度",LetterSpacing:"字距",LineHeight:"行距",StrokeWidth:"描边宽度",BgOpacity:"背景不透明度",TextShadow:"文字阴影",Glow:"发光",WordRate:"逐词间隔",FontSize:"字号","Pos X":"位置 X","Pos Y":"位置 Y",Bright:"亮度",Sat:"饱和度",Gray:"灰度",Temp:"色温",Radius:"圆角",RGB:"RGB",Size:"字号",Track:"字距","9:16 · Reel 1080×1920":"9:16 · 竖屏 1080×1920","4:5 · IG 1080×1350":"4:5 · 社交媒体 1080×1350"};let o="zh-CN";function a(e){return"en"===e?"en":"zh-CN"}function i(e,a={}){const i="zh-CN"===o&&Object.hasOwn(t,e)?t[e]:e;return String(i).replace(/\{(\w+)\}/g,(e,t)=>Object.hasOwn(a,t)?String(a[t]):e)}const r={text:i,normalize:a,setLanguage:function(e){o=a(e)},apply:function(e){e.documentElement.lang=o;for(const t of e.querySelectorAll("[data-i18n]"))t.textContent=i(t.getAttribute("data-i18n"));for(const t of["title","aria-label","placeholder"])for(const o of e.querySelectorAll(`[data-i18n-${t}]`))o.setAttribute(t,i(o.getAttribute(`data-i18n-${t}`)))},messages:t,get language(){return o}};"undefined"!=typeof module&&module.exports?module.exports=r:e.FableCutI18n=r}(globalThis);
1
+ "use strict";!function(e){const t={Cover:"封面","Cover selection":"封面选择","Remove cover":"删除封面","Set cover":"设置封面","Select video frame":"选取视频帧","Choose local image":"选择本地图片","Cover frame time":"封面帧时间","Use as cover":"设为封面","Reset cover":"恢复默认封面","Loading cover…":"正在加载封面…","Cover could not be loaded":"封面加载失败,请重新选择","Cover image upload failed":"封面图片上传失败,请重试","Choose a PNG, JPEG, WebP or GIF image":"请选择 PNG、JPEG、WebP 或 GIF 图片","Thumbnails {loaded}/{total}":"缩略图 {loaded}/{total}","{failed} thumbnails unavailable":"{failed} 张缩略图无法加载","Project not found":"项目不存在","Project could not be loaded":"项目加载失败","Invalid project data":"工程数据格式无效","Invalid project link. Open the project again from its original entry.":"项目链接无效,请从原入口重新打开项目。","Project request failed ({status})":"工程读取请求失败({status})","Project ID: {id}":"项目 ID:{id}","This project is not in the current or legacy storage folder. It may have been moved or deleted. Restore the project folder, then retry.":"当前及旧存储目录中均未找到此工程,可能已被移动或删除。请恢复工程文件夹后重试。","The project file could not be read or the editor service is unavailable. Check the file or service, then retry.":"工程文件无法读取,或编辑器服务暂时不可用。请检查文件或服务后重试。","Error details":"错误详情","Retry loading":"重新加载","Media missing":"媒体丢失","Media unreadable":"媒体无法读取","Check again":"重新检测","The source file was deleted or moved. Restore it to its original location, then retry.":"源文件已被删除或移动,请恢复到原位置后重新检测。","The source file could not be loaded. Check the file or connection, then retry.":"源文件无法加载,请检查文件或网络连接后重新检测。","Snapping on (N)":"吸附已开启(N)","Snapping off (N)":"吸附已关闭(N)","Zoom out (-)":"缩小时间轴(-)","Zoom in (+)":"放大时间轴(+)","Timeline zoom":"时间轴缩放","Insert before {clip}":"插到「{clip}」之前","Insert after {clip}":"插到「{clip}」之后","Move to {track} · {seconds}s":"移到{track} · {seconds}秒","Moves together":"一起移动","Moves together: {tracks}":"同时移动:{tracks}","Adjusted to an available position":"位置受限,已预览可用落点","Drop on a matching track":"请拖入同类型轨道或轨道间隙","Trimmed {seconds}s":"裁掉 {seconds} 秒","Restored {seconds}s":"恢复 {seconds} 秒","Duration {seconds}s":"剩余时长 {seconds} 秒","Following clips move together":"后续片段随裁剪联动","Linked media stays in sync":"关联音视频保持同步","Source or clip limit reached":"已到裁剪边界(素材范围/最短时长/重叠限制)","Clip actions":"片段操作","Timeline actions":"时间轴操作","Selected {count} clips · linked media follows":"已选 {count} 个片段 · 编辑包含关联音视频","Shared gap on enabled tracks":"闭合各启用轨道的公共空隙","No shared gap: {track} contains a clip":"没有公共空隙:{track}此处有片段","Move the playhead inside the clip first":"请先将播放头移到片段内部","Go to clip start":"跳到此片段片头","Zoom to selected clips":"放大所选片段","Split at playhead":"在播放头处分割","Trim start to playhead":"裁片头到播放头(当前片段)","Trim end to playhead":"裁片尾到播放头(当前片段)","Delete selected clips":"删除所选片段","Close shared gap":"闭合公共空隙","Timeline changed. Open the menu again.":"时间轴已更新,请重新打开菜单。","+ New track":"+ 新建轨道","Place here":"放置到此处","Drop media here to create a track":"拖入素材以创建轨道","+ Adjust":"+ 调整层","+ Import":"+ 导入","+ Text":"+ 文字","+click bin":"+单击素材区","+click clip":"+单击片段","+wheel":"+滚轮","AI bg remove":"AI 去背景","Add / remove from selection":"加入/移出选区","Add / remove marker at playhead":"在播放头处添加/移除标记","Add text at the playhead":"在播放头处添加文字","Add an adjustment layer (grades everything below it)":"添加调整图层(影响下方所有图层)","Add at playhead":"添加到播放头位置","Add transition at playhead (in / out by half)":"在播放头处添加转场(按前后半段选择入场/出场)","Adjustment layer":"调整图层",Align:"水平对齐",Animation:"动画",Assets:"素材","Assets panel":"素材面板","Inspector panel":"属性面板","Connection status":"连接状态","Settings button":"设置入口","Project switcher":"项目切换","Layout controls":"布局切换","Monitor toolbar":"预览工具栏","Audio meter":"音量表","Go to start / end":"跳至开头/末尾","Fit timeline":"适应时间轴","Clip {number}":"片段{number}","Visible modules":"显示模块","Hidden modules keep their functions and shortcuts.":"隐藏模块仍保留原有功能和快捷键。","Audio / Time":"音频/时间","Audio Hold — while paused, loop one frame of audio at the playhead. Play/Stop turns it off.":"音频保持:暂停时循环播放播放头处一帧音频。播放/停止会关闭此功能。","Audio hold":"音频保持","Audio {number}":"音频{number}","Video {number}":"视频{number}",Undo:"撤销",Redo:"重做","Undo (Ctrl/Cmd+Z)":"撤销(Ctrl/Cmd+Z)","Redo (Ctrl/Cmd+Shift+Z)":"重做(Ctrl/Cmd+Shift+Z)","Back 1 frame (←)":"后退一帧(←)","Background removal unavailable — couldn't load MediaPipe (offline?). Using chroma key still works.":"去背景不可用:无法加载 MediaPipe,请检查网络。仍可使用色度抠像。","Bg color":"背景色",Blend:"混合模式",Bold:"粗体","Box W/H":"文本框宽/高","Buffering…":"缓冲中…","Cached video frames · {count} compatibility fallbacks · You can switch tabs.":"使用缓存帧 · {count} 个片段使用兼容模式 · 可切换标签页。",Cancel:"取消","Canvas aspect preset":"画布宽高比预设",Channel:"声道","Channel {number}":"声道 {number}","Choose…":"选择…","Clear IN / OUT marker":"清除入点/出点标记","Clear keyframes":"清除关键帧","Click to switch RMS / LUFS / Peak":"单击切换 RMS/LUFS/峰值","Click: keyframe graph · Ctrl-click: reset":"单击:关键帧曲线 · Ctrl+单击:重置","Clip — {kind}":"片段 — {kind}",Close:"关闭","Close gap":"闭合空隙","Close gap at playhead (enabled tracks)":"闭合播放头处的空隙(已启用轨道)","Close gap at playhead (⇧G)":"闭合播放头处的空隙(⇧G)","Close graph":"关闭曲线","Closed {seconds}s gap":"已闭合 {seconds} 秒空隙",Collapse:"折叠",Color:"颜色","Compact tracks (no thumbnails)":"紧凑轨道(无缩略图)",Content:"内容","Couldn't create project: ":"无法创建项目:","Couldn't import — unsupported or unreadable file type":"无法导入:文件类型不受支持或无法读取","Couldn't play ":"无法播放:","Create a project":"创建项目","Crop L/R %":"左右裁剪 %","Crop T/B %":"上下裁剪 %","Ctrl-click to reset":"Ctrl+单击重置","Custom · {fps} fps":"自定义 · {fps} fps","Custom · {width}×{height}":"自定义 · {width}×{height}","Cycle RMS → LUFS → Peak":"循环切换 RMS → LUFS → 峰值",Delete:"删除","Delete folder (keeps media)":"删除文件夹(保留素材)","Delete selected (Del)":"删除所选片段(Del)","Delete selected clip(s), or clear focused transition":"删除所选片段,或清除当前转场",Direction:"文字方向","Disable chroma key":"关闭色度抠像","Disable gradient":"关闭渐变","Disable track":"禁用轨道","Drag empty area":"拖动空白区域","Drag to adjust duration":"拖动调整时长","Drag to resize · double-click to reset":"拖动调整大小 · 双击重置","Drop files into":"将文件放入","Drop here to move to root":"拖到此处移至根目录","Drop video, audio or images here":"将视频、音频或图片拖到此处",Elements:"装饰素材","Enable track":"启用轨道","Encoding…":"编码中…",Expand:"展开",Export:"导出","Export preview":"成片预览","Export progress":"导出进度","Play exported video":"播放成片","Video duration {seconds}s":"成片时长 {seconds} 秒","Preparing video…":"正在准备视频…","Rendering video…":"正在合成视频…","Rendering clip {current}/{total}":"正在合成第 {current}/{total} 个片段"," · About {seconds}s remaining":" · 预计剩余 {seconds} 秒","Finalizing video…":"正在封装视频,即将完成…","Video complete. Your file is ready.":"合成完成,文件已就绪",Save:"保存","Saving…":"保存中…","Choose a folder to save your video.":"请选择成片保存文件夹","Saved: {name}":"已保存:{name}","Save cancelled. Click Save to choose a folder again.":"已取消保存,可点击「保存」重新选择文件夹","Unable to save. Click Save to try again.":"保存失败,请点击「保存」重试","Click Save to choose a location in your browser.":"请点击「保存」,通过浏览器选择保存位置","Check your browser downloads to confirm the file was saved.":"已请求浏览器下载,请在下载列表中确认保存结果","Retry export":"重试导出","Cancel this export?":"确定取消导出?","The video is still exporting. Cancelling will stop this task.":"视频仍在导出中,取消后将停止本次任务。","Continue export":"继续导出","Confirm cancellation":"确定取消","Quick export":"快捷合成","Quick export in progress. You can switch chats.":"正在后台快捷合成,可以切换聊天","Background recording failed. You can record in this page while it stays open.":"后台录制失败,可在当前页面录制。录制期间请保持页面在前台。","Record in this page":"在当前页面录制","Recording failed. Keep this page open to try again.":"当前页面录制失败,可重新录制。录制期间请保持页面在前台。","Export interrupted. Try quick export to export again.":"合成任务中断,可尝试快捷合成重新导出","Quick export in progress. Keep this page open and in the foreground.":"快捷合成中,请保持当前页面,不要关闭窗口或切换页面","The exported file is unavailable. Please retry.":"未获取到成片文件,请重试。","Unable to check export progress. Please retry.":"暂时无法查询导出进度,请重试。","Unable to preview. Open the downloaded video to watch it.":"暂时无法预览,请打开已下载的视频观看。","Export failed: ":"导出失败:","Exporting…":"导出中…","Exporting… {percent}%":"导出中… {percent}%","Tik — Video Editor":"Tik — 视频编辑器",Family:"字体","CLI Fast (ffmpeg)":"CLI 快速(ffmpeg)","CLI Optimized (ffmpeg + frame cache)":"CLI 优化(ffmpeg + 帧缓存)","Fast (ffmpeg)":"快速(ffmpeg)","Filter / Color":"滤镜/调色","Find gap":"查找空隙","Find gap (G)":"查找空隙(G)","Find next shared gap (wraps; respects IN/OUT)":"查找下一处共同空隙(循环查找,遵循入点/出点)",Fit:"适应","Flip H":"水平翻转","Flip V":"垂直翻转",Font:"字体","Font size":"字号","Forward 1 frame (→)":"前进一帧(→)","Queued for export…":"正在排队,等待导出…","Starting background export…":"正在启动后台导出…","CLI renders in a separate process. Closing this page does not stop export.":"CLI 在独立进程中渲染,关闭此页面后仍继续导出。","CLI pre-decodes and caches frames in a separate process. You can close this page.":"CLI 在独立进程中预解码并缓存视频帧,关闭此页面后仍继续导出。","CLI renders frames in a separate process with ffmpeg. You can close this page.":"CLI 在独立进程中逐帧渲染并交由 ffmpeg 编码,关闭此页面后仍继续导出。","Frame-accurate ffmpeg encode. Keeps rendering if you switch tabs.":"使用 ffmpeg 逐帧编码,切换标签页后仍继续渲染。","Glow color":"发光颜色","Glow uses the text color":"发光使用文字颜色","Go to end (End)":"跳到结尾(End)","Go to previous / next keyframe":"跳到上一/下一关键帧","Go to start (Home)":"跳到开头(Home)","Google font":"Google 字体","Google fonts":"Google 字体","Gradient bottom color":"渐变底部颜色","Height in px (0 = no box — hug content)":"高度(像素);0 表示按内容自适应","IN and OUT must be at different times":"入点与出点不能相同","IN must be before OUT":"入点必须早于出点","Import media files":"导入素材文件","Imported {added}, skipped {skipped}":"已导入 {added} 个,跳过 {skipped} 个","Imported, but cannot replace {kind} with {replacement}":"已导入,但无法用{replacement}替换{kind}片段",In:"入场",Inspector:"属性","Interface language":"界面语言",Italic:"斜体","Jump to start / end":"跳到开头/结尾","Key color":"抠像颜色","Keyboard shortcuts":"键盘快捷键","Keying / Cut-out":"抠像/去背景",L:"L",Language:"语言","Large tracks":"大轨道",Layout:"布局",Left:"左声道","Length (s)":"时长(秒)","Library fonts":"本地字体库",Limit:"限区播放","Limit (toolbar)":"限区播放(工具栏)","Limit playback to IN/OUT (Home/End jump to markers; playhead outside = override)":"限制在入点/出点之间播放(Home/End 跳到标记;播放头在范围外时不限制)","Link timeline and Project bin selection":"联动时间线与项目素材选择","Linked {count} audio channels from {name}":"已关联 {name} 的 {count} 个声道",Load:"加载",'Loading Google font "{name}"…':"正在加载 Google 字体“{name}”…",M:"M","Marquee-select clips":"框选片段","Max size":"最大字号","Media replaced":"已替换素材","Media replaced — trimmed to fit shorter source":"已替换素材,并按较短素材裁剪","Media used by this project — right-click for New folder":"本项目使用的素材,右键可新建文件夹","MediaRecorder is not supported in this browser.":"当前浏览器不支持 MediaRecorder。","Medium tracks":"中等轨道","Missing media":"素材缺失","Mixing audio…":"混音中…","Mode: {mode} — click to switch":"模式:{mode} — 单击切换","Motion FX":"运动特效","Move playhead over the selected clip":"请将播放头移到所选片段上",Name:"名称","Needs the server + ffmpeg and ffprobe on PATH.":"需要运行本地服务,且 PATH 中包含 ffmpeg 和 ffprobe。","Needs the server + ffmpeg on PATH.":"需要运行本地服务,且 PATH 中包含 ffmpeg。","New folder":"新建文件夹","New subfolder":"新建子文件夹","No assets yet.":"暂无素材。","No clips on the timeline use this media":"时间线上没有使用此素材的片段","No enabled tracks":"没有已启用的轨道","No gap on {track} — disable the track or move the playhead":"轨道 {track} 没有空隙,请禁用该轨道或移动播放头","No gaps found":"未找到空隙","No gaps in IN/OUT range":"入点/出点范围内没有空隙","No keyframes":"没有关键帧","No next keyframe":"没有下一关键帧","No other {kind} in bin":"素材区没有其他{kind}素材","No previous keyframe":"没有上一关键帧","Nothing to close at playhead":"播放头处没有可闭合的空隙","Nothing to split at IN/OUT":"入点/出点处没有可分割的片段","Nothing to trim":"没有可裁剪的片段","OUT must be after IN":"出点必须晚于入点","Open project":"打开项目","Optimized (ffmpeg + frame cache)":"优化(ffmpeg + 帧缓存)","Optimized export complete: {seconds}s · {hits} cache hits":"优化导出完成:{seconds} 秒 · 命中缓存 {hits} 次","Optimized export failed: ":"优化导出失败:",Out:"出场","Overlapping audio":"重叠音频","Overlay art from library/elements":"来自 library/elements 的叠加素材","Pick a style — hover to preview it live":"选择样式,悬停可实时预览","Play / Pause (Space)":"播放/暂停(空格)","Play / pause":"播放/暂停","Play stops at OUT; Home/End use IN/OUT":"播放到出点停止;Home/End 跳到入点/出点","Plays the timeline once and records it. Keep the tab focused.":"播放并录制一遍时间线,请保持当前标签页处于前台。","Position X":"位置 X","Position Y":"位置 Y","Pre-decodes video frames and caches them. First export needs preparation; you can switch tabs.":"预解码并缓存视频帧,首次导出需要准备,可切换标签页。","Pre-decoding video and caching frames. You can switch tabs.":"正在预解码视频并缓存帧,可切换标签页。","Preferences are stored in this browser only.":"偏好设置仅保存在当前浏览器中。","Preparing source frames…":"准备素材帧…","Preparing the next part of your video. Export will continue automatically.":"正在准备下一段视频,完成后将自动继续导出。","Preparing video frames…":"准备视频帧…",Preset:"预设",Preview:"预览","Preview speed — click to cycle 1×/1.5×/2×/4×. While playing: L faster · J slower · K play/pause and reset to 1×. Preview only, not export.":"预览速度:单击切换 1×/1.5×/2×/4×。播放时 L 加速、J 减速、K 播放/暂停并恢复 1×。仅影响预览。","Program Monitor":"预览监视器",Project:"项目",Media:"媒体","Project frame rate":"项目帧率","Project name":"项目名称","Project was updated externally — your last change may need redoing.":"项目已被外部更新,可能需要重新应用刚才的修改。","Random style":"随机样式","Realtime (in-browser)":"实时(浏览器内录制)","Remove (and its clips)":"移除素材及其片段","Rendering frames → ffmpeg. You can switch tabs; export continues.":"正在渲染并交由 ffmpeg 编码,切换标签页后仍继续导出。","Rendering your sequence in real time. Keep this tab focused.":"正在实时渲染时间线,请保持当前标签页处于前台。","Rendering… {percent}%":"渲染中… {percent}%","Replace this clip's media — keeps position, trim, keyframes and effects":"替换片段素材,保留位置、裁剪、关键帧和特效","Reset monitor view to fit stage":"重置预览缩放以适应窗口","Restore default layout":"恢复默认布局",Right:"右声道",S:"S",SVG:"SVG","Scale to fit":"缩放以适应","Select a clip first":"请先选择片段","Select a clip to edit its":"选择片段以编辑","Select a clip to zoom to":"请选择要缩放显示的片段","Select all / deselect":"全选/取消选择","Selecting a timeline clip highlights its media in Project, and the reverse. Off by default.":"选择时间线片段时高亮项目中的对应素材,反之亦然。默认关闭。",Selection:"选择","Set IN / OUT work-area marker":"设置工作区入点/出点标记","Set IN and OUT markers first (I / O)":"请先设置入点和出点标记(I / O)","Set an IN or OUT marker first (I / O)":"请先设置入点或出点标记(I / O)","Set keyframe at playhead":"在播放头处设置关键帧","Delivery checks":"成片检查","Checks unavailable":"检查结果不可用","Export encoding":"导出编码","Default export method":"默认导出方式","Optimized (frame cache)":"优化(帧缓存)","Fast (ffmpeg)":"快速(ffmpeg)","WebCodecs (browser encoding, default)":"WebCodecs(浏览器编码,默认)","WebCodecs encodes H.264 in the browser without delivery presets or encoding profiles.":"WebCodecs 使用浏览器编码 H.264,不应用交付预设或编码配置。","Encoding profile":"编码配置","Software H.264 (default)":"软件 H.264(默认)","Automatic hardware encoding":"自动选择硬件编码",Settings:"设置","Show / hide keyframe graph":"显示/隐藏关键帧曲线",Shuffle:"随机","Shuttle preview —":"变速预览 —",Snap:"吸附","Sound FX":"音效","Sound effects from library/sfx":"来自 library/sfx 的音效",Source:"素材源","Split at playhead (S)":"在播放头处分割(S)","Split clip(s) at playhead":"在播放头处分割片段","Split clips at IN/OUT markers":"在入点/出点标记处分割片段","Start (s)":"开始(秒)","Step 1 frame (⇧ = 1 second)":"前进/后退一帧(⇧ = 一秒)","Stroke col.":"描边颜色",System:"系统字体",Text:"文本","Text style":"文本样式","Then drag media onto the timeline below.":"然后将素材拖到下方时间线上。","Timeline is empty — add some clips first.":"时间线为空,请先添加片段。","Timeline track height":"时间线轨道高度","Toggle safe-area guides":"显示/隐藏安全区域参考线","Toggle snapping":"开关吸附","Toggle snapping (N)":"开关吸附(N)","Track {tracks} is disabled and will be omitted from the export.":"轨道 {tracks} 已禁用,不会包含在导出中。","Tracks {tracks} are disabled and will be omitted from the export.":"轨道 {tracks} 已禁用,不会包含在导出中。",Transform:"变换",Transition:"转场",Trim:"裁剪","Trim clips to IN/OUT (remove heads/tails)":"按入点/出点裁剪片段(移除首尾)","Trim clips to IN/OUT (⇧T)":"按入点/出点裁剪片段(⇧T)","Trim selected in / out to playhead":"将所选片段的入点/出点裁至播放头","Type any Google Font name…":"输入 Google 字体名称…","Undo / redo":"撤销/重做","Unsupported file type: ":"不支持的文件类型:","Untitled Project":"未命名项目",Uppercase:"全部大写","V-align":"垂直对齐","Vector animations from library/svg":"来自 library/svg 的矢量动画","Video preview: {name}: {error}":"视频预览:{name}:{error}",Weight:"字重","Width in px (0 = no box — hug content)":"宽度(像素);0 表示按内容自适应",Zoom:"缩放","Zoom timeline":"缩放时间线","Zoom timeline at cursor":"以光标为中心缩放时间线","Zoom timeline to fit":"缩放以显示完整时间线","Zoom timeline to fit (⇧Z)":"缩放以显示完整时间线(⇧Z)","Zoom to IN–OUT work area":"缩放以显示入点至出点的工作区","Zoom to selected clip(s)":"缩放以显示所选片段","faster /":"加速/",flat:"纯色","loading timed out":"加载超时",off:"关闭","or click":"或单击","play/pause + reset to 1×":"播放/暂停并恢复 1×","slower (1× · 1.5× · 2× · 4×);":"减速(1× · 1.5× · 2× · 4×);","transform, effects & audio.":"变换、特效与音频。","{count} clips selected — drag moves them together, Del deletes all. Fields below edit the primary (white-outlined) clip.":"已选 {count} 个片段,拖动可一起移动,Del 删除全部。下方属性仅编辑主选片段(白色边框)。","{name} has fewer channels — removed {count} linked audio clips":"{name} 的声道更少,已移除 {count} 个关联音频片段","{name}: added {tracks} audio tracks and linked {channels} channels":"{name}:已添加 {tracks} 条音轨并关联 {channels} 个声道","{name}: {count} audio channels, only {max} channels supported — extra channels dropped":"{name}:共 {count} 个声道,单个素材最多支持 {max} 个声道,已舍弃多余声道","— this list live-updates.":"— 列表会自动更新。","⊡ Layout":"⊡ 布局","▦ Safe":"▦ 安全区","⚪ local session":"⚪ 本地会话","✂ Split":"✂ 分割","⬇ Export":"⬇ 导出","+ Project":"+ 项目","📂 Browse file…":"📂 浏览文件…","🟢 connected":"🟢 已连接",video:"视频",audio:"音频",image:"图片",svg:"矢量动画",text:"文字",adjust:"调整层",none:"无",fade:"淡入淡出","slide-left":"向左滑入","slide-right":"向右滑入","slide-up":"向上滑入","slide-down":"向下滑入",zoom:"缩放",wipe:"向左擦除","wipe-right":"向右擦除","wipe-up":"向上擦除","wipe-down":"向下擦除",iris:"圆形展开",spin:"旋转",blur:"模糊",whip:"甩镜",glitch:"故障",pop:"弹出",typewriter:"打字机","word-pop":"逐词弹出","word-slide":"逐词滑入",karaoke:"卡拉 OK","letter-pop":"逐字弹出",wave:"波浪",bounce:"弹跳",shake:"抖动","clip-reveal":"裁切揭示","zoom-in":"放大入场","font-cut":"字体切换","rise-mask":"遮罩上升",normal:"正常",multiply:"正片叠底",screen:"滤色",overlay:"叠加",lighter:"加亮","soft-light":"柔光","hard-light":"强光","color-dodge":"颜色减淡",darken:"变暗",lighten:"变亮",difference:"差值",contain:"完整显示",cover:"填满画布",stretch:"拉伸",left:"左对齐",center:"居中",right:"右对齐",justify:"两端对齐",top:"顶部",middle:"中部",bottom:"底部",auto:"自动",ltr:"从左到右",rtl:"从右到左",cinematic:"电影感","teal-orange":"青橙",noir:"黑白电影",vintage:"复古",faded:"褪色",warm:"暖色",cold:"冷色",dreamy:"梦幻",retro:"怀旧","bw-soft":"柔和黑白",cyberpunk:"赛博朋克",sunset:"日落",midnight:"午夜",Plain:"简约",Impact:"冲击",Elegant:"优雅","Kinetic cut":"动态切换",Neon:"霓虹",Handwritten:"手写","Serif drop":"衬线落入",Subtitle:"字幕","Bold rise":"粗体上升",Luxury:"典雅",Scale:"缩放",Rotation:"旋转",Opacity:"不透明度",Brightness:"亮度",Contrast:"对比度",Saturation:"饱和度",Hue:"色相",Temperature:"色温",Tint:"色调",Blur:"模糊",Grayscale:"灰度",Sepia:"褐色",Invert:"反相",Vignette:"暗角",CornerRadius:"圆角",Shake:"抖动",ShakeSpeed:"抖动速度",RgbSplit:"色差分离",Grain:"颗粒",ChromaTolerance:"抠像容差",ChromaSoftness:"边缘柔化",Volume:"音量",Speed:"速度",LetterSpacing:"字距",LineHeight:"行距",StrokeWidth:"描边宽度",BgOpacity:"背景不透明度",TextShadow:"文字阴影",Glow:"发光",WordRate:"逐词间隔",FontSize:"字号","Pos X":"位置 X","Pos Y":"位置 Y",Bright:"亮度",Sat:"饱和度",Gray:"灰度",Temp:"色温",Radius:"圆角",RGB:"RGB",Size:"字号",Track:"字距","9:16 · Reel 1080×1920":"9:16 · 竖屏 1080×1920","4:5 · IG 1080×1350":"4:5 · 社交媒体 1080×1350"};let o="zh-CN";function a(e){return"en"===e?"en":"zh-CN"}function r(e,a={}){const r="zh-CN"===o&&Object.hasOwn(t,e)?t[e]:e;return String(r).replace(/\{(\w+)\}/g,(e,t)=>Object.hasOwn(a,t)?String(a[t]):e)}const i={text:r,normalize:a,setLanguage:function(e){o=a(e)},apply:function(e){e.documentElement.lang=o;for(const t of e.querySelectorAll("[data-i18n]"))t.textContent=r(t.getAttribute("data-i18n"));for(const t of["title","aria-label","placeholder"])for(const o of e.querySelectorAll(`[data-i18n-${t}]`))o.setAttribute(t,r(o.getAttribute(`data-i18n-${t}`)))},messages:t,get language(){return o}};"undefined"!=typeof module&&module.exports?module.exports=i:e.FableCutI18n=i}(globalThis);