@cut-workbench/core 0.4.0 → 0.4.2

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.
Files changed (58) hide show
  1. package/dist/chunks/{canvas-size-Ct61OYjW.cjs → canvas-size-DIABzEW2.cjs} +212 -44
  2. package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
  3. package/dist/chunks/{canvas-size-CM2oKApi.js → canvas-size-Dlnng74w.js} +168 -30
  4. package/dist/chunks/canvas-size-Dlnng74w.js.map +1 -0
  5. package/dist/chunks/id-CJsdlGei.js +35 -0
  6. package/dist/chunks/id-CJsdlGei.js.map +1 -0
  7. package/dist/chunks/id-DaCxD4-5.cjs +70 -0
  8. package/dist/chunks/id-DaCxD4-5.cjs.map +1 -0
  9. package/dist/chunks/{snap-DyIuO2hU.cjs → snap-Cfk15JVg.cjs} +2 -22
  10. package/dist/chunks/{snap-DyIuO2hU.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
  11. package/dist/chunks/{snap-CBV9G4xq.js → snap-DpkVPVJN.js} +3 -17
  12. package/dist/chunks/{snap-CBV9G4xq.js.map → snap-DpkVPVJN.js.map} +1 -1
  13. package/dist/chunks/thumbnails-DEjnao5r.cjs +81 -0
  14. package/dist/chunks/thumbnails-DEjnao5r.cjs.map +1 -0
  15. package/dist/chunks/thumbnails-qH0dX_Zs.js +64 -0
  16. package/dist/chunks/thumbnails-qH0dX_Zs.js.map +1 -0
  17. package/dist/chunks/{time-CnMfSVYX.cjs → time-CYEd91qN.cjs} +1 -69
  18. package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
  19. package/dist/chunks/{time-C_gvkVl1.js → time-ChF1GwCy.js} +2 -34
  20. package/dist/chunks/time-ChF1GwCy.js.map +1 -0
  21. package/dist/chunks/{waveform-Ie-G5Lle.cjs → waveform-BYwDdKpC.cjs} +34 -15
  22. package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
  23. package/dist/chunks/{waveform-DJkRtMct.js → waveform-uyGNE01H.js} +32 -13
  24. package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
  25. package/dist/engine/index.cjs +668 -76
  26. package/dist/engine/index.cjs.map +1 -1
  27. package/dist/engine/index.js +648 -57
  28. package/dist/engine/index.js.map +1 -1
  29. package/dist/engine/webav/exporter.d.ts +3 -1
  30. package/dist/engine/webav/import-files.d.ts +17 -2
  31. package/dist/engine/webav/import-pipeline.d.ts +10 -0
  32. package/dist/engine/webav/types.d.ts +15 -0
  33. package/dist/index.cjs +18 -13
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +6 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/error.d.ts +6 -0
  39. package/dist/media/thumbnails.d.ts +8 -0
  40. package/dist/project/schema.d.ts +16 -4
  41. package/dist/timeline/index.cjs +10 -6
  42. package/dist/timeline/index.d.ts +1 -0
  43. package/dist/timeline/index.js +4 -4
  44. package/dist/timeline/normalize.d.ts +28 -0
  45. package/dist/timeline/ops.d.ts +0 -4
  46. package/dist/timeline/snap.d.ts +0 -2
  47. package/dist/types.d.ts +14 -2
  48. package/package.json +1 -1
  49. package/dist/chunks/canvas-size-CM2oKApi.js.map +0 -1
  50. package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +0 -1
  51. package/dist/chunks/import-pipeline-A8KkqtZT.cjs +0 -610
  52. package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +0 -1
  53. package/dist/chunks/import-pipeline-DLIdgrIb.js +0 -521
  54. package/dist/chunks/import-pipeline-DLIdgrIb.js.map +0 -1
  55. package/dist/chunks/time-C_gvkVl1.js.map +0 -1
  56. package/dist/chunks/time-CnMfSVYX.cjs.map +0 -1
  57. package/dist/chunks/waveform-DJkRtMct.js.map +0 -1
  58. package/dist/chunks/waveform-Ie-G5Lle.cjs.map +0 -1
@@ -1,12 +1,304 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../chunks/rolldown-runtime-B2SfzNBj.cjs");
3
- const require_time = require("../chunks/time-CnMfSVYX.cjs");
4
- const require_canvas_size = require("../chunks/canvas-size-Ct61OYjW.cjs");
5
- const require_waveform = require("../chunks/waveform-Ie-G5Lle.cjs");
3
+ const require_id = require("../chunks/id-DaCxD4-5.cjs");
4
+ const require_time = require("../chunks/time-CYEd91qN.cjs");
5
+ const require_canvas_size = require("../chunks/canvas-size-DIABzEW2.cjs");
6
+ const require_waveform = require("../chunks/waveform-BYwDdKpC.cjs");
6
7
  const require_file_reader = require("../chunks/file-reader-DfMEluGr.cjs");
7
- const require_import_pipeline = require("../chunks/import-pipeline-A8KkqtZT.cjs");
8
+ const require_thumbnails = require("../chunks/thumbnails-DEjnao5r.cjs");
8
9
  let _webav_av_cliper = require("@webav/av-cliper");
9
10
  let _webav_av_canvas = require("@webav/av-canvas");
11
+ // src/engine/webav/styles.ts
12
+ var DEFAULT_TEXT_STYLE = {
13
+ fontSize: 64,
14
+ color: "#ffffff",
15
+ fontFamily: "sans-serif",
16
+ fontWeight: 600,
17
+ fontStyle: "normal",
18
+ strokeColor: "#000000",
19
+ strokeWidth: 0,
20
+ shadowColor: "rgba(0,0,0,0.6)",
21
+ shadowBlur: 8,
22
+ align: "center",
23
+ background: "transparent"
24
+ };
25
+ /**
26
+ * 字幕默认字体。
27
+ *
28
+ * ⚠️ 不要写 'Noto Sans SC' —— 大多数系统并未安装该字体,`EmbedSubtitlesClip` 会把
29
+ * `ctx.font` 设为 `30px Noto Sans SC`,此时浏览器回退到默认字体,中文可能显示为方框。
30
+ * 使用系统字体栈(sans-serif)由浏览器挑选可用的中文字体,最稳。
31
+ */
32
+ var DEFAULT_SUBTITLE_STYLE = {
33
+ color: "#ffffff",
34
+ textBgColor: null,
35
+ fontFamily: "sans-serif",
36
+ fontSize: 30,
37
+ strokeStyle: "#000000",
38
+ lineWidth: null,
39
+ bottomOffset: 30
40
+ };
41
+ // src/engine/webav/text-clip.ts
42
+ function cssFromStyle(style) {
43
+ const shadow = style.shadowBlur > 0 ? `text-shadow: 0 2px ${style.shadowBlur}px ${style.shadowColor};` : "";
44
+ const stroke = style.strokeWidth > 0 ? `-webkit-text-stroke: ${style.strokeWidth}px ${style.strokeColor};` : "";
45
+ return [
46
+ `font-size:${style.fontSize}px`,
47
+ `color:${style.color}`,
48
+ `font-family:${style.fontFamily}`,
49
+ `font-weight:${style.fontWeight}`,
50
+ `font-style:${style.fontStyle}`,
51
+ `text-align:${style.align}`,
52
+ `background:${style.background}`,
53
+ "line-height:1.25",
54
+ "padding:8px 16px",
55
+ "white-space:pre-wrap",
56
+ "max-width:80%",
57
+ stroke,
58
+ shadow
59
+ ].filter(Boolean).join(";");
60
+ }
61
+ /**
62
+ * 文字素材。内部缓存最近一次渲染的 ImageBitmap,按「文本 + 样式」做键避免重复渲染。
63
+ */
64
+ var TextClip = class TextClip {
65
+ ready;
66
+ opts;
67
+ metaValue;
68
+ cacheKey = "";
69
+ cachedBitmap = null;
70
+ destroyed = false;
71
+ constructor(options) {
72
+ this.opts = {
73
+ ...options,
74
+ style: {
75
+ ...DEFAULT_TEXT_STYLE,
76
+ ...options.style
77
+ },
78
+ animation: options.animation ?? "none",
79
+ charsPerSecond: options.charsPerSecond ?? 12,
80
+ animOffsetUs: options.animOffsetUs ?? 0
81
+ };
82
+ this.metaValue = {
83
+ width: options.width,
84
+ height: options.height,
85
+ duration: Math.max(1, options.durationUs)
86
+ };
87
+ this.ready = Promise.resolve(this.metaValue);
88
+ }
89
+ get meta() {
90
+ return this.metaValue;
91
+ }
92
+ visibleTextAt(timeUs) {
93
+ const { text, animation, charsPerSecond } = this.opts;
94
+ if (animation === "none") return text;
95
+ const phaseUs = timeUs + this.opts.animOffsetUs;
96
+ if (animation === "typewriter") {
97
+ const visible = Math.floor(phaseUs / 1e6 * charsPerSecond);
98
+ if (visible <= 0) return null;
99
+ return text.slice(0, visible);
100
+ }
101
+ return Math.floor(phaseUs / 1e6 * 2) % 2 === 0 ? text : null;
102
+ }
103
+ async tick(time) {
104
+ if (this.destroyed) return { state: "done" };
105
+ if (time >= this.opts.durationUs) return { state: "done" };
106
+ const visible = this.visibleTextAt(time);
107
+ if (visible == null || visible.length === 0) return {
108
+ video: await this.renderEmpty(),
109
+ state: "success"
110
+ };
111
+ const key = `${visible}|${JSON.stringify(this.opts.style)}`;
112
+ if (key !== this.cacheKey || this.cachedBitmap == null) {
113
+ this.cachedBitmap?.close();
114
+ this.cachedBitmap = await this.renderTextToFrame(visible);
115
+ this.cacheKey = key;
116
+ }
117
+ return {
118
+ video: await createImageBitmap(this.cachedBitmap),
119
+ state: "success"
120
+ };
121
+ }
122
+ /**
123
+ * 把文字渲染成「整帧」位图(工程分辨率)。
124
+ *
125
+ * 为什么不能直接把 renderTxt2ImgBitmap 的结果当帧用:
126
+ * 它返回的是文字自身尺寸的小图(几十到几百像素宽),Sprite 会把它拉伸到 rect 的
127
+ * 1280×720 —— 文字被放大后严重模糊,且居中位置由 Sprite 决定而非文字块决定。
128
+ * 因此这里先渲染文字小图,再按比例居中合成到工程尺寸的画布上。
129
+ */
130
+ async renderTextToFrame(text) {
131
+ const { width, height } = this.opts;
132
+ const canvas = new OffscreenCanvas(width, height);
133
+ const ctx = canvas.getContext("2d");
134
+ if (ctx == null) throw new Error("无法创建文字渲染上下文");
135
+ const layer = await (0, _webav_av_cliper.renderTxt2ImgBitmap)(text, cssFromStyle(this.opts.style));
136
+ const align = this.opts.style.align;
137
+ const margin = Math.round(width * .05);
138
+ const maxWidth = width - margin * 2;
139
+ const scale = layer.width > maxWidth ? maxWidth / layer.width : 1;
140
+ const drawW = Math.round(layer.width * scale);
141
+ const drawH = Math.round(layer.height * scale);
142
+ const x = align === "left" ? margin : align === "right" ? width - margin - drawW : (width - drawW) / 2;
143
+ const y = Math.round((height - drawH) / 2);
144
+ ctx.drawImage(layer, x, y, drawW, drawH);
145
+ layer.close();
146
+ return canvas.transferToImageBitmap();
147
+ }
148
+ emptyBitmap = null;
149
+ async renderEmpty() {
150
+ if (this.emptyBitmap != null) return this.emptyBitmap;
151
+ const canvas = new OffscreenCanvas(this.opts.width, this.opts.height);
152
+ this.emptyBitmap = canvas.transferToImageBitmap();
153
+ return this.emptyBitmap;
154
+ }
155
+ async split(time) {
156
+ if (time <= 0 || time >= this.opts.durationUs) throw new Error("\"time\" out of bounds");
157
+ const left = new TextClip({
158
+ ...this.opts,
159
+ durationUs: time
160
+ });
161
+ const right = new TextClip({
162
+ ...this.opts,
163
+ durationUs: this.opts.durationUs - time,
164
+ animOffsetUs: this.opts.animOffsetUs + time
165
+ });
166
+ await Promise.all([left.ready, right.ready]);
167
+ return [left, right];
168
+ }
169
+ async clone() {
170
+ const copy = new TextClip({ ...this.opts });
171
+ await copy.ready;
172
+ return copy;
173
+ }
174
+ destroy() {
175
+ this.destroyed = true;
176
+ this.cachedBitmap?.close();
177
+ this.cachedBitmap = null;
178
+ this.emptyBitmap?.close();
179
+ this.emptyBitmap = null;
180
+ }
181
+ };
182
+ // src/engine/webav/clip-factory.ts
183
+ /**
184
+ * File → IClip 的分派工厂。
185
+ *
186
+ * ⚠️ 本文件是 `@webav/*` 的合法引用点之一(另一个是 core/webav 下的其他模块)。
187
+ * 不要在 features / store / components 中直接 import WebAV。
188
+ *
189
+ * 所有 clip 实例的生命周期由 `ClipRegistry` 统一管理,这里只负责创建。
190
+ */
191
+ /**
192
+ * 创建素材的「母版」clip 并读取 meta。
193
+ *
194
+ * 对静态图/文字/字幕,`meta.duration` 可能是 Infinity,这里会归一成有限值:
195
+ * 真正的时长由时间轴 clip 决定,母版只用于 clone 与抽帧。
196
+ */
197
+ async function createClipFromFile(file, kind) {
198
+ switch (kind) {
199
+ case "video": {
200
+ const clip = new _webav_av_cliper.MP4Clip(require_file_reader.fileToStream(file));
201
+ const meta = await clip.ready;
202
+ return {
203
+ clip,
204
+ meta: {
205
+ durationUs: meta.duration,
206
+ width: meta.width,
207
+ height: meta.height,
208
+ hasAudio: meta.audioChanCount > 0
209
+ }
210
+ };
211
+ }
212
+ case "audio": {
213
+ const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
214
+ return {
215
+ clip,
216
+ meta: {
217
+ durationUs: (await clip.ready).duration,
218
+ width: 0,
219
+ height: 0,
220
+ hasAudio: true
221
+ }
222
+ };
223
+ }
224
+ case "image": {
225
+ const clip = new _webav_av_cliper.ImgClip(require_file_reader.fileToStream(file));
226
+ const meta = await clip.ready;
227
+ return {
228
+ clip,
229
+ meta: {
230
+ durationUs: Number.isFinite(meta.duration) ? meta.duration : require_id.DEFAULT_STILL_DURATION_US,
231
+ width: meta.width,
232
+ height: meta.height,
233
+ hasAudio: false
234
+ }
235
+ };
236
+ }
237
+ case "subtitle": {
238
+ const text = await file.text();
239
+ const clip = new _webav_av_cliper.EmbedSubtitlesClip(text, {
240
+ videoWidth: 1280,
241
+ videoHeight: 720
242
+ });
243
+ const meta = await clip.ready;
244
+ return {
245
+ clip,
246
+ meta: {
247
+ durationUs: Number.isFinite(meta.duration) ? meta.duration : require_id.DEFAULT_STILL_DURATION_US,
248
+ width: meta.width,
249
+ height: meta.height,
250
+ hasAudio: false
251
+ }
252
+ };
253
+ }
254
+ default: throw new Error(`Unsupported asset kind: ${kind}`);
255
+ }
256
+ }
257
+ /** 文字素材:不走文件,直接由 TextClip 生成 */
258
+ function createTextClip(init) {
259
+ return new TextClip({
260
+ text: init.text,
261
+ style: init.style,
262
+ width: init.width,
263
+ height: init.height,
264
+ durationUs: init.durationUs
265
+ });
266
+ }
267
+ function createSubtitleClip(init) {
268
+ return new _webav_av_cliper.EmbedSubtitlesClip(init.srt, {
269
+ videoWidth: init.width,
270
+ videoHeight: init.height,
271
+ color: init.style.color,
272
+ textBgColor: init.style.textBgColor,
273
+ fontFamily: init.style.fontFamily,
274
+ fontSize: init.style.fontSize,
275
+ strokeStyle: init.style.strokeStyle,
276
+ lineWidth: init.style.lineWidth,
277
+ bottomOffset: init.style.bottomOffset
278
+ });
279
+ }
280
+ /** Worker 内重建 clip:只支持可序列化的素材类型 */
281
+ function createClipFromBuffer(buffer, kind, opts) {
282
+ switch (kind) {
283
+ case "video": return new _webav_av_cliper.MP4Clip(require_file_reader.bufferToStream(buffer));
284
+ case "audio": return new _webav_av_cliper.AudioClip(require_file_reader.bufferToStream(buffer));
285
+ case "image": return new _webav_av_cliper.ImgClip(require_file_reader.bufferToStream(buffer));
286
+ case "subtitle": return createSubtitleClip({
287
+ srt: opts?.srt ?? "",
288
+ style: opts?.subtitleStyle ?? DEFAULT_SUBTITLE_STYLE,
289
+ width: opts?.width ?? 1280,
290
+ height: opts?.height ?? 720
291
+ });
292
+ case "text": return createTextClip({
293
+ text: opts?.text ?? "",
294
+ style: opts?.textStyle ?? DEFAULT_TEXT_STYLE,
295
+ width: opts?.width ?? 1280,
296
+ height: opts?.height ?? 720,
297
+ durationUs: opts?.durationUs ?? 5e6
298
+ });
299
+ default: throw new Error(`Worker 内不支持重建素材类型:${kind}`);
300
+ }
301
+ }
10
302
  // src/engine/webav/offset-clip.ts
11
303
  var OffsetClip = class OffsetClip {
12
304
  ready;
@@ -22,7 +314,10 @@ var OffsetClip = class OffsetClip {
22
314
  height: 0,
23
315
  duration: opts.durationUs
24
316
  };
25
- this.ready = inner.ready.then((meta) => {
317
+ this.ready = inner.ready.then(async (meta) => {
318
+ try {
319
+ await inner.tick(opts.inPointUs);
320
+ } catch {}
26
321
  this.metaValue = {
27
322
  width: meta.width,
28
323
  height: meta.height,
@@ -96,10 +391,10 @@ var ClipRegistry = class {
96
391
  * 这两个类型无法 clone(clone 会带走旧的 duration / 内容),必须每次新建。
97
392
  */
98
393
  buildProceduralClip(params, asset) {
99
- if (asset.kind === "text") return require_import_pipeline.createTextClip({
394
+ if (asset.kind === "text") return createTextClip({
100
395
  text: params.text ?? asset.text ?? "文本",
101
396
  style: {
102
- ...require_import_pipeline.DEFAULT_TEXT_STYLE,
397
+ ...DEFAULT_TEXT_STYLE,
103
398
  ...asset.textStyle ?? {},
104
399
  ...params.textStyle ?? {}
105
400
  },
@@ -107,10 +402,10 @@ var ClipRegistry = class {
107
402
  height: params.projectHeight,
108
403
  durationUs: params.durationUs
109
404
  });
110
- return require_import_pipeline.createSubtitleClip({
405
+ return createSubtitleClip({
111
406
  srt: params.srt ?? asset.srt ?? "",
112
407
  style: {
113
- ...require_import_pipeline.DEFAULT_SUBTITLE_STYLE,
408
+ ...DEFAULT_SUBTITLE_STYLE,
114
409
  ...asset.subtitleStyle ?? {},
115
410
  ...params.subtitleStyle ?? {}
116
411
  },
@@ -131,7 +426,7 @@ var ClipRegistry = class {
131
426
  let clip;
132
427
  if (asset.kind === "text" || asset.kind === "subtitle") clip = this.buildProceduralClip(params, asset);
133
428
  else if (runtime.master) clip = await runtime.master.clone();
134
- else if (runtime.file) clip = (await require_import_pipeline.createClipFromFile(runtime.file, asset.kind)).clip;
429
+ else if (runtime.file) clip = (await createClipFromFile(runtime.file, asset.kind)).clip;
135
430
  else throw new Error(`素材缺少可用的数据源:${asset.name}`);
136
431
  const wrapped = params.inPointUs > 0 ? new OffsetClip(clip, {
137
432
  inPointUs: params.inPointUs,
@@ -177,7 +472,7 @@ var ClipRegistry = class {
177
472
  runtime?.master?.destroy();
178
473
  } catch {}
179
474
  this.assets.delete(assetId);
180
- require_import_pipeline.releaseThumbnails(assetId);
475
+ require_thumbnails.releaseThumbnails(assetId);
181
476
  }
182
477
  destroyAllInstances() {
183
478
  for (const [id] of this.derived) this.destroyInstance(id);
@@ -189,7 +484,7 @@ var ClipRegistry = class {
189
484
  try {
190
485
  runtime.master?.destroy();
191
486
  } catch {}
192
- require_import_pipeline.releaseThumbnails(assetId);
487
+ require_thumbnails.releaseThumbnails(assetId);
193
488
  }
194
489
  this.assets.clear();
195
490
  }
@@ -503,17 +798,23 @@ function createEditorRuntime() {
503
798
  }
504
799
  };
505
800
  }
801
+ // src/lib/error.ts
802
+ /**
803
+ * 异常 → 可展示文本。
804
+ *
805
+ * 各模块都会在 catch 里把 `unknown` 转成字符串,集中一处避免多份实现漂移。
806
+ */
807
+ function errorMessage(err) {
808
+ return err instanceof Error ? err.message : String(err);
809
+ }
506
810
  // src/editor/create-editor.ts
507
811
  function resolveBridge(bridge) {
508
812
  if (bridge != null) return bridge;
509
813
  return require_waveform.createWebBridge();
510
814
  }
511
815
  var loadImportFiles = () => Promise.resolve().then(() => import_files_exports);
512
- var loadImportPipeline = () => Promise.resolve().then(() => require("../chunks/import-pipeline-A8KkqtZT.cjs")).then((n) => n.import_pipeline_exports);
816
+ var loadImportPipeline = () => Promise.resolve().then(() => import_pipeline_exports);
513
817
  var loadExportRunner = () => Promise.resolve().then(() => export_runner_exports);
514
- function errorMessage$1(err) {
515
- return err instanceof Error ? err.message : String(err);
516
- }
517
818
  /**
518
819
  * 创建编辑器实例。
519
820
  *
@@ -546,7 +847,10 @@ function createEditor(options = {}) {
546
847
  let disposed = false;
547
848
  let lastPlaying = false;
548
849
  let capabilityPromise = null;
850
+ /** 防抖计时器:最后一次变更后等待 intervalMs 再写盘 */
549
851
  let autoSaveTimer = null;
852
+ /** 串行化写盘:自动保存与手动保存永不并发(并发会交错写同一 namespace) */
853
+ let saveChain = Promise.resolve({ ok: true });
550
854
  let prevProject = store.getState().project;
551
855
  let prevAssets = store.getState().assets;
552
856
  let prevSelection = store.getState().selectedClipIds;
@@ -565,11 +869,20 @@ function createEditor(options = {}) {
565
869
  ns: namespace
566
870
  };
567
871
  }
872
+ /**
873
+ * 自动保存调度(防抖)。
874
+ *
875
+ * 语义是「最后一次结构性变更后静置 intervalMs 才写盘」,而不是
876
+ * 「第一次变更后固定 intervalMs 写盘」:
877
+ * 后者在持续编辑(拖滑块、连续裁剪)时会中途写一次,既浪费 I/O,
878
+ * 又会在宿主展示「保存中」时产生无意义的闪烁。每次新变更都重置计时器。
879
+ */
568
880
  function scheduleAutoSave() {
569
- if (autoSave == null || disposed || autoSaveTimer != null) return;
881
+ if (autoSave == null || disposed) return;
882
+ if (autoSaveTimer != null) clearTimeout(autoSaveTimer);
570
883
  autoSaveTimer = setTimeout(() => {
571
884
  autoSaveTimer = null;
572
- editor.save().catch((err) => {
885
+ enqueueSave().catch((err) => {
573
886
  bus.emit("error", {
574
887
  scope: "auto-save",
575
888
  error: err
@@ -577,6 +890,40 @@ function createEditor(options = {}) {
577
890
  });
578
891
  }, autoSave.intervalMs ?? 2e3);
579
892
  }
893
+ /** 把一次保存排进队列,返回该次保存的结果 */
894
+ function enqueueSave() {
895
+ const run = saveChain.then(() => writeProject());
896
+ saveChain = run.catch(() => ({ ok: false }));
897
+ return run;
898
+ }
899
+ /** 真正写盘 + 广播 project:saved */
900
+ async function writeProject() {
901
+ const { port, ns } = saveTarget();
902
+ const projectAtStart = store.getState().project;
903
+ const result = await port.save(ns, editor.serialize());
904
+ const changedDuringSave = store.getState().project !== projectAtStart;
905
+ if (result.ok) {
906
+ if (!changedDuringSave) store.getState().markSaved();
907
+ else scheduleAutoSave();
908
+ bus.emit("project:saved", result.path != null ? {
909
+ ok: true,
910
+ path: result.path
911
+ } : { ok: true });
912
+ } else bus.emit("project:saved", {
913
+ ok: false,
914
+ ...result.error != null ? { error: result.error } : {},
915
+ ...result.path != null ? { path: result.path } : {}
916
+ });
917
+ return result;
918
+ }
919
+ /** 卸载 / 关闭前把待写的防抖任务立刻执行 */
920
+ function flushAutoSave() {
921
+ if (autoSaveTimer != null) {
922
+ clearTimeout(autoSaveTimer);
923
+ autoSaveTimer = null;
924
+ }
925
+ return enqueueSave();
926
+ }
580
927
  const unsubscribeStore = store.subscribe((state) => {
581
928
  if (disposed) return;
582
929
  if (state.project !== prevProject) {
@@ -585,11 +932,11 @@ function createEditor(options = {}) {
585
932
  project: state.project,
586
933
  dirty: state.dirty
587
934
  });
935
+ if (autoSave != null) scheduleAutoSave();
588
936
  }
589
937
  if (state.dirty !== prevDirty) {
590
938
  prevDirty = state.dirty;
591
939
  bus.emit("project:dirty", { dirty: state.dirty });
592
- if (state.dirty && autoSave != null) scheduleAutoSave();
593
940
  }
594
941
  if (state.assets !== prevAssets) {
595
942
  prevAssets = state.assets;
@@ -695,8 +1042,10 @@ function createEditor(options = {}) {
695
1042
  if (file == null) continue;
696
1043
  try {
697
1044
  const result = await importFile(runtime.clips, file, void 0, { id: asset.id });
1045
+ const { regenerateDerived } = await loadImportPipeline();
1046
+ const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
698
1047
  store.getState().replaceAsset(asset.id, {
699
- ...result.asset,
1048
+ ...withDerived,
700
1049
  id: asset.id
701
1050
  });
702
1051
  stillMissing.delete(asset.id);
@@ -718,20 +1067,7 @@ function createEditor(options = {}) {
718
1067
  return { missingAssetIds: remaining };
719
1068
  },
720
1069
  async save() {
721
- const { port, ns } = saveTarget();
722
- const result = await port.save(ns, editor.serialize());
723
- if (result.ok) {
724
- store.getState().markSaved();
725
- bus.emit("project:saved", result.path != null ? {
726
- ok: true,
727
- path: result.path
728
- } : { ok: true });
729
- } else bus.emit("project:saved", {
730
- ok: false,
731
- ...result.error != null ? { error: result.error } : {},
732
- ...result.path != null ? { path: result.path } : {}
733
- });
734
- return result;
1070
+ return enqueueSave();
735
1071
  },
736
1072
  async saveAs(nextStorage, nextNamespace) {
737
1073
  storage = nextStorage;
@@ -741,7 +1077,7 @@ function createEditor(options = {}) {
741
1077
  storage: nextStorage,
742
1078
  namespace: nextNamespace
743
1079
  };
744
- return editor.save();
1080
+ return enqueueSave();
745
1081
  },
746
1082
  isDirty: () => store.getState().dirty,
747
1083
  isEmpty: () => store.getState().project.clips.length === 0,
@@ -783,8 +1119,10 @@ function createEditor(options = {}) {
783
1119
  const { importFile } = await loadImportPipeline();
784
1120
  if (!store.getState().assets.some((a) => a.id === assetId)) throw new Error(`素材不存在:${assetId}`);
785
1121
  const result = await importFile(runtime.clips, file, void 0, { id: assetId });
1122
+ const { regenerateDerived } = await loadImportPipeline();
1123
+ const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
786
1124
  store.getState().replaceAsset(assetId, {
787
- ...result.asset,
1125
+ ...withDerived,
788
1126
  id: assetId
789
1127
  });
790
1128
  store.getState().clearMissing(assetId);
@@ -896,7 +1234,7 @@ function createEditor(options = {}) {
896
1234
  } catch (err) {
897
1235
  if (cancelled || err instanceof require_waveform.ExportCancelledError) throw err instanceof require_waveform.ExportCancelledError ? err : new require_waveform.ExportCancelledError();
898
1236
  bus.emit("export:error", {
899
- message: errorMessage$1(err),
1237
+ message: errorMessage(err),
900
1238
  error: err
901
1239
  });
902
1240
  throw err;
@@ -916,7 +1254,7 @@ function createEditor(options = {}) {
916
1254
  } catch (err) {
917
1255
  return {
918
1256
  ok: false,
919
- error: errorMessage$1(err)
1257
+ error: errorMessage(err)
920
1258
  };
921
1259
  }
922
1260
  },
@@ -927,18 +1265,229 @@ function createEditor(options = {}) {
927
1265
  async dispose() {
928
1266
  if (disposed) return;
929
1267
  disposed = true;
930
- if (autoSaveTimer != null) {
931
- clearTimeout(autoSaveTimer);
932
- autoSaveTimer = null;
933
- }
1268
+ const pending = flushAutoSave();
934
1269
  unsubscribeStore();
935
1270
  bus.emit("dispose", {});
936
1271
  await runtime.dispose();
1272
+ try {
1273
+ await pending;
1274
+ } catch {}
937
1275
  bus.clear();
938
1276
  }
939
1277
  };
940
1278
  return editor;
941
1279
  }
1280
+ // src/engine/webav/import-pipeline.ts
1281
+ /**
1282
+ * 素材导入流水线(供 feature 与 Lab 共用)。
1283
+ *
1284
+ * 步骤:校验类型 → 创建母版 IClip → 读 meta → 抽缩略图 / 波形 → 写入 registry 与 store。
1285
+ *
1286
+ * ⚠️ 所有 WebAV 调用都落在 core 内;本文件位于 core/webav 下,允许 import '@webav/*'。
1287
+ */
1288
+ var import_pipeline_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
1289
+ UnsupportedFileError: () => require_file_reader.UnsupportedFileError,
1290
+ createSubtitleAsset: () => createSubtitleAsset,
1291
+ createTextAsset: () => createTextAsset,
1292
+ importFile: () => importFile,
1293
+ loadAudioClipForAsset: () => loadAudioClipForAsset,
1294
+ loadPcmForAsset: () => loadPcmForAsset,
1295
+ regenerateDerived: () => regenerateDerived
1296
+ });
1297
+ var THUMBNAIL_COUNT = 4;
1298
+ var THUMBNAIL_WIDTH = 120;
1299
+ var WAVEFORM_BUCKETS = 400;
1300
+ /**
1301
+ * 导入单个文件,生成素材记录。
1302
+ *
1303
+ * @param onProgress 0~1 进度(用于卡片骨架/进度条)
1304
+ * @throws UnsupportedFileError 类型不支持时
1305
+ */
1306
+ async function importFile(registry, file, onProgress, options = {}) {
1307
+ const detected = require_file_reader.detectFileType(file);
1308
+ onProgress?.(.1);
1309
+ const { clip, meta } = await createClipFromFile(file, detected.kind);
1310
+ onProgress?.(.4);
1311
+ const assetId = options.id ?? require_id.createId("asset");
1312
+ const asset = {
1313
+ id: assetId,
1314
+ name: file.name,
1315
+ kind: detected.kind,
1316
+ mimeType: detected.mimeType,
1317
+ size: file.size,
1318
+ durationUs: require_time.asUs(meta.durationUs),
1319
+ width: meta.width,
1320
+ height: meta.height,
1321
+ hasAudio: meta.hasAudio,
1322
+ createdAt: Date.now()
1323
+ };
1324
+ if (detected.kind === "video") {
1325
+ try {
1326
+ asset.thumbnailUrls = (await require_thumbnails.generateThumbnails(clip, assetId, THUMBNAIL_COUNT, meta.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
1327
+ } catch (err) {
1328
+ console.warn("[importFile] 缩略图生成失败", err);
1329
+ }
1330
+ onProgress?.(.7);
1331
+ }
1332
+ if (detected.kind === "audio" || detected.kind === "video" && meta.hasAudio) {
1333
+ try {
1334
+ if (detected.kind === "audio") {
1335
+ const pcm = clip.getPCMData();
1336
+ asset.waveform = require_waveform.computeWaveform(pcm, WAVEFORM_BUCKETS);
1337
+ }
1338
+ } catch (err) {
1339
+ console.warn("[importFile] 波形生成失败", err);
1340
+ }
1341
+ onProgress?.(.85);
1342
+ }
1343
+ registry.registerAsset(asset, file, clip);
1344
+ onProgress?.(1);
1345
+ return {
1346
+ asset,
1347
+ clipId: assetId,
1348
+ master: clip
1349
+ };
1350
+ }
1351
+ /**
1352
+ * 为一个「已有元数据」的素材重新生成派生数据(缩略图 / 波形)。
1353
+ *
1354
+ * 用于重新打开工程后 `resolveFile` / `reconnectAsset` 找回文件时:
1355
+ * 元数据里不含 thumbnailUrls(那是会话内 object URL,刷新即失效),
1356
+ * 如果不补这一步,素材卡片会一直显示图标占位 —— 用户看到的就是「资源坏了」。
1357
+ *
1358
+ * 失败只记警告:派生数据缺失不影响素材可用性。
1359
+ */
1360
+ async function regenerateDerived(registry, asset, file) {
1361
+ const master = registry.getMaster(asset.id);
1362
+ if (master == null) return asset;
1363
+ const next = { ...asset };
1364
+ if (asset.kind === "video") {
1365
+ const { regenerateThumbnails } = await Promise.resolve().then(() => require("../chunks/thumbnails-DEjnao5r.cjs")).then((n) => n.thumbnails_exports);
1366
+ try {
1367
+ next.thumbnailUrls = (await regenerateThumbnails(master, asset.id, THUMBNAIL_COUNT, asset.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
1368
+ } catch (err) {
1369
+ console.warn("[regenerateDerived] 缩略图生成失败", asset.id, err);
1370
+ }
1371
+ }
1372
+ if (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio) try {
1373
+ if (asset.kind === "audio") {
1374
+ const pcm = master.getPCMData();
1375
+ next.waveform = require_waveform.computeWaveform(pcm, WAVEFORM_BUCKETS);
1376
+ }
1377
+ } catch (err) {
1378
+ console.warn("[regenerateDerived] 波形生成失败", asset.id, err);
1379
+ }
1380
+ return next;
1381
+ }
1382
+ /** 新建文字素材(非文件来源,无需二进制) */
1383
+ function createTextAsset(params) {
1384
+ const assetId = params.id ?? require_id.createId("asset");
1385
+ const style = {
1386
+ ...DEFAULT_TEXT_STYLE,
1387
+ ...params.style
1388
+ };
1389
+ const durationUs = params.durationUs ?? 5e6;
1390
+ const master = createTextClip({
1391
+ text: params.text,
1392
+ style,
1393
+ width: params.width,
1394
+ height: params.height,
1395
+ durationUs
1396
+ });
1397
+ const asset = {
1398
+ id: assetId,
1399
+ name: params.text.slice(0, 24) || "文字",
1400
+ kind: "text",
1401
+ mimeType: "text/x-title",
1402
+ size: 0,
1403
+ durationUs: require_time.asUs(durationUs),
1404
+ width: params.width,
1405
+ height: params.height,
1406
+ hasAudio: false,
1407
+ createdAt: Date.now(),
1408
+ text: params.text,
1409
+ textStyle: style
1410
+ };
1411
+ params.registry.registerAsset(asset, null, master);
1412
+ return {
1413
+ asset,
1414
+ clipId: assetId
1415
+ };
1416
+ }
1417
+ /**
1418
+ * 解码素材的完整 PCM(48kHz 双声道),供 Worker 导出使用。
1419
+ *
1420
+ * ⚠️ 实测:Worker 内没有 AudioContext / OfflineAudioContext,
1421
+ * `new AudioClip(stream)` 会在 `decodeAudioData` 处抛错,因此必须在主线程解码后
1422
+ * 把 Float32Array 传过去(`new AudioClip(Float32Array[])` 走纯归一化分支)。
1423
+ *
1424
+ * @returns 声道数组;失败或素材无音轨时返回 undefined
1425
+ */
1426
+ async function loadPcmForAsset(registry, assetId) {
1427
+ if (registry.getAsset(assetId) == null) return void 0;
1428
+ const file = registry.getFile(assetId);
1429
+ if (file == null) return void 0;
1430
+ try {
1431
+ const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
1432
+ await clip.ready;
1433
+ const pcm = clip.getPCMData().map((channel) => new Float32Array(channel));
1434
+ clip.destroy();
1435
+ return pcm;
1436
+ } catch (err) {
1437
+ console.warn("[loadPcmForAsset] 音频解码失败", err);
1438
+ return;
1439
+ }
1440
+ }
1441
+ /** 解析素材的音频 PCM(供预览音频引擎注册) */
1442
+ async function loadAudioClipForAsset(registry, assetId) {
1443
+ const file = registry.getFile(assetId);
1444
+ if (file == null) return null;
1445
+ const asset = registry.getAsset(assetId);
1446
+ if (asset == null) return null;
1447
+ if (asset.kind !== "audio" && asset.kind !== "video") return null;
1448
+ const clip = new _webav_av_cliper.AudioClip(require_file_reader.fileToStream(file));
1449
+ await clip.ready;
1450
+ return clip;
1451
+ }
1452
+ /** 用第一条字幕文本给素材命名(无内容时回退为「字幕」) */
1453
+ function subtitleName(srt) {
1454
+ const base = (srt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0 && !line.includes("-->") && !/^\d+$/.test(line)) ?? "") || "字幕";
1455
+ return base.length > 20 ? `${base.slice(0, 20)}…` : base;
1456
+ }
1457
+ /** 新建字幕素材(内容来自粘贴的 SRT,无源文件) */
1458
+ function createSubtitleAsset(params) {
1459
+ const assetId = params.id ?? require_id.createId("asset");
1460
+ const style = {
1461
+ ...DEFAULT_SUBTITLE_STYLE,
1462
+ ...params.style
1463
+ };
1464
+ const durationUs = params.durationUs ?? 5e6;
1465
+ const master = createSubtitleClip({
1466
+ srt: params.srt,
1467
+ style,
1468
+ width: params.width,
1469
+ height: params.height
1470
+ });
1471
+ const asset = {
1472
+ id: assetId,
1473
+ name: subtitleName(params.srt),
1474
+ kind: "subtitle",
1475
+ mimeType: "text/plain",
1476
+ size: params.srt.length,
1477
+ durationUs: require_time.asUs(durationUs),
1478
+ width: params.width,
1479
+ height: params.height,
1480
+ hasAudio: false,
1481
+ createdAt: Date.now(),
1482
+ srt: params.srt,
1483
+ subtitleStyle: style
1484
+ };
1485
+ params.registry.registerAsset(asset, null, master);
1486
+ return {
1487
+ asset,
1488
+ clipId: assetId
1489
+ };
1490
+ }
942
1491
  // src/engine/webav/import-files.ts
943
1492
  var import_files_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ importFiles: () => importFiles });
944
1493
  /** 找出可以「重连」的素材 id(同名 + 同类型 + 当前确实缺少二进制) */
@@ -946,9 +1495,6 @@ function findReconnectableId(state, registry, file, kind) {
946
1495
  if (state.missingAssetIds.length === 0) return null;
947
1496
  return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.kind === kind && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
948
1497
  }
949
- function errorMessage(err) {
950
- return err instanceof Error ? err.message : String(err);
951
- }
952
1498
  /**
953
1499
  * 批量导入素材。
954
1500
  *
@@ -971,7 +1517,7 @@ async function importFiles(registry, store, files, options = {}) {
971
1517
  const requestedId = reconnectId ?? idFor?.(file, index) ?? null;
972
1518
  const assetId = requestedId != null && requestedId !== "" ? requestedId : void 0;
973
1519
  if (reconnectId != null) registry.removeAsset(reconnectId);
974
- const result = await require_import_pipeline.importFile(registry, file, (progress) => onProgress?.(index, progress), assetId != null ? { id: assetId } : void 0);
1520
+ const result = await importFile(registry, file, (progress) => onProgress?.(index, progress), assetId != null ? { id: assetId } : void 0);
975
1521
  if (reconnectId != null) {
976
1522
  store.getState().replaceAsset(reconnectId, result.asset);
977
1523
  store.getState().clearMissing(reconnectId);
@@ -1190,11 +1736,12 @@ function mapClipToOutput(clip, source, target) {
1190
1736
  * @param instance 该 clip 独占的 IClip
1191
1737
  * @param clip 时间轴数据(提供 volume / fadeIn / fadeOut)
1192
1738
  */
1193
- function withClipGain(instance, clip) {
1194
- if (!(clip.muted || clip.volume !== 1 || clip.fadeInUs > 0 || clip.fadeOutUs > 0)) return instance;
1739
+ function withClipGain(instance, clip, options = {}) {
1740
+ const volume = options.trackMuted === true ? 0 : clip.volume;
1741
+ if (!(clip.muted || options.trackMuted === true || volume !== 1 || clip.fadeInUs > 0 || clip.fadeOutUs > 0)) return instance;
1195
1742
  if (clip.muted) return instance;
1196
1743
  return new GainClip(instance, {
1197
- volume: clip.volume,
1744
+ volume,
1198
1745
  fadeInUs: clip.fadeInUs,
1199
1746
  fadeOutUs: clip.fadeOutUs,
1200
1747
  durationUs: clip.durationUs,
@@ -1242,7 +1789,7 @@ async function buildCombinator(inputs, opts) {
1242
1789
  const { instance, track } = item;
1243
1790
  const clip = mapClipToOutput(item.clip, sourceSize, targetSize);
1244
1791
  if (track.kind === "audio" && !opts.includeAudio) continue;
1245
- if (track.muted) continue;
1792
+ const trackMuted = track.muted;
1246
1793
  if (clip.muted) continue;
1247
1794
  const duration = require_canvas_size.clampStillDuration(clip.durationUs);
1248
1795
  if (clip.durationUs <= 0) {
@@ -1256,7 +1803,7 @@ async function buildCombinator(inputs, opts) {
1256
1803
  ...clip,
1257
1804
  durationUs: duration
1258
1805
  };
1259
- const sprite = createOffscreenSprite(withClipGain(instance, normalized), normalized, { zIndex });
1806
+ const sprite = createOffscreenSprite(withClipGain(instance, normalized, { trackMuted }), normalized, { zIndex });
1260
1807
  applyOpacityFade(sprite, normalized);
1261
1808
  await combinator.addSprite(sprite, { main: false });
1262
1809
  maxTimeUs = Math.max(maxTimeUs, clip.startUs + duration);
@@ -1363,7 +1910,7 @@ async function buildWorkerPayload(registry, project, assets, settings, maxTimeUs
1363
1910
  continue;
1364
1911
  }
1365
1912
  let pcm;
1366
- if (settings.includeAudio && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio)) pcm = await require_import_pipeline.loadPcmForAsset(registry, asset.id);
1913
+ if (settings.includeAudio && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio)) pcm = await loadPcmForAsset(registry, asset.id);
1367
1914
  payloadAssets.push({
1368
1915
  id: asset.id,
1369
1916
  kind: asset.kind,
@@ -1396,6 +1943,12 @@ async function buildWorkerPayload(registry, project, assets, settings, maxTimeUs
1396
1943
  includeAudio: settings.includeAudio,
1397
1944
  bgColor: "#000000",
1398
1945
  maxTimeUs,
1946
+ tracks: project.tracks.map((t) => ({
1947
+ id: t.id,
1948
+ kind: t.kind,
1949
+ hidden: t.hidden,
1950
+ muted: t.muted
1951
+ })),
1399
1952
  clips
1400
1953
  },
1401
1954
  missing
@@ -1670,6 +2223,7 @@ var SpriteSynchronizer = class {
1670
2223
  applyClipToSprite(existing, clip);
1671
2224
  existing.zIndex = zIndexOf(project.tracks, clip.trackId, clip);
1672
2225
  existing.visible = !track.hidden;
2226
+ if (previous == null || previous.durationUs !== clip.durationUs || previous.fadeInUs !== clip.fadeInUs || previous.fadeOutUs !== clip.fadeOutUs || previous.opacity !== clip.opacity) applyOpacityFade(existing, clip);
1673
2227
  this.lastClips.set(clip.id, clip);
1674
2228
  continue;
1675
2229
  }
@@ -1679,7 +2233,7 @@ var SpriteSynchronizer = class {
1679
2233
  this.registry.destroyInstance(clip.id);
1680
2234
  }
1681
2235
  try {
1682
- const instance = await this.registry.createInstance({
2236
+ const sprite = createVisibleSprite(await this.registry.createInstance({
1683
2237
  timelineClipId: clip.id,
1684
2238
  assetId: clip.assetId,
1685
2239
  inPointUs: clip.inPointUs,
@@ -1690,9 +2244,9 @@ var SpriteSynchronizer = class {
1690
2244
  subtitleStyle: clip.subtitleStyle,
1691
2245
  projectWidth: project.width,
1692
2246
  projectHeight: project.height
1693
- });
1694
- const sprite = createVisibleSprite(track.kind === "audio" ? instance : instance, clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
2247
+ }), clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
1695
2248
  sprite.visible = !track.hidden;
2249
+ applyOpacityFade(sprite, clip);
1696
2250
  if (track.kind === "audio" && asset.kind === "audio") {
1697
2251
  sprite.rect.w = 0;
1698
2252
  sprite.rect.h = 0;
@@ -2063,6 +2617,16 @@ var cancelled = false;
2063
2617
  function toStream(blob) {
2064
2618
  return blob.stream();
2065
2619
  }
2620
+ /** 轨道静音:音量置 0(画面不受影响) */
2621
+ function withSilence(clip) {
2622
+ return new GainClip(clip, {
2623
+ volume: 0,
2624
+ fadeInUs: 0,
2625
+ fadeOutUs: 0,
2626
+ durationUs: Number.MAX_SAFE_INTEGER,
2627
+ playbackRate: 1
2628
+ });
2629
+ }
2066
2630
  /** 音量 / 淡入淡出包装(与主线程 exporter.withClipGain 保持一致的语义) */
2067
2631
  function withGain(clip, timeline) {
2068
2632
  if (timeline.volume === 1 && timeline.fadeInUs === 0 && timeline.fadeOutUs === 0) return clip;
@@ -2125,6 +2689,7 @@ async function buildClipInWorker(asset) {
2125
2689
  */
2126
2690
  async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2127
2691
  const assetMap = new Map(assets.map((a) => [a.id, a]));
2692
+ const trackMap = new Map((timeline.tracks ?? []).map((t) => [t.id, t]));
2128
2693
  const combinator = new _webav_av_cliper.Combinator({
2129
2694
  width: timeline.width,
2130
2695
  height: timeline.height,
@@ -2133,7 +2698,8 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2133
2698
  bgColor: timeline.bgColor,
2134
2699
  audio: timeline.includeAudio ? void 0 : false
2135
2700
  });
2136
- const clipCache = /* @__PURE__ */ new Map();
2701
+ const masterCache = /* @__PURE__ */ new Map();
2702
+ const derived = [];
2137
2703
  const skipped = [];
2138
2704
  const sourceSize = {
2139
2705
  width: timeline.projectWidth ?? timeline.width,
@@ -2165,11 +2731,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2165
2731
  });
2166
2732
  continue;
2167
2733
  }
2734
+ const track = trackMap.get(item.clip.trackId);
2735
+ if (track?.hidden === true) continue;
2168
2736
  if (item.clip.muted) continue;
2169
- let clip = clipCache.get(asset.id);
2170
- if (clip == null) try {
2171
- clip = await buildClipInWorker(asset);
2172
- clipCache.set(asset.id, clip);
2737
+ if (item.kind === "audio" && !timeline.includeAudio) continue;
2738
+ let master = masterCache.get(asset.id);
2739
+ if (master == null) try {
2740
+ master = await buildClipInWorker(asset);
2741
+ masterCache.set(asset.id, master);
2173
2742
  } catch (err) {
2174
2743
  const reason = err instanceof Error ? err.message : String(err);
2175
2744
  console.warn("[export.worker] clip skipped:", item.clip.id, reason);
@@ -2180,7 +2749,24 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2180
2749
  continue;
2181
2750
  }
2182
2751
  const tc = item.clip;
2183
- const sprite = new _webav_av_cliper.OffscreenSprite(withGain(clip, tc));
2752
+ let instance;
2753
+ try {
2754
+ instance = await master.clone();
2755
+ } catch (err) {
2756
+ const reason = err instanceof Error ? err.message : String(err);
2757
+ skipped.push({
2758
+ clipId: tc.id,
2759
+ reason
2760
+ });
2761
+ continue;
2762
+ }
2763
+ if (tc.inPointUs > 0) instance = new OffsetClip(instance, {
2764
+ inPointUs: tc.inPointUs,
2765
+ durationUs: tc.durationUs * tc.playbackRate
2766
+ });
2767
+ derived.push(instance);
2768
+ const gainInput = track?.muted === true ? withSilence(instance) : instance;
2769
+ const sprite = new _webav_av_cliper.OffscreenSprite(withGain(gainInput, tc));
2184
2770
  const mapped = mapTransformToOutput(tc.transform, sourceSize, targetSize);
2185
2771
  sprite.rect.x = mapped.x;
2186
2772
  sprite.rect.y = mapped.y;
@@ -2195,6 +2781,7 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2195
2781
  duration: tc.durationUs,
2196
2782
  playbackRate: tc.playbackRate
2197
2783
  };
2784
+ applyOpacityFade(sprite, tc);
2198
2785
  zIndex += 1;
2199
2786
  maxTimeUs = Math.max(maxTimeUs, tc.startUs + tc.durationUs);
2200
2787
  await combinator.addSprite(sprite, { main: false });
@@ -2216,10 +2803,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2216
2803
  skipped
2217
2804
  };
2218
2805
  } finally {
2219
- for (const clip of clipCache.values()) try {
2806
+ for (const clip of derived) try {
2807
+ clip.destroy();
2808
+ } catch {}
2809
+ derived.length = 0;
2810
+ for (const clip of masterCache.values()) try {
2220
2811
  clip.destroy();
2221
2812
  } catch {}
2222
- clipCache.clear();
2813
+ masterCache.clear();
2223
2814
  try {
2224
2815
  combinator.destroy();
2225
2816
  } catch {}
@@ -2258,8 +2849,8 @@ function handleWorkerMessage(event) {
2258
2849
  });
2259
2850
  }
2260
2851
  exports.ClipRegistry = ClipRegistry;
2261
- exports.DEFAULT_SUBTITLE_STYLE = require_import_pipeline.DEFAULT_SUBTITLE_STYLE;
2262
- exports.DEFAULT_TEXT_STYLE = require_import_pipeline.DEFAULT_TEXT_STYLE;
2852
+ exports.DEFAULT_SUBTITLE_STYLE = DEFAULT_SUBTITLE_STYLE;
2853
+ exports.DEFAULT_TEXT_STYLE = DEFAULT_TEXT_STYLE;
2263
2854
  exports.ENGINE_DEPENDENCIES_INSTALL_COMMAND = require_waveform.ENGINE_DEPENDENCIES_INSTALL_COMMAND;
2264
2855
  exports.GainClip = GainClip;
2265
2856
  exports.OffsetClip = OffsetClip;
@@ -2268,7 +2859,7 @@ exports.PlaybackClock = PlaybackClock;
2268
2859
  exports.PreviewAudioEngine = PreviewAudioEngine;
2269
2860
  exports.PreviewCanvas = PreviewCanvas;
2270
2861
  exports.SpriteSynchronizer = SpriteSynchronizer;
2271
- exports.TextClip = require_import_pipeline.TextClip;
2862
+ exports.TextClip = TextClip;
2272
2863
  exports.UnsupportedFileError = require_file_reader.UnsupportedFileError;
2273
2864
  exports.WorkerUnsupportedClipError = WorkerUnsupportedClipError;
2274
2865
  exports.applyClipToSprite = applyClipToSprite;
@@ -2280,33 +2871,34 @@ exports.canUseWorker = canUseWorker;
2280
2871
  exports.checkExportSupport = checkExportSupport;
2281
2872
  exports.computeExportDuration = computeExportDuration;
2282
2873
  exports.computeTimelineEnd = computeTimelineEnd;
2283
- exports.createClipFromBuffer = require_import_pipeline.createClipFromBuffer;
2284
- exports.createClipFromFile = require_import_pipeline.createClipFromFile;
2874
+ exports.createClipFromBuffer = createClipFromBuffer;
2875
+ exports.createClipFromFile = createClipFromFile;
2285
2876
  exports.createEditor = createEditor;
2286
2877
  exports.createEditorRuntime = createEditorRuntime;
2287
2878
  exports.createEditorStore = require_waveform.createEditorStore;
2288
2879
  exports.createEventBus = require_waveform.createEventBus;
2289
2880
  exports.createHistoryStore = require_waveform.createHistoryStore;
2290
2881
  exports.createOffscreenSprite = createOffscreenSprite;
2291
- exports.createSubtitleAsset = require_import_pipeline.createSubtitleAsset;
2292
- exports.createSubtitleClip = require_import_pipeline.createSubtitleClip;
2293
- exports.createTextAsset = require_import_pipeline.createTextAsset;
2294
- exports.createTextClip = require_import_pipeline.createTextClip;
2882
+ exports.createSubtitleAsset = createSubtitleAsset;
2883
+ exports.createSubtitleClip = createSubtitleClip;
2884
+ exports.createTextAsset = createTextAsset;
2885
+ exports.createTextClip = createTextClip;
2295
2886
  exports.createVisibleSprite = createVisibleSprite;
2296
- exports.cssFromStyle = require_import_pipeline.cssFromStyle;
2887
+ exports.cssFromStyle = cssFromStyle;
2297
2888
  exports.detectDependencies = require_waveform.detectDependencies;
2298
2889
  exports.detectEnvironment = require_waveform.detectEnvironment;
2299
2890
  exports.detectOpfs = require_waveform.detectOpfs;
2300
2891
  exports.exportProject = exportProject;
2301
2892
  exports.fixFMP4DurationStream = fixFMP4DurationStream;
2302
2893
  exports.handleWorkerMessage = handleWorkerMessage;
2303
- exports.importFile = require_import_pipeline.importFile;
2894
+ exports.importFile = importFile;
2304
2895
  exports.importFiles = importFiles;
2305
2896
  exports.isEnvironmentUsable = require_waveform.isEnvironmentUsable;
2306
- exports.loadAudioClipForAsset = require_import_pipeline.loadAudioClipForAsset;
2307
- exports.loadPcmForAsset = require_import_pipeline.loadPcmForAsset;
2897
+ exports.loadAudioClipForAsset = loadAudioClipForAsset;
2898
+ exports.loadPcmForAsset = loadPcmForAsset;
2308
2899
  exports.mapClipToOutput = mapClipToOutput;
2309
2900
  exports.mapTransformToOutput = mapTransformToOutput;
2901
+ exports.regenerateDerived = regenerateDerived;
2310
2902
  exports.runExportInWorker = runExportInWorker;
2311
2903
  exports.selectAssetById = require_waveform.selectAssetById;
2312
2904
  exports.selectAssets = require_waveform.selectAssets;