@uzooai/anime-ai-cli 0.0.0-stage → 0.5.0

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 (123) hide show
  1. package/.agents/skills/uzoo-anime-cli/SKILL.md +83 -0
  2. package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +51 -0
  3. package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
  4. package/.agents/skills/uzoo-anime-cli/references/commands.md +250 -0
  5. package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +152 -0
  6. package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +175 -0
  7. package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
  8. package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +123 -0
  9. package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +95 -0
  10. package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +94 -0
  11. package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
  12. package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +300 -0
  13. package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +41 -0
  14. package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +71 -0
  15. package/CHANGELOG.md +92 -0
  16. package/LICENSE +21 -0
  17. package/README.md +242 -3
  18. package/assets/README.md +4 -0
  19. package/assets/favicon-uzoo.png +0 -0
  20. package/assets/logo-uzoo.svg +7 -0
  21. package/docs/CLI/345/275/223/345/211/215/350/214/203/345/233/264/344/270/216/346/234/254/345/234/260/346/211/223/345/214/205-20261103.md +134 -0
  22. package/docs/CLI/346/216/210/346/235/203/346/216/245/345/217/243/344/270/216/344/273/244/347/211/214/350/207/252/345/212/250/345/210/267/346/226/260/350/256/276/350/256/241.md +540 -0
  23. package/docs/CLI/347/274/272/345/217/243/346/240/270/345/257/271/344/270/216/346/224/266/345/260/276-20261105.md +65 -0
  24. package/docs/CLI/347/275/221/351/241/265/344/277/235/345/255/230/350/212/202/347/202/271/347/274/272/345/244/261/344/272/244/346/216/245.md +52 -0
  25. package/docs/M0-M5/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +89 -0
  26. package/docs/npm/347/272/277/344/270/212/345/217/221/345/270/203/344/270/216/345/256/211/350/243/205/344/275/277/347/224/250.md +151 -0
  27. package/docs//344/270/213/350/275/275/346/216/245/345/217/243/351/200/202/351/205/215/344/270/216/351/252/214/346/224/266.md +107 -0
  28. package/docs//344/270/232/345/212/241/350/203/275/345/212/233/350/241/245/345/205/250/344/270/216Skill/347/273/223/346/236/204/350/247/204/345/210/222.md +311 -0
  29. package/docs//345/205/250/345/261/200/351/231/204/344/273/266/350/265/204/346/272/220/345/234/260/345/235/200/344/270/216/346/250/241/345/236/213/350/257/273/345/217/226/345/267/256/345/274/202.md +47 -0
  30. package/docs//345/206/205/351/203/250Windows/345/256/211/350/243/205/344/270/216/345/215/207/347/272/247/351/252/214/346/224/266/346/270/205/345/215/225.md +34 -0
  31. package/docs//345/233/276/347/211/207/346/212/240/345/233/276/346/216/245/345/205/245/344/270/216/351/252/214/346/224/266-20261104.md +110 -0
  32. package/docs//345/274/200/345/217/221/350/256/241/345/210/222.md +127 -0
  33. package/docs//345/274/202/346/255/245/346/212/240/345/233/276CLI/346/235/203/351/231/220/344/272/244/346/216/245-20261105.md +50 -0
  34. package/docs//345/274/202/346/255/245/346/212/240/345/233/276/347/234/237/345/256/236/351/252/214/346/224/266-20261105.md +44 -0
  35. package/docs//346/226/260/344/274/232/350/257/235/344/273/273/345/212/241/344/272/244/344/273/243-/351/230/266/346/256/2652A.md +79 -0
  36. package/docs//346/226/260/351/203/250/347/275/262/347/224/273/345/270/203/345/206/262/347/252/201/351/252/214/346/224/266-20261102.md +61 -0
  37. package/docs//346/231/256/351/200/232/346/265/217/350/247/210/345/231/250/347/231/273/345/275/225.md +53 -0
  38. package/docs//346/255/243/345/274/217/347/253/231/345/210/207/346/215/242/344/270/216/351/252/214/346/224/266-20261104.md +90 -0
  39. package/docs//347/224/273/345/270/203CLI/344/270/213/344/270/200/351/230/266/346/256/265/345/274/200/345/217/221/350/256/241/345/210/222.md +136 -0
  40. package/docs//347/224/273/345/270/203CLI/351/235/236/350/275/254/347/273/230/345/256/214/345/226/204/350/256/241/345/210/222.md +183 -0
  41. package/docs//347/224/273/345/270/203/350/203/275/345/212/233/345/245/221/347/272/246/346/240/270/345/257/271.md +59 -0
  42. package/docs//347/234/237/345/256/236/344/270/232/345/212/241/346/216/245/345/205/245/344/270/216/346/265/213/350/257/225.md +1004 -0
  43. package/docs//347/264/240/346/235/220/345/205/245/345/272/223/344/270/216/347/224/273/345/270/203/344/277/235/345/255/230/351/227/256/351/242/230/345/256/232/344/275/215.md +52 -0
  44. package/docs//351/230/266/346/256/2653B/351/252/214/346/224/266/344/272/244/346/216/245.md +44 -0
  45. package/docs//351/230/266/346/256/265/344/270/200/351/252/214/346/224/266.md +60 -0
  46. package/docs//351/235/236/350/275/254/347/273/230/346/214/201/347/273/255/345/256/214/345/226/204/344/270/216/351/252/214/346/224/266-20261103.md +76 -0
  47. package/docs//351/235/236/350/275/254/347/273/230/347/254/254/344/272/214/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +39 -0
  48. package/docs//351/235/236/350/275/254/347/273/230/350/241/245/345/205/205/346/235/241/344/273/266/351/252/214/346/224/266-20261103.md +113 -0
  49. package/docs//351/235/236/350/275/254/347/273/230/351/246/226/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +71 -0
  50. package/examples/matting.json +5 -0
  51. package/examples/real-business/image-request.template.json +7 -0
  52. package/examples/real-business/node-patch.template.json +11 -0
  53. package/extensions/uzoo-cli-login/manifest.json +9 -0
  54. package/extensions/uzoo-cli-login/popup.css +1 -0
  55. package/extensions/uzoo-cli-login/popup.html +14 -0
  56. package/extensions/uzoo-cli-login/popup.js +65 -0
  57. package/package.json +56 -6
  58. package/scripts/check-package-resources.mjs +56 -0
  59. package/scripts/check-release.mjs +19 -0
  60. package/scripts/pack-local.mjs +33 -0
  61. package/scripts/release-policy.mjs +22 -0
  62. package/src/agreement-page.js +62 -0
  63. package/src/agreement.js +419 -0
  64. package/src/annotation-media.js +386 -0
  65. package/src/api.js +168 -0
  66. package/src/browser-login.js +13 -0
  67. package/src/callback-page.js +44 -0
  68. package/src/canvas.js +105 -0
  69. package/src/cli-authorization.js +122 -0
  70. package/src/cli.js +459 -0
  71. package/src/download-name.js +17 -0
  72. package/src/draft-finalize.js +229 -0
  73. package/src/generation-graph.js +124 -0
  74. package/src/generation-input.js +472 -0
  75. package/src/graph-disconnect.js +89 -0
  76. package/src/graph.js +431 -0
  77. package/src/group-edit.js +53 -0
  78. package/src/group-layout.js +113 -0
  79. package/src/group-membership.js +126 -0
  80. package/src/history-add.js +119 -0
  81. package/src/image-annotation.js +246 -0
  82. package/src/image-crop.js +283 -0
  83. package/src/image-matting.js +82 -0
  84. package/src/image-media.js +89 -0
  85. package/src/image-output.js +385 -0
  86. package/src/local-output.js +23 -0
  87. package/src/material-download.js +55 -0
  88. package/src/material-save.js +254 -0
  89. package/src/materials.js +147 -0
  90. package/src/matting-background.js +73 -0
  91. package/src/matting-jobs.js +63 -0
  92. package/src/matting-worker.js +49 -0
  93. package/src/matting.js +170 -0
  94. package/src/media-copy.js +53 -0
  95. package/src/media.js +343 -0
  96. package/src/model-options.js +76 -0
  97. package/src/multipart.js +28 -0
  98. package/src/node-copy.js +95 -0
  99. package/src/node-delete.js +49 -0
  100. package/src/node-history.js +231 -0
  101. package/src/normal-login.js +118 -0
  102. package/src/package-info.js +8 -0
  103. package/src/png-alpha.js +60 -0
  104. package/src/protocol.js +83 -0
  105. package/src/remake.js +738 -0
  106. package/src/repaint-archive.js +53 -0
  107. package/src/repaint-delivery.js +783 -0
  108. package/src/repaint-export.js +63 -0
  109. package/src/repaint-graph.js +214 -0
  110. package/src/repaint-history.js +267 -0
  111. package/src/repaint-materials.js +601 -0
  112. package/src/repaint-reverse.js +614 -0
  113. package/src/repaint-sequence.js +132 -0
  114. package/src/repaint-settings.js +576 -0
  115. package/src/repaint-video.js +798 -0
  116. package/src/repaint.js +326 -0
  117. package/src/resource.js +110 -0
  118. package/src/review.js +435 -0
  119. package/src/service.js +85 -0
  120. package/src/store.js +100 -0
  121. package/src/video-clip.js +381 -0
  122. package/src/video-frame.js +296 -0
  123. package/src/workflows.js +214 -0
package/src/remake.js ADDED
@@ -0,0 +1,738 @@
1
+ import crypto from 'node:crypto'
2
+ import path from 'node:path'
3
+ import { setTimeout as sleep } from 'node:timers/promises'
4
+ import { atomic, json, root, safeName } from './store.js'
5
+ import { scopedCanvas, materialScope } from './materials.js'
6
+ import { readSource, normalizeMode, promptInput } from './generation-input.js'
7
+ import { sameVideoReferences } from './media.js'
8
+ import { segment } from './workflows.js'
9
+ import { readProtocol, agreementError } from './agreement.js'
10
+ import { cancellation } from './api.js'
11
+
12
+ const text = (v) => String(v ?? '').trim()
13
+ const hash = (v) => crypto.createHash('sha256').update(JSON.stringify(v)).digest('hex')
14
+ const fail = (code, message, error) =>
15
+ Object.assign(new Error(message), {
16
+ code,
17
+ ...(error
18
+ ? {
19
+ cause: error,
20
+ diagnostic: {
21
+ ...(error.status ? { http_status: error.status } : {}),
22
+ ...(error.apiCode ? { api_code: error.apiCode } : {}),
23
+ ...(error.requestId ? { request_id: error.requestId } : {})
24
+ }
25
+ }
26
+ : {})
27
+ })
28
+ export const remakePath = (profile, operation) =>
29
+ path.join(root(), 'remakes', safeName(profile), safeName(operation) + '.json')
30
+ // Server prompt sanitization uses NFKC. Only treat compatibility punctuation as
31
+ // equivalent here; other rewritten text still requires investigation.
32
+ const comparablePrompt = (value) => text(value).normalize('NFKC')
33
+ export function remakeInput(value) {
34
+ if (
35
+ !value ||
36
+ typeof value !== 'object' ||
37
+ Array.isArray(value) ||
38
+ Object.keys(value).some(
39
+ (key) =>
40
+ ![
41
+ 'doc_id',
42
+ 'node_id',
43
+ 'space',
44
+ 'team_uid',
45
+ 'start_ms',
46
+ 'end_ms',
47
+ 'model_name',
48
+ 'resolution',
49
+ 'prompt'
50
+ ].includes(key)
51
+ )
52
+ )
53
+ throw new Error('重拍输入无效或包含未接入字段;本轮仅源视频,不支持附加参考/时长/比例/声音开关')
54
+ for (const key of ['doc_id', 'node_id', 'model_name', 'resolution', 'prompt'])
55
+ if (typeof value[key] !== 'string' || !text(value[key])) throw new Error('重拍缺少 ' + key)
56
+ if (value.prompt.length > 20000 || /<[^>]+>|\[\[.*?\]\]/.test(value.prompt))
57
+ throw new Error('重拍提示词须为不含素材占位符的纯文本,最长20000字符')
58
+ if (
59
+ ![value.start_ms, value.end_ms].every(Number.isSafeInteger) ||
60
+ value.start_ms < 0 ||
61
+ value.end_ms - value.start_ms < 1000 ||
62
+ value.end_ms - value.start_ms > 120000
63
+ )
64
+ throw new Error('重拍使用整数毫秒,区间须在1–120秒内,另按当前模型限制校验')
65
+ materialScope(value.space, value.team_uid)
66
+ return Object.fromEntries(
67
+ Object.entries(value).map(([key, v]) => [key, typeof v === 'string' ? v.trim() : v])
68
+ )
69
+ }
70
+ export function mediaNodeSize(width, height) {
71
+ const ratio = width / height
72
+ return ratio >= 1
73
+ ? { width: Math.round(272 * ratio) + 2, height: 300 }
74
+ : { width: 274, height: Math.round(272 / ratio) + 28 }
75
+ }
76
+ async function sourceContext(client, input) {
77
+ const doc = await scopedCanvas(client, input.doc_id, input.space, input.team_uid)
78
+ const node = doc.nodes.find((n) => n.id === input.node_id),
79
+ data = node?.data || {}
80
+ if (
81
+ !node ||
82
+ !['video', 'video_upload'].includes(node.type) ||
83
+ data.extra?.repaint ||
84
+ data.extra?.video_remake ||
85
+ data.extra?.clip_redraw
86
+ )
87
+ throw new Error('重拍来源须为普通视频生成/素材节点')
88
+ const source = readSource(node)
89
+ const refs = [
90
+ data.resultFileRef,
91
+ data.fileRef,
92
+ data.file_ref,
93
+ data.extra?.file_ref,
94
+ data.result?.file_ref,
95
+ data.node_output?.file_ref
96
+ ]
97
+ .map(text)
98
+ .filter(Boolean)
99
+ const taskId = text(
100
+ data.sourceTaskId || data.extra?.source_task_id || data.last_task_id || data.lastTaskId
101
+ )
102
+ if (
103
+ !source.url ||
104
+ source.uploading ||
105
+ source.uploadError ||
106
+ new Set(refs).size !== 1 ||
107
+ !/^[1-9]\d*$/.test(taskId)
108
+ )
109
+ throw new Error('重拍要求同画布真实完成视频任务与一致媒体引用')
110
+ const task = await client.raw('/canvas/generations/' + segment(taskId))
111
+ if (
112
+ text(task.task_id ?? task.id) !== taskId ||
113
+ task.doc_id !== doc.id ||
114
+ task.segment_type !== 'video' ||
115
+ task.status !== 'completed' ||
116
+ !(await sameVideoReferences(client, refs[0], task.result?.file_ref))
117
+ )
118
+ throw new Error('重拍来源与同画布已完成视频任务不匹配')
119
+ const width = Number(task.result?.width),
120
+ height = Number(task.result?.height),
121
+ duration = Number(task.result?.duration)
122
+ if (
123
+ ![width, height].every((v) => Number.isSafeInteger(v) && v > 0) ||
124
+ !Number.isFinite(duration) ||
125
+ duration <= 0 ||
126
+ input.end_ms > Math.round(duration * 1000)
127
+ )
128
+ throw new Error('重拍区间超出真实视频,或缺少源片尺寸/时长')
129
+ return {
130
+ doc,
131
+ node,
132
+ binding: { task_id: taskId, file_ref: refs[0], width, height, duration },
133
+ url: client.baseUrl + '/files/' + encodeURIComponent(refs[0])
134
+ }
135
+ }
136
+ async function identity(client, input) {
137
+ const me = await client.raw('/auth/me')
138
+ if (!text(me.uid)) throw new Error('重拍账号信息缺少UID')
139
+ const team =
140
+ input.space === 'team' &&
141
+ me.teams?.find((t) => text(t.uid) === input.team_uid && t.status === 'active')
142
+ if (input.space === 'team' && !team) throw new Error('当前账号没有目标团队的有效成员身份')
143
+ return {
144
+ owner: text(me.uid),
145
+ wallet: {
146
+ space: input.space,
147
+ team_uid: input.team_uid || null,
148
+ ...(team ? { team_name: team.name, shared: true } : {}),
149
+ balance_snapshot: (team || me).oo_balance ?? (team || me).credit_balance ?? null,
150
+ currency: 'OO'
151
+ }
152
+ }
153
+ }
154
+ async function validateModel(client, input) {
155
+ const catalog = await client.raw(
156
+ '/canvas/models' + (input.space === 'team' ? '?team_uid=' + segment(input.team_uid) : '')
157
+ )
158
+ const model = catalog.items?.find(
159
+ (m) => m.label === input.model_name && m.content_type === 'video' && !m.member_locked
160
+ )
161
+ if (!model || !model.generation_modes?.map(normalizeMode).includes('video_editing'))
162
+ throw new Error('模型未在当前空间声明支持视频编辑/重拍')
163
+ if (!model.resolutions?.includes(input.resolution))
164
+ throw new Error('重拍分辨率不在当前模型可选项内')
165
+ if (model.supports_draft === true && /draft|样片/i.test(model.label))
166
+ throw new Error('片段重拍不支持样片模型')
167
+ const seconds = (input.end_ms - input.start_ms) / 1000
168
+ if (
169
+ (model.durations?.min != null && seconds < Number(model.durations.min)) ||
170
+ (model.durations?.max != null && seconds > Number(model.durations.max))
171
+ )
172
+ throw new Error('片段时长不在当前模型的重拍范围内')
173
+ if (model.prompt_max_length && input.prompt.length > model.prompt_max_length)
174
+ throw new Error('提示词超出当前模型长度限制')
175
+ return model
176
+ }
177
+ function configNode(record, current) {
178
+ const { input, binding } = record
179
+ return {
180
+ id: record.configId,
181
+ type: 'video',
182
+ ...mediaNodeSize(binding.width, binding.height),
183
+ position: {
184
+ x: Math.round(Number(current.node.position?.x || 0) + Number(current.node.width || 486) + 80),
185
+ y: Math.round(Number(current.node.position?.y || 0))
186
+ },
187
+ data: {
188
+ title: '片段重拍',
189
+ prompt: input.prompt,
190
+ model_name: input.model_name,
191
+ duration: (input.end_ms - input.start_ms) / 1000,
192
+ extra: {
193
+ node_kind: 'generation',
194
+ generation_mode: 'video_editing',
195
+ resolution: input.resolution,
196
+ aspect_ratio: binding.width + ':' + binding.height,
197
+ video_remake: {
198
+ role: 'node',
199
+ sourceNodeId: input.node_id,
200
+ sourceRef: binding.file_ref,
201
+ sourceUrl: current.url,
202
+ sourceDuration: binding.duration,
203
+ clipStart: input.start_ms / 1000,
204
+ clipEnd: input.end_ms / 1000
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ function configMatches(node, record) {
211
+ const data = node?.data || {},
212
+ remake = data.extra?.video_remake,
213
+ input = record.input
214
+ return (
215
+ ['video', 'video_upload'].includes(node?.type) &&
216
+ data.model_name === input.model_name &&
217
+ data.prompt === input.prompt &&
218
+ data.extra?.resolution === input.resolution &&
219
+ data.extra?.generation_mode === 'video_editing' &&
220
+ remake?.role === 'node' &&
221
+ remake.sourceNodeId === input.node_id &&
222
+ remake.sourceRef === record.binding.file_ref &&
223
+ remake.clipStart === input.start_ms / 1000 &&
224
+ remake.clipEnd === input.end_ms / 1000
225
+ )
226
+ }
227
+ const edge = (id, source, target, role) => ({
228
+ id,
229
+ source,
230
+ target,
231
+ sourceHandle: '',
232
+ targetHandle: '',
233
+ type: 'default',
234
+ data: { role, repaintSystem: true }
235
+ })
236
+ const edgeExists = (doc, source, target, role) =>
237
+ doc.edges.some((e) => e.source === source && e.target === target && e.data?.role === role)
238
+ const summary = (r) => ({
239
+ operation: r.operation,
240
+ state: r.state,
241
+ doc_id: r.input.doc_id,
242
+ source_node_id: r.input.node_id,
243
+ configuration_node_id: r.configId,
244
+ result_node_id: r.task ? r.resultId : null,
245
+ task_id: r.task?.id || null,
246
+ canvas_synced: r.canvasSynced === true,
247
+ processing_submitted: Boolean(r.task || r.taskId) ? true : r.submitAttempted ? null : false,
248
+ library_saved: false,
249
+ ...(r.task ? { task: r.task } : {}),
250
+ ...(r.lastFailure ? { last_failure: r.lastFailure } : {})
251
+ })
252
+ async function bound(client, profile, operation) {
253
+ const record = await json(remakePath(profile, operation), null)
254
+ if (!record) throw new Error('未找到原重拍operation')
255
+ if (
256
+ record.baseUrl !== client.baseUrl ||
257
+ record.owner !== (await identity(client, record.input)).owner
258
+ )
259
+ throw new Error('重拍operation的账号或服务不匹配')
260
+ const current = await sourceContext(client, record.input)
261
+ const { file_ref: oldRef, ...oldBinding } = record.binding,
262
+ { file_ref: newRef, ...newBinding } = current.binding
263
+ if (hash(oldBinding) !== hash(newBinding) || !(await sameVideoReferences(client, oldRef, newRef)))
264
+ throw new Error('重拍源任务或素材已变化,停止恢复')
265
+ const config = current.doc.nodes.find((n) => n.id === record.configId)
266
+ if (
267
+ config &&
268
+ (!configMatches(config, record) ||
269
+ !edgeExists(current.doc, record.input.node_id, config.id, 'video-remake-input'))
270
+ )
271
+ throw new Error('重拍配置/来源线已变化,停止覆盖')
272
+ if (!config && (record.configSaved || record.configSaveAttempted))
273
+ throw fail('REMAKE_SYNC_PENDING', '原配置节点缺失或保存未知;不自动重建')
274
+ return { record, current, config }
275
+ }
276
+ async function save(client, profile, record, doc, nodes, edges, attemptedKey) {
277
+ if (!Number.isSafeInteger(doc.version) || !doc.viewport || !doc.meta || !Array.isArray(doc.edges))
278
+ throw new Error('重拍画布快照不完整')
279
+ record[attemptedKey] = true
280
+ await atomic(remakePath(profile, record.operation), record)
281
+ try {
282
+ const saved = await client.raw(
283
+ '/canvas/docs/' + segment(doc.id),
284
+ {
285
+ method: 'PUT',
286
+ body: {
287
+ version: doc.version,
288
+ name: doc.name,
289
+ viewport: doc.viewport,
290
+ nodes,
291
+ edges,
292
+ meta: doc.meta
293
+ }
294
+ },
295
+ { retry: false }
296
+ )
297
+ if (
298
+ saved.id !== doc.id ||
299
+ saved.version <= doc.version ||
300
+ !Array.isArray(saved.nodes) ||
301
+ !Array.isArray(saved.edges)
302
+ )
303
+ throw new Error('画布保存响应不完整')
304
+ return saved
305
+ } catch (error) {
306
+ if (error.status === 409 || error.apiCode === 3002) record[attemptedKey] = false
307
+ record.state = 'sync_pending'
308
+ await atomic(remakePath(profile, record.operation), record)
309
+ throw fail(
310
+ 'REMAKE_SYNC_PENDING',
311
+ '重拍画布保存未确认;查原节点后恢复原operation,不重拍或重建缺失节点',
312
+ error
313
+ )
314
+ }
315
+ }
316
+ async function persistConfig(client, profile, record, current) {
317
+ const existing = current.doc.nodes.find((n) => n.id === record.configId)
318
+ if (!existing) {
319
+ if (record.configSaved || record.configSaveAttempted)
320
+ throw fail('REMAKE_SYNC_PENDING', '配置保存未知或节点被删,不自动重建')
321
+ const node = configNode(record, current),
322
+ link = edge(
323
+ 'remake-input-' + record.configId,
324
+ record.input.node_id,
325
+ record.configId,
326
+ 'video-remake-input'
327
+ )
328
+ const saved = await save(
329
+ client,
330
+ profile,
331
+ record,
332
+ current.doc,
333
+ [...current.doc.nodes, node],
334
+ [...current.doc.edges, link],
335
+ 'configSaveAttempted'
336
+ )
337
+ if (
338
+ !configMatches(
339
+ saved.nodes.find((n) => n.id === node.id),
340
+ record
341
+ ) ||
342
+ !edgeExists(saved, record.input.node_id, node.id, 'video-remake-input')
343
+ )
344
+ throw fail('REMAKE_SYNC_PENDING', '重拍配置保存结果不完整')
345
+ } else if (
346
+ !configMatches(existing, record) ||
347
+ !edgeExists(current.doc, record.input.node_id, existing.id, 'video-remake-input')
348
+ )
349
+ throw new Error('原重拍配置已变化')
350
+ record.configSaved = true
351
+ record.state = 'prepared'
352
+ await atomic(remakePath(profile, record.operation), record)
353
+ return summary(record)
354
+ }
355
+ export async function prepareRemake(client, profile, value, operation, confirmed = false) {
356
+ const input = remakeInput(value),
357
+ file = remakePath(profile, operation),
358
+ old = await json(file, null)
359
+ if (old) {
360
+ if (hash(old.input) !== hash(input)) throw new Error('原重拍operation不能修改输入')
361
+ return getRemake(client, profile, operation)
362
+ }
363
+ const current = await sourceContext(client, input),
364
+ account = await identity(client, input)
365
+ await validateModel(client, input)
366
+ if (!confirmed)
367
+ return {
368
+ operation,
369
+ configuration_saved: false,
370
+ processing_submitted: false,
371
+ library_saved: false,
372
+ input,
373
+ source: {
374
+ task_id: current.binding.task_id,
375
+ width: current.binding.width,
376
+ height: current.binding.height,
377
+ duration: current.binding.duration
378
+ },
379
+ wallet: account.wallet,
380
+ next: '核对区间/参数并暂停画布编辑后,prepare加--yes仅创建重拍配置和来源线;之后submit预估,不生成'
381
+ }
382
+ const record = {
383
+ operation,
384
+ owner: account.owner,
385
+ baseUrl: client.baseUrl,
386
+ input,
387
+ binding: current.binding,
388
+ configId: 'remake-' + crypto.randomUUID(),
389
+ resultId: 'remake-result-' + crypto.randomUUID(),
390
+ state: 'preparing',
391
+ createdAt: new Date().toISOString()
392
+ }
393
+ await atomic(file, record)
394
+ return persistConfig(client, profile, record, current)
395
+ }
396
+ export function estimateBody(record, current) {
397
+ const input = record.input
398
+ const source = readSource(current.node)
399
+ const prompt = promptInput({ data: { prompt: input.prompt } }, [source], [], [source], {
400
+ image: [],
401
+ video: [source.url],
402
+ audio: []
403
+ }).nodePrompt.trim()
404
+ return {
405
+ doc_id: input.doc_id,
406
+ node_id: record.configId,
407
+ segment_type: 'video',
408
+ prompt,
409
+ model_name: input.model_name,
410
+ duration: Math.round((input.end_ms - input.start_ms) / 1000),
411
+ resolution: input.resolution,
412
+ reference_video_urls: [source.url],
413
+ reference_image_urls: [],
414
+ reference_audio_urls: [],
415
+ reference_asset_ids: [],
416
+ reference_canvas_task_ids: [],
417
+ extra: { generation_mode: 'video_editing' },
418
+ protocol_agreed: false
419
+ }
420
+ }
421
+ async function quote(client, record, current) {
422
+ await validateModel(client, record.input)
423
+ const estimate = await client.raw('/canvas/generations/estimate', {
424
+ method: 'POST',
425
+ body: estimateBody(record, current)
426
+ })
427
+ if (
428
+ !Number.isFinite(Number(estimate.estimated_credit_cost)) ||
429
+ Number(estimate.estimated_credit_cost) < 0 ||
430
+ estimate.estimated_credit_cost == null ||
431
+ estimate.will_intercept
432
+ )
433
+ throw new Error('重拍预估未通过或费用缺失,停止提交')
434
+ return estimate
435
+ }
436
+ export function submissionBody(record, sourceRef = record.binding.file_ref) {
437
+ return {
438
+ source_ref: sourceRef,
439
+ start_ms: record.input.start_ms,
440
+ end_ms: record.input.end_ms,
441
+ model_name: record.input.model_name,
442
+ prompt: record.input.prompt,
443
+ resolution: record.input.resolution,
444
+ generation_mode: 'video_editing',
445
+ protocol_agreed: false
446
+ }
447
+ }
448
+ function validateTask(response, record) {
449
+ const task = response?.task || response,
450
+ clip = response?.clip || task?.clip
451
+ if (
452
+ !task ||
453
+ !/^[a-zA-Z0-9_-]{1,100}$/.test(text(task.id)) ||
454
+ !clip ||
455
+ text(clip.source_task_id) !== record.binding.task_id ||
456
+ clip.start_ms !== record.input.start_ms ||
457
+ clip.end_ms !== record.input.end_ms ||
458
+ Number(clip.id) <= 0 ||
459
+ Number(task.clip_id) !== Number(clip.id) ||
460
+ comparablePrompt(task.prompt) !== comparablePrompt(record.input.prompt) ||
461
+ task.model_name !== record.input.model_name ||
462
+ task.resolution !== record.input.resolution ||
463
+ !['queued', 'processing', 'retrying', 'completed', 'failed', 'cancelled'].includes(task.status)
464
+ )
465
+ throw new Error('重拍任务来源/区间/参数响应不完整,不重发提交')
466
+ if (record.task && text(record.task.id) !== text(task.id)) throw new Error('查询返回不同重拍任务')
467
+ return { ...structuredClone(task), clip: structuredClone(clip) }
468
+ }
469
+ export async function submitRemake(
470
+ client,
471
+ profile,
472
+ operation,
473
+ confirmed = false,
474
+ expectedCost,
475
+ options = {}
476
+ ) {
477
+ let { record, current, config } = await bound(client, profile, operation)
478
+ if (!config || !record.configSaved) throw new Error('先恢复原配置保存,再预估')
479
+ if (record.submitAttempted) return getRemake(client, profile, operation)
480
+ let estimate = await quote(client, record, current)
481
+ if (!confirmed) {
482
+ record.estimate = estimate
483
+ await atomic(remakePath(profile, operation), record)
484
+ return {
485
+ ...summary(record),
486
+ submitted: false,
487
+ parameters: record.input,
488
+ estimate,
489
+ wallet: (await identity(client, record.input)).wallet,
490
+ next: '展示原视频/区间/模型/提示词/钱包和费用,取得当次确认后submit原operation加--yes --expected-cost;不能保证重发幂等'
491
+ }
492
+ }
493
+ const cost = Number(expectedCost)
494
+ if (
495
+ expectedCost === undefined ||
496
+ !Number.isFinite(cost) ||
497
+ cost < 0 ||
498
+ !record.estimate ||
499
+ Number(record.estimate.estimated_credit_cost) !== cost ||
500
+ Number(estimate.estimated_credit_cost) !== cost
501
+ )
502
+ throw new Error('重拍须先预估并当次确认,--expected-cost须与原预估及实时费用一致')
503
+ let protocol = await readProtocol(client)
504
+ if (protocol.required && !protocol.agreed) {
505
+ if (!options.onAgreementRequired)
506
+ throw agreementError(
507
+ 'AGREEMENT_REQUIRED',
508
+ '重拍前需本人签署《统一协议》《Seedance2.0模型使用承诺书》(code4401),尚未提交重拍',
509
+ protocol
510
+ )
511
+ await options.onAgreementRequired({
512
+ operation,
513
+ body: { model_name: record.input.model_name },
514
+ estimate
515
+ })
516
+ const fresh = await bound(client, profile, operation)
517
+ record = fresh.record
518
+ current = fresh.current
519
+ const next = await readProtocol(client)
520
+ if (next.version !== protocol.version || (next.required && !next.agreed))
521
+ throw agreementError('AGREEMENT_CHANGED', '协议版本或状态变化,停止重拍', next)
522
+ protocol = next
523
+ estimate = await quote(client, record, current)
524
+ if (Number(estimate.estimated_credit_cost) !== cost)
525
+ throw new Error('协议确认后重拍费用已变化,重新预估确认')
526
+ }
527
+ const fresh = await bound(client, profile, operation)
528
+ record = fresh.record
529
+ current = fresh.current
530
+ // No server idempotency contract. Persist the attempt before the only POST.
531
+ record.submitAttempted = true
532
+ record.state = 'submitting'
533
+ record.approvedCost = cost
534
+ await atomic(remakePath(profile, operation), record)
535
+ try {
536
+ const response = await client.raw(
537
+ '/clip-redraw/tasks',
538
+ { method: 'POST', body: submissionBody(record, current.binding.file_ref) },
539
+ { retry: false }
540
+ )
541
+ const id = text((response?.task || response)?.id)
542
+ if (/^[a-zA-Z0-9_-]{1,100}$/.test(id)) {
543
+ record.taskId = id
544
+ await atomic(remakePath(profile, operation), record)
545
+ }
546
+ record.task = validateTask(response, record)
547
+ record.state = 'submitted'
548
+ await atomic(remakePath(profile, operation), record)
549
+ } catch (error) {
550
+ if (error.apiCode === 4401) {
551
+ record.submitAttempted = false
552
+ record.state = 'blocked_agreement'
553
+ record.lastFailure = { api_code: 4401, status: 'awaiting_agreement' }
554
+ await atomic(remakePath(profile, operation), record)
555
+ throw agreementError(
556
+ 'AGREEMENT_REQUIRED',
557
+ '《统一协议》《Seedance2.0模型使用承诺书》需本人确认(code4401);重拍未执行',
558
+ await readProtocol(client)
559
+ )
560
+ }
561
+ record.state = 'submission_unknown'
562
+ record.lastFailure = {
563
+ ...(error.status ? { http_status: error.status } : {}),
564
+ ...(error.apiCode ? { api_code: error.apiCode } : {}),
565
+ ...(error.requestId ? { request_id: error.requestId } : {})
566
+ }
567
+ await atomic(remakePath(profile, operation), record)
568
+ throw fail(
569
+ 'REMAKE_SUBMISSION_UNKNOWN',
570
+ '重拍提交结果未确认;不换operation或重发POST,先查后端与原任务副作用',
571
+ error
572
+ )
573
+ }
574
+ return syncRemake(client, profile, operation)
575
+ }
576
+ export async function getRemake(client, profile, operation) {
577
+ const { record, current, config } = await bound(client, profile, operation)
578
+ if (record.submitAttempted && !record.task && !record.taskId)
579
+ throw fail('REMAKE_SUBMISSION_UNKNOWN', '重拍提交未知且没有可靠查询ID,不自动重提或猜测任务')
580
+ if (record.task || record.taskId) {
581
+ record.task = validateTask(
582
+ await client.raw('/clip-redraw/tasks/' + segment(record.task?.id || record.taskId)),
583
+ record
584
+ )
585
+ if (record.state === 'submission_unknown') record.state = 'submitted'
586
+ await atomic(remakePath(profile, operation), record)
587
+ }
588
+ const result = summary(record)
589
+ if (!config) result.state = 'configuration_pending'
590
+ if (record.task) {
591
+ const node = current.doc.nodes.find((n) => n.id === record.resultId)
592
+ result.canvas_synced = Boolean(
593
+ node &&
594
+ node.data?.extra?.clip_redraw?.taskId === record.task.id &&
595
+ edgeExists(current.doc, record.configId, node.id, 'clip-redraw-link') &&
596
+ node.data.extra.clip_redraw.status === record.task.status &&
597
+ (record.task.status !== 'completed' || node.data?.result?.url === record.task.result?.url)
598
+ )
599
+ if (record.canvasSynced && !node) result.state = 'canvas_changed'
600
+ }
601
+ return result
602
+ }
603
+ export async function syncRemake(client, profile, operation) {
604
+ await getRemake(client, profile, operation)
605
+ const { record, current, config } = await bound(client, profile, operation)
606
+ if (!record.task) return persistConfig(client, profile, record, current)
607
+ const task = record.task,
608
+ clip = task.clip,
609
+ result = task.result
610
+ let node = current.doc.nodes.find((n) => n.id === record.resultId)
611
+ if (!node && (record.resultSaveAttempted || record.canvasSynced))
612
+ throw fail('REMAKE_SYNC_PENDING', '重拍结果节点缺失或保存未知,禁止自动重建')
613
+ if (
614
+ node &&
615
+ (node.type !== 'video_upload' ||
616
+ node.data?.extra?.clip_redraw?.taskId !== task.id ||
617
+ !edgeExists(current.doc, record.configId, node.id, 'clip-redraw-link'))
618
+ )
619
+ throw new Error('重拍结果节点/系统线已变化,停止覆盖')
620
+ if (
621
+ node?.data?.result?.url &&
622
+ node.data.extra.clip_redraw.status === 'completed' &&
623
+ result?.file_ref &&
624
+ !(await sameVideoReferences(client, node.data.extra.file_ref, result.file_ref))
625
+ )
626
+ throw new Error('重拍结果素材已被替换,停止同步')
627
+ const marker = {
628
+ role: 'clip-redraw',
629
+ taskId: task.id,
630
+ taskNo: task.task_no || '',
631
+ clipId: clip.id,
632
+ status: task.status,
633
+ progress: task.progress || 0,
634
+ prompt: task.prompt,
635
+ sourceNodeId: record.configId,
636
+ clip,
637
+ result: result || null,
638
+ errorMessage: task.error_message || ''
639
+ }
640
+ if (task.status === 'completed' && (!result?.url || !result.file_ref))
641
+ throw new Error('重拍任务完成但缺少稳定结果引用,停止落图')
642
+ const data = {
643
+ ...(node?.data || {}),
644
+ title: node?.data?.title || '片段重拍 结果',
645
+ mediaType: 'video',
646
+ status: task.status,
647
+ url: result?.url || '',
648
+ file_ref: result?.file_ref || '',
649
+ file_name: clip.file_name || '',
650
+ file_size: result?.file_size || result?.size || 0,
651
+ width: result?.width || 0,
652
+ height: result?.height || 0,
653
+ duration: result?.duration || (record.input.end_ms - record.input.start_ms) / 1000,
654
+ last_task_id: task.id,
655
+ result: result || null,
656
+ extra: {
657
+ ...(node?.data?.extra || {}),
658
+ node_kind: 'asset',
659
+ file_ref: result?.file_ref || '',
660
+ clip_redraw: marker,
661
+ task_no: task.task_no || '',
662
+ aspect_ratio: result?.width && result?.height ? result.width + ':' + result.height : ''
663
+ }
664
+ }
665
+ const output = {
666
+ ...(node || {
667
+ id: record.resultId,
668
+ type: 'video_upload',
669
+ position: { x: config.position.x + (config.width || 420) + 96, y: config.position.y }
670
+ }),
671
+ data
672
+ }
673
+ const configData = {
674
+ ...config.data,
675
+ extra: {
676
+ ...config.data.extra,
677
+ video_remake: { ...config.data.extra.video_remake, clipId: clip.id, clip },
678
+ clip_redraw: {
679
+ taskId: task.id,
680
+ taskNo: task.task_no || '',
681
+ clipId: clip.id,
682
+ status: task.status,
683
+ progress: task.progress || 0
684
+ }
685
+ }
686
+ }
687
+ const nodes = current.doc.nodes.map((n) =>
688
+ n.id === record.configId ? { ...n, data: configData } : n.id === record.resultId ? output : n
689
+ )
690
+ if (!node) nodes.push(output)
691
+ const edges = node
692
+ ? current.doc.edges
693
+ : [
694
+ ...current.doc.edges,
695
+ edge(
696
+ 'remake-result-' + record.resultId,
697
+ record.configId,
698
+ record.resultId,
699
+ 'clip-redraw-link'
700
+ )
701
+ ]
702
+ const saved = await save(
703
+ client,
704
+ profile,
705
+ record,
706
+ current.doc,
707
+ nodes,
708
+ edges,
709
+ 'resultSaveAttempted'
710
+ )
711
+ if (
712
+ saved.nodes.find((n) => n.id === record.resultId)?.data?.extra?.clip_redraw?.taskId !==
713
+ task.id ||
714
+ !edgeExists(saved, record.configId, record.resultId, 'clip-redraw-link')
715
+ )
716
+ throw fail('REMAKE_SYNC_PENDING', '重拍结果保存响应不完整')
717
+ record.canvasSynced = true
718
+ record.state =
719
+ task.status === 'completed' ? 'completed' : task.status === 'failed' ? 'failed' : 'submitted'
720
+ await atomic(remakePath(profile, operation), record)
721
+ return summary(record)
722
+ }
723
+ export async function recoverRemake(client, profile, operation, confirmed = false) {
724
+ return confirmed ? syncRemake(client, profile, operation) : getRemake(client, profile, operation)
725
+ }
726
+ export async function waitRemake(client, profile, operation, interval = 6000, timeout = 600000) {
727
+ const deadline = Date.now() + timeout
728
+ while (Date.now() < deadline) {
729
+ const status = await getRemake(client, profile, operation)
730
+ if (!status.task_id) throw new Error('重拍尚未提交任务')
731
+ if (['completed', 'failed', 'cancelled'].includes(status.task.status))
732
+ return syncRemake(client, profile, operation)
733
+ await sleep(Math.min(interval, Math.max(1, deadline - Date.now())), undefined, {
734
+ signal: cancellation.signal
735
+ })
736
+ }
737
+ throw fail('WAIT_TIMEOUT', '重拍等待超时,保留原任务,用原operation继续wait/recover')
738
+ }