dsh-drawai 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +430 -418
- package/README.md +7 -2
- package/lib/audio-media.js +5 -2
- package/lib/client.js +17 -5
- package/lib/drawing-runner.js +6 -4
- package/lib/ffmpeg-manager.js +1056 -0
- package/lib/index.js +6 -2
- package/lib/motion/apply.js +4 -3
- package/lib/motion/assets.js +4 -3
- package/lib/motion/beats.js +3 -2
- package/lib/motion/collect.js +3 -1
- package/lib/motion/delivery.js +29 -1
- package/lib/motion/ffmpeg.js +5 -4
- package/lib/motion/jobs.js +5 -1
- package/lib/motion/preview.js +5 -3
- package/lib/motion/render.js +3 -1
- package/lib/motion/runtime/package-lock.json +3058 -0
- package/lib/motion/runtime/package.json +16 -0
- package/lib/motion/style-frames.js +12 -9
- package/lib/motion/video-runtime.js +192 -0
- package/lib/motion-capture.js +2 -1
- package/lib/motion-film-review.js +2 -2
- package/lib/motion-project.js +12 -5
- package/lib/motion-render-worker.js +13 -5
- package/lib/motion-vision.js +3 -2
- package/lib/shotcard-reference.js +2 -2
- package/lib/video-export.js +32 -20
- package/package.json +17 -12
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "drawai-video-runtime",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@react-three/fiber": "9.5.0",
|
|
7
|
+
"@remotion/bundler": "4.0.484",
|
|
8
|
+
"@remotion/motion-blur": "4.0.484",
|
|
9
|
+
"@remotion/renderer": "4.0.484",
|
|
10
|
+
"@remotion/three": "4.0.484",
|
|
11
|
+
"react": "19.2.7",
|
|
12
|
+
"react-dom": "19.2.7",
|
|
13
|
+
"remotion": "4.0.484",
|
|
14
|
+
"three": "0.183.2"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 打开 demos/<slug>/index.html,按 cues.json 的时间点调 renderAt(t) 截帧,交给模型当
|
|
7
7
|
* 对照物。缺字体/纸纹时用占位资源兜底,并记在 notes 里,不假装素材齐全。
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { loadVideoModule, ensureVideoRuntime } from './video-runtime.js'
|
|
10
10
|
import {existsSync,mkdirSync,readFileSync,statSync,writeFileSync} from 'node:fs'
|
|
11
11
|
import {createServer} from 'node:http'
|
|
12
12
|
import {extname,join,normalize,resolve,sep} from 'node:path'
|
|
@@ -25,7 +25,7 @@ const DEMO_FONT_ALIASES = {
|
|
|
25
25
|
// 上游 demo 的 importmap 指向 /node_modules/three/...、/node_modules/opentype.js/...,
|
|
26
26
|
// 但这些包只在插件根目录的 node_modules 里。旧版只服务 upstream 目录,于是模块 404、
|
|
27
27
|
// window.__ready 永不置真,最后报了句"没有在 120 秒内就绪"——错误信息指向了完全错误的地方。
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
|
|
30
30
|
const MIME = {
|
|
31
31
|
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
@@ -101,16 +101,16 @@ function pickTimes(slug, count, explicit, duration) {
|
|
|
101
101
|
|
|
102
102
|
// demo 的 importmap 指向 /node_modules/*,只有真装在本机的包才服务得出来。
|
|
103
103
|
// 缺包时页面会静默卡在模块加载阶段,等到超时也只会说"没就绪"—— 这里提前查,几十毫秒就报清缺什么。
|
|
104
|
-
function missingModules(slug) {
|
|
104
|
+
function missingModules(slug, modules) {
|
|
105
105
|
const html = join(UPSTREAM, 'demos', slug, 'index.html')
|
|
106
106
|
let text = ''
|
|
107
107
|
try { text = readFileSync(html, 'utf8') } catch { return [] }
|
|
108
108
|
const refs = [...text.matchAll(/["'](\/node_modules\/[^"']+)["']/g)].map((match) => match[1])
|
|
109
|
-
return [...new Set(refs)].filter((ref) => !existsSync(join(
|
|
109
|
+
return [...new Set(refs)].filter((ref) => !existsSync(join(modules, ref.slice('/node_modules/'.length))))
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function startStaticServer(misses) {
|
|
113
|
-
const roots = {vendor: resolve(
|
|
112
|
+
function startStaticServer(misses, modules) {
|
|
113
|
+
const roots = {vendor: resolve(modules), upstream: resolve(UPSTREAM)}
|
|
114
114
|
return new Promise((done) => {
|
|
115
115
|
const server = createServer((req, res) => {
|
|
116
116
|
try {
|
|
@@ -181,16 +181,19 @@ export async function captureStyleFrames({slug, times: explicitTimes, count = 8,
|
|
|
181
181
|
if (typeof slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/i.test(slug)) throw new Error('slug 无效:' + slug)
|
|
182
182
|
const demo = join(UPSTREAM, 'demos', slug)
|
|
183
183
|
if (!existsSync(join(demo, 'index.html'))) throw new Error('这个风格没有可截帧的上游 demo:' + slug + '(只有说明、没有 index.html)')
|
|
184
|
-
const
|
|
184
|
+
const runtime = await ensureVideoRuntime({signal})
|
|
185
|
+
const modules = join(runtime, 'node_modules')
|
|
186
|
+
const { openBrowser } = await loadVideoModule('@remotion/renderer', {signal})
|
|
187
|
+
const missing = missingModules(slug, modules)
|
|
185
188
|
if (missing.length) {
|
|
186
189
|
throw new Error('这个 demo 的依赖没装在本机(' + missing.join(', ') + '),页面加载不到模块,截不了帧。'
|
|
187
190
|
+ '三条路:① 改用 action:"source" 读 demos/' + slug + '/ 下的源码与 cues.json 反推动效(多数情况够用);'
|
|
188
|
-
+ '②
|
|
191
|
+
+ '② 检查独立视频运行环境中的额外依赖(' + modules + ');'
|
|
189
192
|
+ '③ 换一个不依赖它们的 demo。')
|
|
190
193
|
}
|
|
191
194
|
|
|
192
195
|
const misses = []
|
|
193
|
-
const server = await startStaticServer(misses)
|
|
196
|
+
const server = await startStaticServer(misses, modules)
|
|
194
197
|
const port = server.address().port
|
|
195
198
|
const executable = findChrome(browserExecutable)
|
|
196
199
|
signal?.throwIfAborted()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Isolated video dependencies: no Remotion/esbuild is loaded or installed at plugin startup.
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve, parse } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const localRequire = createRequire(import.meta.url)
|
|
11
|
+
const MANIFEST = fileURLToPath(new URL('./runtime/package.json', import.meta.url))
|
|
12
|
+
const LOCK = fileURLToPath(new URL('./runtime/package-lock.json', import.meta.url))
|
|
13
|
+
const fingerprint = createHash('sha256').update(readFileSync(LOCK)).digest('hex')
|
|
14
|
+
const spec = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
|
15
|
+
const key = `v1-${process.platform}-${process.arch}-node${process.versions.node.split('.')[0]}-${fingerprint.slice(0, 12)}`
|
|
16
|
+
const pending = new Map()
|
|
17
|
+
|
|
18
|
+
function waitForInitialization(work, signal) {
|
|
19
|
+
if (!signal) return work
|
|
20
|
+
signal.throwIfAborted()
|
|
21
|
+
return new Promise((done, fail) => {
|
|
22
|
+
const abort = () => fail(signal.reason || new Error('已取消视频环境初始化'))
|
|
23
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
24
|
+
work.then(done, fail).finally(() => signal.removeEventListener('abort', abort))
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function videoRuntimeDirectory() {
|
|
29
|
+
const directory = resolve(process.env.DRAWAI_VIDEO_RUNTIME_DIR || join(homedir(), '.dsh', 'drawai-video-runtime', key))
|
|
30
|
+
if ([parse(directory).root, resolve(homedir()), resolve(process.cwd())].includes(directory))
|
|
31
|
+
throw new Error('视频运行环境必须使用独立子目录')
|
|
32
|
+
return directory
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function videoRuntimeRequire(directory = videoRuntimeDirectory()) {
|
|
36
|
+
return createRequire(join(directory, 'package.json'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validate(directory, native = false) {
|
|
40
|
+
const require = videoRuntimeRequire(directory)
|
|
41
|
+
for (const [name, version] of Object.entries(spec.dependencies)) {
|
|
42
|
+
let file
|
|
43
|
+
try { file = require.resolve(name + '/package.json') } catch (error) {
|
|
44
|
+
if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error
|
|
45
|
+
let parent = dirname(require.resolve(name))
|
|
46
|
+
while (dirname(parent) !== parent) {
|
|
47
|
+
const candidate = join(parent, 'package.json')
|
|
48
|
+
if (existsSync(candidate) && JSON.parse(readFileSync(candidate, 'utf8')).name === name) { file = candidate; break }
|
|
49
|
+
parent = dirname(parent)
|
|
50
|
+
}
|
|
51
|
+
if (!file) throw new Error('缺少视频依赖清单:' + name)
|
|
52
|
+
}
|
|
53
|
+
const rel = relative(realpathSync(join(directory, 'node_modules')), realpathSync(file))
|
|
54
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith('..' + (process.platform === 'win32' ? '\\' : '/')))
|
|
55
|
+
throw new Error('视频依赖未安装在独立目录:' + name)
|
|
56
|
+
if (JSON.parse(readFileSync(file, 'utf8')).version !== version) throw new Error('视频依赖版本不匹配:' + name)
|
|
57
|
+
}
|
|
58
|
+
if (!native) return
|
|
59
|
+
// Validate in a child so Windows DLL handles and cached module paths don't
|
|
60
|
+
// pin the staging directory while it is atomically renamed into the cache.
|
|
61
|
+
const checked = spawnSync(process.execPath, ['-e',
|
|
62
|
+
"const r=require('node:module').createRequire(process.argv[1]);const b=r('@remotion/bundler');r('@remotion/renderer');if(!b.BundlerInternals.esbuild.transformSync('const n: number = 1',{loader:'ts'}).code)process.exit(1)",
|
|
63
|
+
join(directory, 'package.json')], { windowsHide: true, encoding: 'utf8', timeout: 90000,
|
|
64
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } })
|
|
65
|
+
if (checked.status !== 0) throw new Error('视频二进制验证失败:' + (checked.error?.message || checked.stderr))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function videoRuntimeState() {
|
|
69
|
+
const directory = videoRuntimeDirectory()
|
|
70
|
+
try {
|
|
71
|
+
const ready = JSON.parse(readFileSync(join(directory, '.ready.json'), 'utf8'))
|
|
72
|
+
if (ready.fingerprint !== fingerprint || ready.key !== key) return { ready: false, directory }
|
|
73
|
+
validate(directory)
|
|
74
|
+
return { ready: true, directory }
|
|
75
|
+
} catch { return { ready: false, directory } }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function install(directory, { signal, onProgress }) {
|
|
79
|
+
const cli = join(dirname(localRequire.resolve('npm/package.json')), 'bin', 'npm-cli.js')
|
|
80
|
+
const registry = process.env.DRAWAI_RUNTIME_REGISTRY || 'https://registry.npmjs.org'
|
|
81
|
+
const url = new URL(registry)
|
|
82
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('视频依赖 registry 地址无效')
|
|
83
|
+
return new Promise((done, fail) => {
|
|
84
|
+
const child = spawn(process.execPath, [cli, 'ci', '--ignore-scripts', '--include=optional', '--no-audit', '--no-fund',
|
|
85
|
+
'--registry=' + url.href, '--fetch-retries=1', '--fetch-timeout=30000', '--loglevel=error'], {
|
|
86
|
+
cwd: directory, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'],
|
|
87
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', NODE_ENV: 'production', npm_config_ignore_scripts: 'true' },
|
|
88
|
+
})
|
|
89
|
+
let stderr = '', settled = false
|
|
90
|
+
const abort = () => child.kill()
|
|
91
|
+
const timer = setTimeout(() => child.kill(), 10 * 60000)
|
|
92
|
+
const progress = setInterval(() => onProgress?.({ phase: 'runtime-installing', message: '正在下载和校验视频依赖,首次初始化可能需要数分钟' }), 5000)
|
|
93
|
+
const finish = error => {
|
|
94
|
+
if (settled) return
|
|
95
|
+
settled = true; clearTimeout(timer); clearInterval(progress); signal?.removeEventListener('abort', abort)
|
|
96
|
+
if (signal?.aborted) fail(signal.reason || new Error('已取消视频环境初始化'))
|
|
97
|
+
else if (error) fail(error)
|
|
98
|
+
else done()
|
|
99
|
+
}
|
|
100
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-3000) })
|
|
101
|
+
child.on('error', finish)
|
|
102
|
+
child.on('close', code => finish(code === 0 ? null : new Error('视频依赖安装失败(' + code + '):' + stderr)))
|
|
103
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
104
|
+
if (signal?.aborted) abort()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function initialize({ signal, onProgress }) {
|
|
109
|
+
const directory = videoRuntimeDirectory(), lock = directory + '.lock'
|
|
110
|
+
if (existsSync(directory)) {
|
|
111
|
+
const previous = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'))
|
|
112
|
+
if (previous.name !== spec.name) throw new Error('缓存位置包含其他文件,请指定独立目录')
|
|
113
|
+
}
|
|
114
|
+
mkdirSync(dirname(directory), { recursive: true })
|
|
115
|
+
// Coordinate independent render workers. A dead owner's lock is recoverable.
|
|
116
|
+
let ownsLock = false
|
|
117
|
+
const deadline = Date.now() + 11 * 60000
|
|
118
|
+
while (!ownsLock) {
|
|
119
|
+
signal?.throwIfAborted()
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(lock)
|
|
122
|
+
ownsLock = true
|
|
123
|
+
writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, at: Date.now() }))
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (ownsLock || error.code !== 'EEXIST') throw error
|
|
126
|
+
try {
|
|
127
|
+
const owner = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8'))
|
|
128
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0) throw new Error('invalid lock owner')
|
|
129
|
+
try { process.kill(owner.pid, 0) } catch (e) {
|
|
130
|
+
if (e.code === 'ESRCH') { rmSync(lock, { recursive: true, force: true }); continue }
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Recover if the installer crashed between mkdir and writing its owner.
|
|
134
|
+
if (existsSync(lock) && Date.now() - statSync(lock).mtimeMs > 30000) {
|
|
135
|
+
rmSync(lock, { recursive: true, force: true }); continue
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Date.now() > deadline) throw new Error('等待其他视频环境初始化超时,请重试')
|
|
139
|
+
onProgress?.({ phase: 'runtime-installing', message: '正在等待共享视频运行环境就绪' })
|
|
140
|
+
await new Promise(done => setTimeout(done, 500))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const staging = directory + '.tmp-' + randomUUID()
|
|
144
|
+
try {
|
|
145
|
+
if (videoRuntimeState().ready) return directory
|
|
146
|
+
onProgress?.({ phase: 'runtime-installing', message: '首次使用:正在初始化独立视频运行环境' })
|
|
147
|
+
signal?.throwIfAborted()
|
|
148
|
+
mkdirSync(staging)
|
|
149
|
+
writeFileSync(join(staging, 'package.json'), readFileSync(MANIFEST))
|
|
150
|
+
writeFileSync(join(staging, 'package-lock.json'), readFileSync(LOCK))
|
|
151
|
+
await install(staging, { signal, onProgress })
|
|
152
|
+
signal?.throwIfAborted()
|
|
153
|
+
onProgress?.({ phase: 'runtime-checking', message: '正在验证视频运行环境' })
|
|
154
|
+
validate(staging, true)
|
|
155
|
+
writeFileSync(join(staging, '.ready.json'), JSON.stringify({ fingerprint, key }))
|
|
156
|
+
// Only replace this cache after installation and native-binary validation succeed.
|
|
157
|
+
if (existsSync(directory)) {
|
|
158
|
+
const previous = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'))
|
|
159
|
+
if (previous.name !== spec.name) throw new Error('缓存位置包含其他文件,请指定独立目录')
|
|
160
|
+
rmSync(directory, { recursive: true, force: true })
|
|
161
|
+
}
|
|
162
|
+
renameSync(staging, directory)
|
|
163
|
+
onProgress?.({ phase: 'runtime-ready', message: '视频运行环境已就绪,后续离线复用' })
|
|
164
|
+
return directory
|
|
165
|
+
} finally {
|
|
166
|
+
rmSync(staging, { recursive: true, force: true })
|
|
167
|
+
rmSync(lock, { recursive: true, force: true })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function ensureVideoRuntime(options = {}) {
|
|
172
|
+
options.signal?.throwIfAborted()
|
|
173
|
+
const state = videoRuntimeState()
|
|
174
|
+
if (state.ready) return state.directory
|
|
175
|
+
// A caller's cancellation must not cancel another caller's shared initialization.
|
|
176
|
+
if (pending.has(state.directory)) {
|
|
177
|
+
try { await waitForInitialization(pending.get(state.directory), options.signal) } catch { /* Another caller may have cancelled. */ }
|
|
178
|
+
options.signal?.throwIfAborted()
|
|
179
|
+
return ensureVideoRuntime(options)
|
|
180
|
+
}
|
|
181
|
+
const work = initialize({ ...options, onProgress: options.onProgress || (event => console.error('[drawai] ' + event.message)) }).catch(error => {
|
|
182
|
+
throw new Error('视频运行环境未就绪:' + error.message + '\n联网后重试当前操作即可重新初始化。可用 DRAWAI_RUNTIME_REGISTRY 设置镜像,DRAWAI_VIDEO_RUNTIME_DIR 设置缓存目录。', { cause: error })
|
|
183
|
+
}).finally(() => pending.delete(state.directory))
|
|
184
|
+
pending.set(state.directory, work)
|
|
185
|
+
return work
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function loadVideoModule(name, options = {}) {
|
|
189
|
+
if (!Object.hasOwn(spec.dependencies, name)) throw new Error('不支持的视频模块:' + name)
|
|
190
|
+
const directory = await ensureVideoRuntime(options)
|
|
191
|
+
return videoRuntimeRequire(directory)(name)
|
|
192
|
+
}
|
package/lib/motion-capture.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* 由 tools/build.mjs 生成 —— 请勿直接编辑;改 src/ 下的源文件。 */
|
|
2
|
-
import {
|
|
2
|
+
import { loadVideoModule } from './motion/video-runtime.js'
|
|
3
3
|
import {existsSync,mkdirSync,writeFileSync} from 'node:fs'
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {randomUUID,createHash} from 'node:crypto'
|
|
@@ -12,6 +12,7 @@ export async function captureMotionPage(args,{directory,signal,browserExecutable
|
|
|
12
12
|
if(!Array.isArray(args.elements??[])||(args.elements||[]).length>20||!Array.isArray(args.steps??[])||(args.steps||[]).length>16)throw new Error('单页最多20个切片、16个操作')
|
|
13
13
|
const executable=[browserExecutable,process.env.DRAWAI_CHROME_PATH,'C:/Program Files/Google/Chrome/Application/chrome.exe','C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'].find(p=>p&&existsSync(p))
|
|
14
14
|
signal?.throwIfAborted()
|
|
15
|
+
const { openBrowser } = await loadVideoModule('@remotion/renderer', {signal})
|
|
15
16
|
const browser=await openBrowser('chrome',{browserExecutable:executable,logLevel:'error'})
|
|
16
17
|
const abort=()=>{void browser.close({silent:true}).catch(()=>{})};signal?.addEventListener('abort',abort,{once:true})
|
|
17
18
|
try{
|
|
@@ -3,11 +3,11 @@ import {mkdirSync,readFileSync,readdirSync,writeFileSync} from 'node:fs'
|
|
|
3
3
|
import {join} from 'node:path'
|
|
4
4
|
import {execFile} from 'node:child_process'
|
|
5
5
|
import {promisify} from 'node:util'
|
|
6
|
-
import {
|
|
6
|
+
import {ensureFfmpeg} from './ffmpeg-manager.js'
|
|
7
7
|
import {agentHash,agentAtomicJson} from './motion-agent-files.js'
|
|
8
8
|
import {streamAgentTurn,agentMessage} from './motion-agent-protocol.js'
|
|
9
9
|
const exec=promisify(execFile)
|
|
10
|
-
const run=(args,signal)=>exec(
|
|
10
|
+
const run=async(args,signal)=>{const binary=await ensureFfmpeg({signal});return exec(binary,['-hide_banner','-nostdin',...args],{windowsHide:true,signal,timeout:180000,maxBuffer:8*1024*1024})}
|
|
11
11
|
|
|
12
12
|
// Decode the actual delivered file, including its final mixed audio. Visual
|
|
13
13
|
// semantics use ordered contact sheets; listening is a separate human verdict.
|
package/lib/motion-project.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
3
|
import { join, dirname, resolve, relative, isAbsolute } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import {
|
|
5
|
+
import { videoRuntimeRequire } from './motion/video-runtime.js'
|
|
6
6
|
import { validateMotionSources, sceneComponentPath } from './motion-source.js'
|
|
7
7
|
import { shotcraftModuleMap } from './motion-library.js'
|
|
8
8
|
import { enforceMotionCards } from './motion-library.js'
|
|
9
9
|
import { enforceMgStyle } from './motion-library.js'
|
|
10
10
|
import { validateMotionPlan } from './motion-core.js'
|
|
11
11
|
import { parseProjectJson } from './motion/json.js'
|
|
12
|
-
const require = createRequire(import.meta.url)
|
|
13
12
|
|
|
14
13
|
// Agent projects are edited on disk. Read their sources before validating or
|
|
15
14
|
// building; never restore a stale inline source over an agent's changes.
|
|
@@ -74,7 +73,15 @@ export function prepareMotionProject(composition, directory, options = {}) {
|
|
|
74
73
|
})
|
|
75
74
|
} }
|
|
76
75
|
alias['@drawai/motion$'] = fileURLToPath(new URL('./motion/reference-media.jsx', import.meta.url))
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
// Resolve the runtime lazily: creating a project only writes its files, and
|
|
77
|
+
// the video runtime may not be initialized yet (it is installed on first use).
|
|
78
|
+
const runtimeModules = () => {
|
|
79
|
+
const require = videoRuntimeRequire(options.runtimeDirectory)
|
|
80
|
+
return [...new Set(['react', 'remotion', '@remotion/motion-blur'].map((name) => dirname(require.resolve(name + '/package.json')).replace(/[\\/]@remotion[\\/][^\\/]+$/, '').replace(/[\\/](?:react|remotion)$/, '')))]
|
|
81
|
+
}
|
|
82
|
+
return { entryPoint, webpackOverride: (config) => {
|
|
83
|
+
const modules = runtimeModules()
|
|
84
|
+
return { ...config, plugins: [...(config.plugins || []), libraryImports], resolve: { ...config.resolve,
|
|
85
|
+
alias: { ...config.resolve?.alias, ...alias }, modules: [...modules, ...(config.resolve?.modules || ['node_modules'])] } }
|
|
86
|
+
} }
|
|
80
87
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* 由 tools/build.mjs 生成 —— 请勿直接编辑;改 src/ 下的源文件。 */
|
|
2
|
-
import {
|
|
3
|
-
import { openBrowser, selectComposition, renderMedia, renderStill, makeCancelSignal } from '@remotion/renderer'
|
|
2
|
+
import { ensureVideoRuntime, videoRuntimeRequire } from './motion/video-runtime.js'
|
|
4
3
|
import { existsSync, rmSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
4
|
import { dirname, join, relative, isAbsolute, resolve } from 'node:path'
|
|
6
5
|
import { createHash } from 'node:crypto'
|
|
@@ -8,16 +7,25 @@ import { prepareMotionProject } from './motion-project.js'
|
|
|
8
7
|
import { runFfmpeg } from './motion/ffmpeg.js'
|
|
9
8
|
|
|
10
9
|
let cancelled = false, running = false
|
|
11
|
-
const
|
|
10
|
+
const runtimeAbort = new AbortController()
|
|
11
|
+
let cancel = () => {}
|
|
12
12
|
const send = (message) => { if (process.connected) process.send(message) }
|
|
13
13
|
process.on('message', async (request) => {
|
|
14
|
-
if (request.type === 'cancel') { cancelled = true; cancel(); return }
|
|
14
|
+
if (request.type === 'cancel') { cancelled = true; runtimeAbort.abort(new Error('已取消动效生成')); cancel(); return }
|
|
15
15
|
if (running) return
|
|
16
16
|
running = true
|
|
17
17
|
let browser
|
|
18
18
|
const outDir = request.bundleDir || join(dirname(request.output), 'bundle')
|
|
19
19
|
const cacheFile = join(outDir, '.drawai-bundle-key')
|
|
20
20
|
try {
|
|
21
|
+
const runtimeDirectory = await ensureVideoRuntime({signal:runtimeAbort.signal, onProgress:message => send({type:'progress', ...message, progress:0})})
|
|
22
|
+
if (cancelled) throw new Error('已取消动效生成')
|
|
23
|
+
const require = videoRuntimeRequire(runtimeDirectory)
|
|
24
|
+
const { bundle } = require('@remotion/bundler')
|
|
25
|
+
const { openBrowser, selectComposition, renderMedia, renderStill, makeCancelSignal } = require('@remotion/renderer')
|
|
26
|
+
const cancellation = makeCancelSignal()
|
|
27
|
+
const cancelSignal = cancellation.cancelSignal
|
|
28
|
+
cancel = cancellation.cancel
|
|
21
29
|
// 同一个工程连续预览/渲染时复用上次的 bundle:预览从 30 秒降到 3 秒,
|
|
22
30
|
// 改一个场景文件只会让 key 变化一次。request.bundleKey 由调用方按源码计算。
|
|
23
31
|
const reuse = request.reuseBundle === true && request.bundleKey && existsSync(join(outDir, 'index.html'))
|
|
@@ -27,7 +35,7 @@ process.on('message', async (request) => {
|
|
|
27
35
|
serveUrl = outDir
|
|
28
36
|
send({ type: 'progress', phase: 'bundling', progress: .1 })
|
|
29
37
|
} else {
|
|
30
|
-
const project = prepareMotionProject(request.composition, request.projectDirectory || join(dirname(request.output), 'project'), {preserveSources:request.preserveSources===true})
|
|
38
|
+
const project = prepareMotionProject(request.composition, request.projectDirectory || join(dirname(request.output), 'project'), {preserveSources:request.preserveSources===true, runtimeDirectory})
|
|
31
39
|
serveUrl = await bundle({ ...project, outDir, ignoreRegisterRootWarning: true,
|
|
32
40
|
publicDir: request.publicDir, onProgress: (progress) => send({ type: 'progress', phase: 'bundling', progress: progress / 100 * .1 }) })
|
|
33
41
|
const index = join(outDir, 'index.html')
|
package/lib/motion-vision.js
CHANGED
|
@@ -4,7 +4,7 @@ import { relative, isAbsolute, 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 {
|
|
7
|
+
import { ensureFfmpeg } from './ffmpeg-manager.js'
|
|
8
8
|
const exec=promisify(execFile)
|
|
9
9
|
|
|
10
10
|
export async function loadFrozenReferenceImages(input,assets,publicDir,options) {
|
|
@@ -29,7 +29,8 @@ export async function loadFrozenReferenceImages(input,assets,publicDir,options)
|
|
|
29
29
|
if(fraction===null){data=await readFile(path);mediaType=asset.src.endsWith('jpg')?'image/jpeg':asset.src.endsWith('webp')?'image/webp':'image/png'}
|
|
30
30
|
else{
|
|
31
31
|
// Video snapshots describe appearance; two stills are not motion analysis.
|
|
32
|
-
const
|
|
32
|
+
const binary=await ensureFfmpeg({signal})
|
|
33
|
+
const {stdout}=await exec(binary,['-hide_banner','-loglevel','error','-ss',String(fraction),'-i',path,'-frames:v','1','-vf','scale=1280:-2','-f','image2pipe','-vcodec','png','pipe:1'],{encoding:'buffer',windowsHide:true,timeout:30000,maxBuffer:8*1024*1024,signal})
|
|
33
34
|
data=stdout;mediaType='image/png';name=asset.src+'-'+fraction+'.png'
|
|
34
35
|
}
|
|
35
36
|
const attachment=await attachments.saveImage({data,mediaType,name})
|
|
@@ -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 {
|
|
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 ||
|
|
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 }
|
package/lib/video-export.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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.
|
|
3
|
+
"version": "0.4.1",
|
|
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
|
-
"
|
|
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
|
}
|