dsh-drawai 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 (45) hide show
  1. package/LICENSE +430 -418
  2. package/README.md +96 -249
  3. package/lib/audio-media.js +5 -2
  4. package/lib/client.js +45 -12
  5. package/lib/drawing-runner.js +6 -4
  6. package/lib/ffmpeg-manager.js +1056 -0
  7. package/lib/index.js +7719 -7396
  8. package/lib/motion/apply.js +4 -3
  9. package/lib/motion/asset-pack.js +394 -335
  10. package/lib/motion/assets.js +4 -3
  11. package/lib/motion/beats.js +3 -2
  12. package/lib/motion/collect.js +3 -1
  13. package/lib/motion/delivery.js +29 -1
  14. package/lib/motion/demo-assets-lock.js +10 -0
  15. package/lib/motion/ffmpeg.js +5 -4
  16. package/lib/motion/jobs.js +5 -1
  17. package/lib/motion/mgstyles/vendor/README.md +28 -0
  18. package/lib/motion/mgstyles/vendor/modules/d3/LICENSE +13 -0
  19. package/lib/motion/mgstyles/vendor/modules/d3/dist/d3.min.js +2 -0
  20. package/lib/motion/mgstyles/vendor/modules/opentype.js/LICENSE +20 -0
  21. package/lib/motion/mgstyles/vendor/modules/opentype.js/dist/opentype.mjs +14460 -0
  22. package/lib/motion/mgstyles/vendor/modules/simplex-noise/LICENSE +21 -0
  23. package/lib/motion/mgstyles/vendor/modules/simplex-noise/dist/esm/simplex-noise.js +466 -0
  24. package/lib/motion/mgstyles/vendor/modules/topojson-client/LICENSE +13 -0
  25. package/lib/motion/mgstyles/vendor/modules/topojson-client/dist/topojson-client.min.js +2 -0
  26. package/lib/motion/preview.js +13 -4
  27. package/lib/motion/render.js +179 -159
  28. package/lib/motion/runtime/package-lock.json +3058 -0
  29. package/lib/motion/runtime/package.json +16 -0
  30. package/lib/motion/style-frames.js +306 -268
  31. package/lib/motion/sync.js +214 -214
  32. package/lib/motion/video-runtime.js +192 -0
  33. package/lib/motion-capture.js +2 -1
  34. package/lib/motion-film-review.js +2 -2
  35. package/lib/motion-project.js +105 -79
  36. package/lib/motion-render-worker.js +174 -144
  37. package/lib/motion-vision.js +3 -2
  38. package/lib/routing/router.js +222 -211
  39. package/lib/routing/vendor/README.md +96 -96
  40. package/lib/routing/vendor/libavoid-routing.js +753 -753
  41. package/lib/routing/vendor/libavoid.min.js +59 -59
  42. package/lib/shotcard-reference.js +2 -2
  43. package/lib/style-kernel.js +16 -6
  44. package/lib/video-export.js +32 -20
  45. package/package.json +17 -12
@@ -4,7 +4,7 @@ import { join } from 'node:path'
4
4
  import { createHash } from 'node:crypto'
5
5
  import { execFile } from 'node:child_process'
6
6
  import { promisify } from 'node:util'
7
- import { ffmpegBinary } from './video-export.js'
7
+ import { ensureFfmpeg } from './ffmpeg-manager.js'
8
8
  import { motionCardContract } from './motion-library.js'
9
9
  const exec = promisify(execFile)
10
10
 
@@ -30,7 +30,7 @@ export async function prepareReferenceFrames(composition, directory, options = {
30
30
  }
31
31
  writeFileSync(video + '.tmp', Buffer.concat(chunks)); renameSync(video + '.tmp', video)
32
32
  }
33
- const binary = options.ffmpegPath || ffmpegBinary()
33
+ const binary = options.ffmpegPath || await ensureFfmpeg({ signal: options.signal })
34
34
  let info = ''
35
35
  try { const probe = await exec(binary, ['-hide_banner', '-i', video], { windowsHide: true, timeout: 15000, signal: options.signal }); info = probe.stderr }
36
36
  catch (error) { info = error.stderr || ''; if (!info) throw error }
@@ -152,9 +152,16 @@ var DASH_PRESETS = {
152
152
  /** 线型枚举顺序。 */
153
153
  var DASH_KINDS = ['solid', 'dashed', 'dotted']
154
154
 
155
- /** 箭头枚举 → style 键。none 是缺省(drawio 里键缺省就不画箭头),落盘即删键。 */
155
+ /**
156
+ * 箭头枚举 → style 键。
157
+ *
158
+ * `none` 必须**显式写 `endArrow=none`**:drawio 的 defaultEdge 自带
159
+ * `endArrow=classic`(`mxStylesheet.createDefaultEdgeStyle`),删键只会让这条边
160
+ * 回落到缺省,箭头反而又长出来。`=none` 才是在合并后的样式里把这个键删掉
161
+ * (`mxStylesheet.getCellStyle`:value 为 `none` 时 delete),从而真的不画。
162
+ */
156
163
  var ARROW_PRESETS = {
157
- none: null,
164
+ none: { endArrow: 'none' },
158
165
  end: { endArrow: 'classic' },
159
166
  both: { endArrow: 'classic', startArrow: 'classic' },
160
167
  start: { startArrow: 'classic' },
@@ -655,7 +662,10 @@ function arrowValueIsSet(value) {
655
662
 
656
663
  /** style → 箭头枚举。 */
657
664
  function arrowFromStyle(style) {
658
- var end = arrowValueIsSet(styleGet(style, 'endArrow', null))
665
+ // 按 drawio 的**有效样式**算:mxStylesheet.getCellStyle 会先把 defaultEdge 合进来,
666
+ // 而 defaultEdge 自带 endArrow=classic —— 所以缺键不是"没有箭头",是有 classic。
667
+ // startArrow 不在 defaultEdge 里,缺键才是真的没有。
668
+ var end = arrowValueIsSet(styleGet(style, 'endArrow', 'classic'))
659
669
  var start = arrowValueIsSet(styleGet(style, 'startArrow', null))
660
670
  if (end && start) return 'both'
661
671
  if (end) return 'end'
@@ -663,12 +673,12 @@ function arrowFromStyle(style) {
663
673
  return 'none'
664
674
  }
665
675
 
666
- /** 箭头枚举 → style。none 即删键(= drawio 缺省不画)。 */
676
+ /** 箭头枚举 → style。none 写 `endArrow=none`;null/undefined 才是"删键回落到 drawio 缺省"。 */
667
677
  function styleWithArrow(style, kind) {
668
678
  var cleared = stylePatch(style, { endArrow: null, startArrow: null })
669
- if (kind === null || kind === undefined || kind === 'none') return cleared
679
+ if (kind === null || kind === undefined) return cleared
670
680
  var preset = ARROW_PRESETS[kind]
671
- if (preset === undefined || preset === null) return cleared
681
+ if (preset === undefined) return cleared
672
682
  return stylePatch(cleared, preset)
673
683
  }
674
684
 
@@ -6,9 +6,9 @@ import { Worker } from 'node:worker_threads'
6
6
  import { copyFile } from 'node:fs/promises'
7
7
  import { existsSync, mkdirSync, mkdtempSync, rmSync, renameSync, statSync, openSync, readSync, closeSync, realpathSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
8
8
  import { join, relative, isAbsolute, extname, dirname, basename } from 'node:path'
9
- import { fileURLToPath } from 'node:url'
10
9
  import { clipDirFor, readManifest, findSourceFile, sanitizeClipId } from './media.js'
11
10
  import { createPerformanceLog } from './performance-log.js'
11
+ import { ensureFfmpeg, ffmpegPathSync } from './ffmpeg-manager.js'
12
12
 
13
13
  function runFfmpeg(binary, args, signal, progress) {
14
14
  return new Promise((resolve, reject) => {
@@ -36,17 +36,14 @@ function runFfmpeg(binary, args, signal, progress) {
36
36
  })
37
37
  }
38
38
 
39
+ /** 同步近似值,只给「还没异步准备过」的旧路径兜底;真正导出走 ensureFfmpeg。 */
39
40
  export function ffmpegBinary() {
40
- if (process.env.DRAWAI_FFMPEG_PATH) return process.env.DRAWAI_FFMPEG_PATH
41
- // Development checkout only; published packages use the configured binary or PATH.
42
- const local = fileURLToPath(new URL('../_local/bin/' + (process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'), import.meta.url))
43
- return existsSync(local) ? local : 'ffmpeg'
41
+ return ffmpegPathSync()
44
42
  }
45
43
 
46
44
  export function createVideoExportManager(options = {}) {
47
45
  const jobs = new Map()
48
46
  let closed = false
49
- const binary = options.ffmpegPath || ffmpegBinary()
50
47
  const historyRoot = (path) => join(dirname(path), '.drawai-cache', 'video-export-history')
51
48
  const ended = (job) => ['done', 'error', 'interrupted'].includes(job.status)
52
49
  function migrationRecords(scope) {
@@ -60,7 +57,7 @@ export function createVideoExportManager(options = {}) {
60
57
  }
61
58
  function persist(job) {
62
59
  const root = historyRoot(job.drawioAbs); mkdirSync(root, { recursive: true })
63
- const record = Object.fromEntries(['sessionId', 'drawioAbs', 'clip', 'token', 'status', 'time', 'next', 'encoded', 'format', 'kind', 'w', 'h', 'fps', 'count', 'bytes', 'savedPath', 'error'].map((key) => [key, job[key]]))
60
+ const record = Object.fromEntries(['sessionId', 'drawioAbs', 'clip', 'token', 'status', 'time', 'next', 'encoded', 'format', 'kind', 'w', 'h', 'fps', 'count', 'bytes', 'savedPath', 'error', 'message'].map((key) => [key, job[key]]))
64
61
  const file = join(root, job.token + '.json')
65
62
  writeFileSync(file + '.tmp', JSON.stringify(record)); renameSync(file + '.tmp', file)
66
63
  }
@@ -87,7 +84,7 @@ export function createVideoExportManager(options = {}) {
87
84
  function view(job) {
88
85
  return { token: job.token, status: job.status, count: job.count, received: job.next, encoded: job.encoded,
89
86
  clip: job.clip, fps: job.fps, w: job.w, h: job.h, bytes: job.bytes || 0, error: job.error || '', format: job.format,
90
- background: true, path: job.savedPath || null,
87
+ message: job.message || '', background: true, path: job.savedPath || null,
91
88
  performanceEnabled: job.perf.enabled, performanceId: job.perf.id }
92
89
  }
93
90
  function owned(scope, token) {
@@ -99,6 +96,7 @@ export function createVideoExportManager(options = {}) {
99
96
  }
100
97
  async function remove(job) {
101
98
  jobs.delete(job.token)
99
+ job.removed = true
102
100
  job.control.abort()
103
101
  if (job.preflight) await job.preflight.catch(() => {})
104
102
  if (job.promise) await job.promise.catch(() => {})
@@ -138,6 +136,26 @@ export function createVideoExportManager(options = {}) {
138
136
  async function runJob(job) {
139
137
  let release
140
138
  try {
139
+ // Resolve FFmpeg inside the task: the first use may download it, and a
140
+ // later retry must not keep using a path cached when the host started.
141
+ job.status = 'preparing'
142
+ job.message = '正在准备 FFmpeg…'
143
+ persist(job)
144
+ job.binary = options.ffmpegPath || await ensureFfmpeg({
145
+ signal: job.control.signal,
146
+ onProgress: (event) => { if (event?.message) job.message = event.message },
147
+ })
148
+ if (job.control.signal.aborted) throw new Error('已取消视频导出')
149
+ job.status = 'queued'
150
+ job.message = ''
151
+ persist(job)
152
+ // Probe the actual output path and encoder before uploading any large frames.
153
+ job.preflight = runFfmpeg(job.binary, ['-hide_banner', '-nostdin', '-loglevel', 'error', '-y', '-f', 'lavfi', '-i', 'color=white:s=2x2:r=1',
154
+ '-frames:v', '1', '-c:v', job.format === 'mp4' ? 'libx264' : job.format === 'gif' ? 'gif' : 'libvpx-vp9', '-threads', '1', job.output], job.control.signal)
155
+ const probeDone = job.perf.start('encoderPreflight')
156
+ try { await job.preflight }
157
+ catch (error) { throw new Error('导出预检查失败(目录:' + job.temp + '):' + error.message) }
158
+ finally { probeDone() }
141
159
  release = await options.slots?.acquire(job.control.signal)
142
160
  job.status = 'rendering'
143
161
  persist(job)
@@ -159,8 +177,10 @@ export function createVideoExportManager(options = {}) {
159
177
  persist(job)
160
178
  job.perf.finish('done')
161
179
  } catch (error) {
162
- job.status = closed ? 'interrupted' : 'error'; job.error = String(error.message || error); job.time = Date.now()
163
- persist(job)
180
+ if (!job.removed) {
181
+ job.status = closed ? 'interrupted' : 'error'; job.error = String(error.message || error); job.message = ''; job.time = Date.now()
182
+ persist(job)
183
+ }
164
184
  job.perf.finish(job.control.signal.aborted ? 'cancelled' : 'failed')
165
185
  } finally { release?.(); rmSync(job.temp, { recursive: true, force: true }) }
166
186
  }
@@ -199,11 +219,11 @@ export function createVideoExportManager(options = {}) {
199
219
  paletteArgs.push('-filter_complex', gifWhite + '[opaque]palettegen=max_colors=256:reserve_transparent=0:stats_mode=full[palette]', '-map', '[palette]',
200
220
  '-frames:v', '1', '-threads', '1', '-update', '1', join(job.temp, 'palette.png'))
201
221
  const paletteDone = job.perf.start('gifPaletteGenerate')
202
- try { await runFfmpeg(binary, paletteArgs, job.control.signal) }
222
+ try { await runFfmpeg(job.binary, paletteArgs, job.control.signal) }
203
223
  finally { paletteDone() }
204
224
  }
205
225
  const encodeDone = job.perf.start('ffmpegEncode')
206
- try { await runFfmpeg(binary, args, job.control.signal, (frame) => { job.encoded = frame }) }
226
+ try { await runFfmpeg(job.binary, args, job.control.signal, (frame) => { job.encoded = frame }) }
207
227
  finally { encodeDone() }
208
228
  if (job.kind === 'svg' && job.encoded !== job.count) throw new Error('输出帧数不完整:预期 ' + job.count + ',实际 ' + job.encoded)
209
229
  job.bytes = statSync(job.output).size
@@ -265,14 +285,6 @@ export function createVideoExportManager(options = {}) {
265
285
  next: 0, received: 0, encoded: 0, status: 'queued', time: Date.now(), control: new AbortController(), output: join(temp, 'output.' + format) }
266
286
  jobs.set(job.token, job)
267
287
  persist(job)
268
- // Probe the actual output path and encoder before uploading any large frames.
269
- job.preflight = runFfmpeg(binary, ['-hide_banner', '-nostdin', '-loglevel', 'error', '-y', '-f', 'lavfi', '-i', 'color=white:s=2x2:r=1',
270
- '-frames:v', '1', '-c:v', format === 'mp4' ? 'libx264' : format === 'gif' ? 'gif' : 'libvpx-vp9', '-threads', '1', job.output], job.control.signal)
271
- const probeDone = perf.start('encoderPreflight')
272
- try { await job.preflight }
273
- catch (error) { throw new Error('导出预检查失败(目录:' + temp + '):' + error.message) }
274
- finally { probeDone() }
275
- if (closed || job.control.signal.aborted) throw new Error('视频导出服务已关闭')
276
288
  job.promise = runJob(job)
277
289
  return view(job)
278
290
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-drawai",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "DSH 右侧栏的可编辑画布 + AI 语义绘图工具(diagram_apply / diagram_read),直接读写原生 .drawio 文件",
6
6
  "license": "MIT",
@@ -22,10 +22,12 @@
22
22
  "test:audio-ui": "node tools/check-audio-ui.mjs",
23
23
  "watch": "node tools/watch.mjs",
24
24
  "check": "node tools/check-package.mjs",
25
- "test": "node tools/check-media-package.mjs && node tools/check-motion-session.mjs && node tools/check-motion-host.mjs && node tools/check-motion-canvas-spec.mjs && node tools/check-motion-json-bom.mjs && node tools/check-motion-pilot-host.mjs && node tools/check-motion-boundaries.mjs && node tools/check-motion-cache-fetch.mjs && node tools/check-media-library-host.mjs && node tools/check-motion-ui.mjs && node tools/check-native-canvas.mjs && node tools/check-adaptive-palette.mjs && node tools/check-auto-save.mjs && node tools/check-drawing-queue.mjs && node tools/check-background-tasks.mjs && node tools/check-background-ui.mjs && node tools/check-trace-optimization.mjs && node tools/check-performance.mjs && node tools/check-mxfile.mjs && node tools/check-routing.mjs && node tools/check-route-preview.mjs && node tools/check-host.mjs && node tools/check-render.mjs && node tools/check-component.mjs && node tools/check-text-copy.mjs && node tools/check-image-export.mjs && node tools/check-canvas-sessions.mjs && node tools/check-trace.mjs && node tools/check-media.mjs && node tools/check-video-player.mjs && node tools/check-image-trace.mjs && node tools/check-video-trace.mjs && node tools/check-video-reuse.mjs && node tools/check-video-audio.mjs && node tools/check-video-export.mjs && node tools/check-video-import.mjs && node tools/check-node-labels.mjs && node tools/check-imagegen-disabled.mjs && node tools/check-imagegen.mjs",
25
+ "test": "node tools/check-video-runtime.mjs && node tools/check-ffmpeg-manager.mjs && node tools/check-media-package.mjs && node tools/check-motion-session.mjs && node tools/check-motion-host.mjs && node tools/check-motion-canvas-spec.mjs && node tools/check-motion-json-bom.mjs && node tools/check-motion-pilot-host.mjs && node tools/check-motion-boundaries.mjs && node tools/check-motion-cache-fetch.mjs && node tools/check-media-library-host.mjs && node tools/check-motion-ui.mjs && node tools/check-native-canvas.mjs && node tools/check-adaptive-palette.mjs && node tools/check-auto-save.mjs && node tools/check-drawing-queue.mjs && node tools/check-background-tasks.mjs && node tools/check-background-ui.mjs && node tools/check-trace-optimization.mjs && node tools/check-performance.mjs && node tools/check-mxfile.mjs && node tools/check-routing.mjs && node tools/check-route-preview.mjs && node tools/check-host.mjs && node tools/check-render.mjs && node tools/check-component.mjs && node tools/check-text-copy.mjs && node tools/check-image-export.mjs && node tools/check-canvas-sessions.mjs && node tools/check-trace.mjs && node tools/check-media.mjs && node tools/check-video-player.mjs && node tools/check-image-trace.mjs && node tools/check-video-trace.mjs && node tools/check-video-reuse.mjs && node tools/check-video-audio.mjs && node tools/check-video-export.mjs && node tools/check-video-import.mjs && node tools/check-node-labels.mjs && node tools/check-imagegen-disabled.mjs && node tools/check-imagegen.mjs",
26
26
  "bench:performance": "node tools/benchmark-performance.mjs",
27
27
  "test:shotcards-render": "node tools/check-shotcard-render.mjs",
28
- "test:shotcards-ui": "node tools/check-shotcards-ui.mjs"
28
+ "test:shotcards-ui": "node tools/check-shotcards-ui.mjs",
29
+ "test:video-runtime": "node tools/check-video-runtime.mjs",
30
+ "test:ffmpeg": "node tools/check-ffmpeg-manager.mjs"
29
31
  },
30
32
  "exports": {
31
33
  ".": "./lib/index.js",
@@ -54,18 +56,10 @@
54
56
  "dependencies": {
55
57
  "@babel/parser": "7.28.5",
56
58
  "@babel/traverse": "7.28.5",
57
- "@react-three/fiber": "9.5.0",
58
- "@remotion/bundler": "4.0.484",
59
- "@remotion/motion-blur": "4.0.484",
60
- "@remotion/renderer": "4.0.484",
61
- "@remotion/three": "4.0.484",
62
59
  "@resvg/resvg-js": "^2.6.2",
63
60
  "parse5": "7.3.0",
64
61
  "postcss": "8.5.32",
65
- "react": "19.2.7",
66
- "react-dom": "19.2.7",
67
- "remotion": "4.0.484",
68
- "three": "0.183.2"
62
+ "npm": "10.9.4"
69
63
  },
70
64
  "peerDependencies": {
71
65
  "@deepseek-ai/dsh-tools": ">=0.1.0-rc.1 <0.1.0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0"
@@ -82,5 +76,16 @@
82
76
  "@deepseek-ai/dsh-api-workspace-files"
83
77
  ]
84
78
  }
79
+ },
80
+ "devDependencies": {
81
+ "@react-three/fiber": "9.5.0",
82
+ "@remotion/bundler": "4.0.484",
83
+ "@remotion/motion-blur": "4.0.484",
84
+ "@remotion/renderer": "4.0.484",
85
+ "@remotion/three": "4.0.484",
86
+ "react": "19.2.7",
87
+ "react-dom": "19.2.7",
88
+ "remotion": "4.0.484",
89
+ "three": "0.183.2"
85
90
  }
86
91
  }