@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
@@ -0,0 +1,798 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { isDeepStrictEqual } from 'node:util'
4
+ import { setTimeout as sleep } from 'node:timers/promises'
5
+ import { atomic, json, root, safeName } from './store.js'
6
+ import { repaintPromptContext } from './repaint-reverse.js'
7
+ import { repaintOf, jobPath } from './repaint-graph.js'
8
+ import {
9
+ collectGenerationSources,
10
+ buildGenerationRequest,
11
+ normalizeMode,
12
+ readSource
13
+ } from './generation-input.js'
14
+ import { requireServiceReady } from './service.js'
15
+ import { readProtocol, agreementError } from './agreement.js'
16
+ import { archiveRepaintResult } from './repaint-archive.js'
17
+ import { cancellation } from './api.js'
18
+
19
+ const text = (value) => String(value ?? '').trim()
20
+ const fail = (code, message, cause) =>
21
+ Object.assign(new Error(message), {
22
+ code,
23
+ ...(cause
24
+ ? {
25
+ cause,
26
+ diagnostic: {
27
+ ...(cause.status ? { http_status: cause.status } : {}),
28
+ ...(cause.apiCode ? { api_code: cause.apiCode } : {}),
29
+ ...(cause.requestId ? { request_id: cause.requestId } : {})
30
+ }
31
+ }
32
+ : {})
33
+ })
34
+ const api = (client, endpoint, options = {}, controls) =>
35
+ client.raw(
36
+ endpoint,
37
+ {
38
+ ...options,
39
+ headers: { ...options.headers, 'X-Client-Source': 'canvas' }
40
+ },
41
+ controls
42
+ )
43
+ export const repaintVideoPath = (profile, operation) =>
44
+ path.join(
45
+ root(),
46
+ 'repaint-video',
47
+ safeName(profile),
48
+ safeName(operation) + '.json'
49
+ )
50
+ const persist = (record) =>
51
+ atomic(repaintVideoPath(record.profile, record.operation), record)
52
+ const statuses = ['queued', 'processing', 'retrying', 'completed', 'failed']
53
+ const pending = (task) =>
54
+ ['queued', 'processing', 'retrying'].includes(task.status)
55
+
56
+ export function repaintVideoInput(value) {
57
+ if (
58
+ !value ||
59
+ typeof value !== 'object' ||
60
+ Array.isArray(value) ||
61
+ Object.keys(value).some(
62
+ (k) =>
63
+ !['model_name', 'duration', 'aspect_ratio', 'resolution'].includes(k)
64
+ )
65
+ )
66
+ throw new Error(
67
+ '转绘视频只支持model_name、duration、aspect_ratio、resolution;不接入声音、样片、额外参考或批量字段'
68
+ )
69
+ if (
70
+ !Number.isSafeInteger(value.duration) ||
71
+ value.duration < 1 ||
72
+ value.duration > 60
73
+ )
74
+ throw new Error('转绘视频时长须为1–60整数秒,再按实际模型范围校验')
75
+ for (const key of ['model_name', 'aspect_ratio', 'resolution'])
76
+ if (typeof value[key] !== 'string' || !text(value[key]))
77
+ throw new Error('转绘视频缺少明确选择的' + key)
78
+ return {
79
+ model_name: text(value.model_name),
80
+ duration: value.duration,
81
+ aspect_ratio: text(value.aspect_ratio),
82
+ resolution: text(value.resolution)
83
+ }
84
+ }
85
+ const configData = (data, input) => ({
86
+ ...data,
87
+ model_name: input.model_name,
88
+ duration: input.duration,
89
+ aspect_ratio: input.aspect_ratio,
90
+ resolution: input.resolution,
91
+ extra: {
92
+ ...data.extra,
93
+ aspect_ratio: input.aspect_ratio,
94
+ resolution: input.resolution,
95
+ generation_mode: 'text_to_video'
96
+ }
97
+ })
98
+ // Compare effective persisted inputs/results, tolerating web codec defaults and
99
+ // top-level/extra mirrors. Layout, title and unrelated canvas edits are retained.
100
+ export function repaintVideoNodeView(node) {
101
+ const d = node.data || {},
102
+ e = d.extra || {}
103
+ return {
104
+ prompt: String(d.prompt || ''),
105
+ model: text(d.model_name || d.model),
106
+ duration: Number(d.duration ?? e.duration) || 0,
107
+ aspect_ratio: text(d.aspect_ratio || e.aspect_ratio),
108
+ resolution: text(d.resolution || e.resolution),
109
+ mode: normalizeMode(e.generation_mode || d.generation_mode || d.videoMode),
110
+ last_task_id: text(d.last_task_id || d.lastTaskId),
111
+ status: text(d.status),
112
+ result: d.result || null,
113
+ file_ref: text(e.file_ref || d.resultFileRef),
114
+ error_message: text(d.error_message || d.errorMessage),
115
+ seedance_auto_review:
116
+ (d.seedanceAutoReview ?? e.seedance_auto_review) === true
117
+ }
118
+ }
119
+ const nodeView = repaintVideoNodeView
120
+ async function context(client, profile, parent, index) {
121
+ const ctx = await repaintPromptContext(client, profile, parent, index)
122
+ const group = ctx.doc.meta.canvas_groups.find(
123
+ (g) => g.id === ctx.binding.group_id
124
+ )
125
+ const nodes = ctx.doc.nodes.filter(
126
+ (n) =>
127
+ group.nodeIds.includes(n.id) &&
128
+ repaintOf(n).jobId === ctx.job.id &&
129
+ repaintOf(n).role === 'generation' &&
130
+ repaintOf(n).segmentIndex === index
131
+ )
132
+ if (nodes.length !== 1 || nodes[0].type !== 'video')
133
+ throw new Error('缺少唯一原转绘视频生成节点,不创建占位节点')
134
+ const node = nodes[0],
135
+ incoming = ctx.doc.edges.filter((e) => e.target === node.id)
136
+ if (
137
+ incoming.length !== 1 ||
138
+ incoming[0].source !== ctx.final.id ||
139
+ incoming[0].data?.role !== 'final-generation-link'
140
+ )
141
+ throw new Error(
142
+ '本轮仅原最终提示词→生成节点;额外入边或系统线缺失须另行接入,不静默丢参考'
143
+ )
144
+ const inputs = collectGenerationSources(ctx.doc, node)
145
+ if (
146
+ inputs.references.length ||
147
+ inputs.all.some((s) => s.mediaType !== 'text')
148
+ )
149
+ throw new Error('本轮画布转绘不接额外参考/素材池附件;不静默忽略或漏算')
150
+ if (
151
+ !readSource(ctx.final).text ||
152
+ !ctx.job.delivery_prompts?.some(
153
+ (p) => p.segment_index === index && text(p.prompt)
154
+ ) ||
155
+ ctx.output.delivery_prompt_status !== 'completed' ||
156
+ ctx.output.delivery_prompt_fallback
157
+ )
158
+ throw new Error(
159
+ '先完成并核验该段正常最终提示词;不能将未处理或回退文本直接当成正常最终结果'
160
+ )
161
+ if (
162
+ node.data?.generation_config ||
163
+ node.data?.generate_audio !== undefined ||
164
+ node.data?.generateAudio !== undefined
165
+ )
166
+ throw new Error('生成节点存在未接入覆盖配置或声音字段,先核对前端生效配置')
167
+ return {
168
+ ...ctx,
169
+ generation: node,
170
+ inputs,
171
+ binding: {
172
+ ...ctx.binding,
173
+ reverse_node_id: ctx.node.id,
174
+ node_id: node.id,
175
+ final_id: ctx.final.id,
176
+ final_text: readSource(ctx.final).text
177
+ }
178
+ }
179
+ }
180
+ const eligible = (m) =>
181
+ m.content_type === 'video' &&
182
+ !m.member_locked &&
183
+ m.is_sample_mode !== true &&
184
+ !/draft|样片/i.test(m.label) &&
185
+ m.generation_modes?.map(normalizeMode).includes('text_to_video')
186
+ async function catalog(client, ctx) {
187
+ const data = await client.raw(
188
+ '/canvas/models' +
189
+ (ctx.doc.team_uid
190
+ ? '?team_uid=' + encodeURIComponent(ctx.doc.team_uid)
191
+ : '')
192
+ )
193
+ if (!Array.isArray(data.items)) throw new Error('模型目录不完整')
194
+ return data.items
195
+ }
196
+ export async function repaintVideoOptions(client, profile, parent, index) {
197
+ await requireServiceReady(client)
198
+ const ctx = await context(client, profile, parent, index),
199
+ models = await catalog(client, ctx)
200
+ return {
201
+ repaint_operation: parent,
202
+ job_id: ctx.job.id,
203
+ plan_version: ctx.job.plan_version,
204
+ segment_index: index,
205
+ node_id: ctx.generation.id,
206
+ source_range_ms: [ctx.binding.start_ms, ctx.binding.end_ms],
207
+ workflow: 'canvas_redraw',
208
+ generation_mode: 'text_to_video',
209
+ source_video_sent_as_reference: false,
210
+ reference_images: [],
211
+ reference_audio: [],
212
+ current: {
213
+ model_name: nodeView(ctx.generation).model,
214
+ duration: nodeView(ctx.generation).duration,
215
+ aspect_ratio: nodeView(ctx.generation).aspect_ratio,
216
+ resolution: nodeView(ctx.generation).resolution
217
+ },
218
+ models: models.filter(eligible).map((m) => ({
219
+ label: m.label,
220
+ aspect_ratios: m.aspect_ratios || [],
221
+ resolutions: m.resolutions || [],
222
+ durations: m.durations || null,
223
+ prompt_max_length: m.prompt_max_length || null
224
+ })),
225
+ submitted: false,
226
+ next: '使用交互卡片选择模型和实际参数,再repaint video预估;不自动选模型或发起收费任务'
227
+ }
228
+ }
229
+ async function prepare(client, ctx, input) {
230
+ const model = (await catalog(client, ctx)).find(
231
+ (m) => m.label === input.model_name && eligible(m)
232
+ )
233
+ return repaintVideoRequests(ctx, model, input)
234
+ }
235
+ export function repaintVideoRequests(ctx, model, input) {
236
+ if (!model) throw new Error('模型不在当前空间可用的无额外参考转绘选项内')
237
+ if (
238
+ !model.aspect_ratios?.includes(input.aspect_ratio) ||
239
+ !model.resolutions?.includes(input.resolution)
240
+ )
241
+ throw new Error('比例或分辨率不在当前模型选项内')
242
+ const min = Number(model.durations?.min),
243
+ max = Number(model.durations?.max)
244
+ if (
245
+ !Number.isFinite(min) ||
246
+ !Number.isFinite(max) ||
247
+ input.duration < min ||
248
+ input.duration > max
249
+ )
250
+ throw new Error('当前模型时长范围未知或所选时长不合规')
251
+ const configured = {
252
+ ...ctx.generation,
253
+ data: configData(ctx.generation.data, input)
254
+ }
255
+ const { request } = buildGenerationRequest(
256
+ ctx.doc,
257
+ configured,
258
+ model,
259
+ ctx.inputs
260
+ )
261
+ if (
262
+ request.extra.generation_mode !== 'text_to_video' ||
263
+ [
264
+ 'reference_image_urls',
265
+ 'reference_video_urls',
266
+ 'reference_audio_urls',
267
+ 'reference_asset_ids',
268
+ 'reference_canvas_task_ids'
269
+ ].some((k) => request[k]?.length)
270
+ )
271
+ throw new Error(
272
+ '本轮仅最终提示词驱动的画布转绘,不丢弃未接入参考或改用普通生成提交'
273
+ )
274
+ if (
275
+ !request.prompt ||
276
+ request.prompt.length > 20000 ||
277
+ (model.prompt_max_length && request.prompt.length > model.prompt_max_length)
278
+ )
279
+ throw new Error('实际生成提示词为空或超过当前模型限制')
280
+ return {
281
+ configured,
282
+ estimateBody: { ...request, protocol_agreed: false },
283
+ body: redrawVideoBody(ctx, request),
284
+ prompt: request.prompt
285
+ }
286
+ }
287
+ // Matches canvasService.createCanvasRedrawTask; consent is checked separately,
288
+ // never submitted as true on the user's behalf.
289
+ export function redrawVideoBody(ctx, request) {
290
+ return {
291
+ segment_index: ctx.binding.index,
292
+ prompt: request.prompt || null,
293
+ model_name: request.model_name,
294
+ duration: request.duration,
295
+ aspect_ratio: request.aspect_ratio || request.extra?.aspect_ratio,
296
+ resolution: request.resolution || request.extra?.resolution,
297
+ reference_image_material_ids: [],
298
+ reference_audio_material_ids: [],
299
+ canvas_doc_id: ctx.doc.id,
300
+ canvas_node_id: ctx.generation.id,
301
+ protocol_agreed: false
302
+ }
303
+ }
304
+ async function quote(client, ctx, prepared) {
305
+ const canvas = await client.raw(
306
+ '/canvas/generations/estimate',
307
+ { method: 'POST', body: prepared.estimateBody },
308
+ { retry: false }
309
+ )
310
+ const { model_name, duration, aspect_ratio, resolution, prompt } =
311
+ prepared.body
312
+ const redraw = await api(
313
+ client,
314
+ jobPath(ctx.job.id) + '/redraw-estimate',
315
+ {
316
+ method: 'POST',
317
+ body: {
318
+ model_name,
319
+ duration,
320
+ aspect_ratio,
321
+ resolution,
322
+ prompt,
323
+ segment_count: 1,
324
+ reference_image_material_ids: [],
325
+ reference_audio_material_ids: []
326
+ }
327
+ },
328
+ { retry: false }
329
+ )
330
+ const cost = canvas.estimated_credit_cost
331
+ if (
332
+ cost == null ||
333
+ !Number.isFinite(Number(cost)) ||
334
+ Number(cost) < 0 ||
335
+ canvas.will_intercept ||
336
+ redraw.total == null ||
337
+ Number(redraw.total) !== Number(cost) ||
338
+ Number(redraw.per_segment) !== Number(cost) ||
339
+ redraw.segment_count !== 1
340
+ )
341
+ throw new Error('画布与专用转绘预估缺失、被拦截或不一致,停止提交')
342
+ const me = await client.raw('/auth/me'),
343
+ team = ctx.doc.team_uid
344
+ ? me.teams?.find(
345
+ (t) => text(t.uid) === text(ctx.doc.team_uid) && t.status === 'active'
346
+ )
347
+ : null
348
+ if (text(me.uid) !== ctx.parent.owner || (ctx.doc.team_uid && !team))
349
+ throw new Error('原账号或团队有效成员身份已变化')
350
+ return {
351
+ estimated_credit_cost: Number(cost),
352
+ currency: 'OO',
353
+ canvas,
354
+ redraw,
355
+ wallet: {
356
+ space: ctx.parent.input.space,
357
+ team_uid: ctx.doc.team_uid || null,
358
+ team_name: team?.name || null,
359
+ shared: Boolean(team),
360
+ balance_snapshot: (team || me).oo_balance ?? null
361
+ }
362
+ }
363
+ }
364
+ async function listTasks(client, ctx) {
365
+ const data = await api(
366
+ client,
367
+ jobPath(ctx.job.id) + '/redraw-tasks?segment_index=' + ctx.binding.index
368
+ )
369
+ if (!Array.isArray(data.items)) throw new Error('原片段转绘任务列表不完整')
370
+ return data.items
371
+ }
372
+ async function preventDuplicate(profile, operation, ctx) {
373
+ const directory = path.dirname(repaintVideoPath(profile, operation))
374
+ const files = await fs.readdir(directory).catch((e) => {
375
+ if (e.code === 'ENOENT') return []
376
+ throw e
377
+ })
378
+ for (const file of files.filter((f) => f.endsWith('.json'))) {
379
+ const r = await json(path.join(directory, file))
380
+ if (
381
+ r.operation !== operation &&
382
+ r.owner === ctx.parent.owner &&
383
+ r.baseUrl === ctx.parent.baseUrl &&
384
+ r.binding?.job_id === ctx.job.id &&
385
+ r.index === ctx.binding.index &&
386
+ r.attempted &&
387
+ !['completed', 'failed'].includes(r.state)
388
+ )
389
+ throw new Error(
390
+ '同片段已有进行中或未知转绘operation,先查询原操作,不换名重提'
391
+ )
392
+ }
393
+ }
394
+ function assertNode(record, ctx) {
395
+ const view = nodeView(ctx.generation)
396
+ const allowed = record.configSaved
397
+ ? [record.savedView, record.pendingView]
398
+ : [record.nodeBefore, record.pendingView]
399
+ if (!allowed.filter(Boolean).some((v) => isDeepStrictEqual(view, v)))
400
+ throw fail(
401
+ 'REPAINT_VIDEO_SYNC_PENDING',
402
+ '原生成节点的输入/结果已被编辑或旧快照覆盖,停止;不覆盖或自动恢复被清除内容'
403
+ )
404
+ }
405
+ async function bound(client, profile, operation) {
406
+ const record = await json(repaintVideoPath(profile, operation), null)
407
+ if (
408
+ !record ||
409
+ record.schema !== 1 ||
410
+ record.profile !== profile ||
411
+ record.baseUrl !== client.baseUrl
412
+ )
413
+ throw new Error('转绘视频operation不存在或服务不匹配')
414
+ const ctx = await context(
415
+ client,
416
+ profile,
417
+ record.parentOperation,
418
+ record.index
419
+ )
420
+ if (
421
+ record.owner !== ctx.parent.owner ||
422
+ !isDeepStrictEqual(record.binding, ctx.binding)
423
+ )
424
+ throw new Error('原任务/版本/来源/规则/最终提示词变化,停止原视频操作')
425
+ assertNode(record, ctx)
426
+ return { record, ctx }
427
+ }
428
+ function summary(record, task = null) {
429
+ return {
430
+ operation: record.operation,
431
+ repaint_operation: record.parentOperation,
432
+ state: record.state,
433
+ job_id: record.binding.job_id,
434
+ plan_version: record.binding.plan_version,
435
+ segment_index: record.index,
436
+ node_id: record.binding.node_id,
437
+ parameters: record.input,
438
+ source_range_ms: [record.binding.start_ms, record.binding.end_ms],
439
+ source_video_sent_as_reference: false,
440
+ submitted: Boolean(record.taskId),
441
+ submission_attempted: Boolean(record.attempted),
442
+ task_id: record.taskId || null,
443
+ canvas_synced: Boolean(record.canvasSynced),
444
+ estimate: record.estimate,
445
+ task,
446
+ library_saved: false,
447
+ ...(record.diagnostic ? { diagnostic: record.diagnostic } : {})
448
+ }
449
+ }
450
+ async function saveNode(client, record, ctx, data) {
451
+ const wanted = { ...ctx.generation, data },
452
+ view = nodeView(wanted)
453
+ if (!isDeepStrictEqual(nodeView(ctx.generation), view)) {
454
+ record.pendingView = view
455
+ await persist(record)
456
+ ctx.generation.data = data
457
+ try {
458
+ const saved = await api(
459
+ client,
460
+ '/canvas/docs/' + encodeURIComponent(ctx.doc.id),
461
+ {
462
+ method: 'PUT',
463
+ body: {
464
+ version: ctx.doc.version,
465
+ name: ctx.doc.name,
466
+ viewport: ctx.doc.viewport,
467
+ nodes: ctx.doc.nodes,
468
+ edges: ctx.doc.edges,
469
+ meta: ctx.doc.meta
470
+ }
471
+ },
472
+ { retry: false }
473
+ )
474
+ if (
475
+ saved.version !== ctx.doc.version + 1 ||
476
+ !isDeepStrictEqual(
477
+ nodeView(saved.nodes?.find((n) => n.id === wanted.id) || {}),
478
+ view
479
+ )
480
+ )
481
+ throw new Error('保存响应缺少匹配的节点/版本')
482
+ } catch (error) {
483
+ record.state = 'sync_pending'
484
+ await persist(record)
485
+ throw fail(
486
+ 'REPAINT_VIDEO_SYNC_PENDING',
487
+ '原任务/配置保留;画布保存未确认,先用原operation恢复查询,不重发生成',
488
+ error
489
+ )
490
+ }
491
+ }
492
+ record.savedView = view
493
+ record.pendingView = null
494
+ record.configSaved = true
495
+ await persist(record)
496
+ }
497
+ export function validateRepaintVideoTask(record, task) {
498
+ const b = record.body
499
+ if (
500
+ !task ||
501
+ !Number.isSafeInteger(task.id) ||
502
+ task.id <= 0 ||
503
+ String(task.id) !== record.taskId ||
504
+ !Number.isSafeInteger(task.version) ||
505
+ task.version < 1 ||
506
+ task.job_id !== record.binding.job_id ||
507
+ task.segment_index !== record.index ||
508
+ !statuses.includes(task.status) ||
509
+ text(task.prompt).normalize('NFKC') !== text(b.prompt).normalize('NFKC') ||
510
+ task.model_name !== b.model_name ||
511
+ task.duration !== b.duration ||
512
+ task.aspect_ratio !== b.aspect_ratio ||
513
+ task.resolution !== b.resolution ||
514
+ task.reference_image_count !== 0 ||
515
+ task.reference_audio_count !== 0
516
+ )
517
+ throw fail(
518
+ 'REPAINT_VIDEO_SUBMISSION_UNKNOWN',
519
+ '返回任务与原转绘参数/片段不一致,仅保留原接收ID,不重提或回填'
520
+ )
521
+ if (record.taskVersion && task.version !== record.taskVersion)
522
+ throw new Error('原转绘任务版本已变化')
523
+ if (
524
+ task.status === 'completed' &&
525
+ (!task.result?.file_ref || !task.result?.url || !task.completed_at)
526
+ )
527
+ throw fail(
528
+ 'REPAINT_VIDEO_SYNC_PENDING',
529
+ '原任务completed但缺少真实视频结果,停止回填'
530
+ )
531
+ return task
532
+ }
533
+ const validateTask = validateRepaintVideoTask
534
+ async function taskFor(client, record) {
535
+ if (!record.taskId) return null
536
+ return validateTask(
537
+ record,
538
+ await api(
539
+ client,
540
+ jobPath(record.binding.job_id) +
541
+ '/redraw-tasks/' +
542
+ encodeURIComponent(record.taskId)
543
+ )
544
+ )
545
+ }
546
+ export async function startRepaintVideo(
547
+ client,
548
+ profile,
549
+ parent,
550
+ index,
551
+ operation,
552
+ value,
553
+ confirmed = false,
554
+ expectedCost,
555
+ options = {}
556
+ ) {
557
+ safeName(operation)
558
+ const input = repaintVideoInput(value),
559
+ file = repaintVideoPath(profile, operation)
560
+ await requireServiceReady(client)
561
+ let ctx = await context(client, profile, parent, index),
562
+ record = await json(file, null)
563
+ await preventDuplicate(profile, operation, ctx)
564
+ if (record) {
565
+ if (
566
+ record.parentOperation !== parent ||
567
+ record.index !== index ||
568
+ !isDeepStrictEqual(record.input, input)
569
+ )
570
+ throw new Error('operation已绑定其他配置;不修改或换名重提未知任务')
571
+ ;({ record, ctx } = await bound(client, profile, operation))
572
+ if (record.attempted) return recoverRepaintVideo(client, profile, operation)
573
+ } else {
574
+ const view = nodeView(ctx.generation)
575
+ if (view.last_task_id || view.result)
576
+ throw new Error('本轮只接首轮转绘;目标已有结果/任务,不自动覆盖或重做')
577
+ }
578
+ let prepared = await prepare(client, ctx, input)
579
+ if (record && !isDeepStrictEqual(record.body, prepared.body))
580
+ throw new Error('原生成请求已变化,停止')
581
+ if ((await listTasks(client, ctx)).some(pending))
582
+ throw new Error('原片段已有进行中的远端转绘任务,停止新提交')
583
+ let estimate = await quote(client, ctx, prepared)
584
+ if (!record)
585
+ record = {
586
+ schema: 1,
587
+ profile,
588
+ operation,
589
+ parentOperation: parent,
590
+ index,
591
+ input,
592
+ baseUrl: client.baseUrl,
593
+ owner: ctx.parent.owner,
594
+ binding: ctx.binding,
595
+ nodeBefore: nodeView(ctx.generation),
596
+ body: prepared.body,
597
+ state: 'prepared',
598
+ createdAt: new Date().toISOString()
599
+ }
600
+ if (!confirmed) {
601
+ record.estimate = estimate
602
+ await persist(record)
603
+ return {
604
+ ...summary(record),
605
+ prompt: prepared.prompt,
606
+ request: record.body,
607
+ next: '交互卡片展示模型/输出时长/比例/分辨率/提示词/实际钱包和费用,明确确认后沿用原video命令加--yes --expected-cost;源片不作为视频参考发送'
608
+ }
609
+ }
610
+ const cost = Number(expectedCost)
611
+ if (
612
+ expectedCost === undefined ||
613
+ !Number.isFinite(cost) ||
614
+ cost < 0 ||
615
+ !record.estimate ||
616
+ record.estimate.estimated_credit_cost !== cost ||
617
+ estimate.estimated_credit_cost !== cost
618
+ )
619
+ throw new Error(
620
+ '先预估并当次明确确认;--expected-cost须与原预估及当前两个预估一致'
621
+ )
622
+ let protocol = await readProtocol(client)
623
+ if (protocol.required && !protocol.agreed) {
624
+ if (!options.onAgreementRequired)
625
+ throw agreementError(
626
+ 'AGREEMENT_REQUIRED',
627
+ '需本人签署《统一协议》《Seedance2.0模型使用承诺书》,转绘尚未提交',
628
+ protocol
629
+ )
630
+ await options.onAgreementRequired({
631
+ operation,
632
+ body: record.body,
633
+ estimate
634
+ })
635
+ const next = await readProtocol(client)
636
+ if (next.version !== protocol.version || (next.required && !next.agreed))
637
+ throw agreementError(
638
+ 'AGREEMENT_CHANGED',
639
+ '协议版本/状态变化,停止生成',
640
+ next
641
+ )
642
+ protocol = next
643
+ }
644
+ ;({ record, ctx } = await bound(client, profile, operation))
645
+ prepared = await prepare(client, ctx, input)
646
+ if (!isDeepStrictEqual(record.body, prepared.body))
647
+ throw new Error('协议确认后原生成请求变化')
648
+ estimate = await quote(client, ctx, prepared)
649
+ if (estimate.estimated_credit_cost !== cost)
650
+ throw new Error('实时费用变化,停止并重新确认')
651
+ if ((await listTasks(client, ctx)).some(pending))
652
+ throw new Error('原片段出现进行中任务,未提交')
653
+ await saveNode(client, record, ctx, prepared.configured.data)
654
+ ;({ record, ctx } = await bound(client, profile, operation))
655
+ record.approvedCost = cost
656
+ record.attempted = true
657
+ record.state = 'submitting'
658
+ await persist(record)
659
+ try {
660
+ const task = await api(
661
+ client,
662
+ jobPath(ctx.job.id) + '/redraw-tasks',
663
+ { method: 'POST', body: record.body },
664
+ { retry: false }
665
+ )
666
+ if (Number.isSafeInteger(task?.id) && task.id > 0) {
667
+ record.taskId = String(task.id)
668
+ await persist(record)
669
+ }
670
+ validateTask(record, task)
671
+ record.taskVersion = task.version
672
+ record.state = 'submitted'
673
+ await persist(record)
674
+ } catch (error) {
675
+ if (error.apiCode === 4401 && !record.taskId) {
676
+ record.attempted = false
677
+ record.state = 'blocked_agreement'
678
+ await persist(record)
679
+ throw agreementError(
680
+ 'AGREEMENT_REQUIRED',
681
+ '《统一协议》《Seedance2.0模型使用承诺书》待签署(4401);不是视频执行失败',
682
+ await readProtocol(client)
683
+ )
684
+ }
685
+ record.state = 'submission_unknown'
686
+ record.diagnostic = {
687
+ ...(error.status ? { http_status: error.status } : {}),
688
+ ...(error.apiCode ? { api_code: error.apiCode } : {}),
689
+ ...(error.requestId ? { request_id: error.requestId } : {})
690
+ }
691
+ await persist(record)
692
+ throw fail(
693
+ 'REPAINT_VIDEO_SUBMISSION_UNKNOWN',
694
+ '转绘提交响应未确认;先检查服务并查询原operation,不重发POST或换operation',
695
+ error
696
+ )
697
+ }
698
+ return recoverRepaintVideo(client, profile, operation)
699
+ }
700
+ export async function getRepaintVideo(client, profile, operation) {
701
+ const { record, ctx } = await bound(client, profile, operation),
702
+ task = await taskFor(client, record)
703
+ const result = summary(record, task)
704
+ if (record.attempted && !record.taskId)
705
+ result.observed_remote_tasks = (await listTasks(client, ctx)).map((t) => ({
706
+ id: t.id,
707
+ status: t.status,
708
+ model_name: t.model_name,
709
+ segment_index: t.segment_index
710
+ }))
711
+ return result
712
+ }
713
+ export async function recoverRepaintVideo(client, profile, operation) {
714
+ await requireServiceReady(client)
715
+ const { record, ctx } = await bound(client, profile, operation)
716
+ if (!record.attempted) {
717
+ if (
718
+ record.pendingView &&
719
+ isDeepStrictEqual(nodeView(ctx.generation), record.pendingView)
720
+ ) {
721
+ record.savedView = record.pendingView
722
+ record.pendingView = null
723
+ record.configSaved = true
724
+ record.state = 'prepared'
725
+ await persist(record)
726
+ }
727
+ return summary(record)
728
+ }
729
+ if (!record.taskId)
730
+ throw fail(
731
+ 'REPAINT_VIDEO_SUBMISSION_UNKNOWN',
732
+ '尚无原提交接收ID;get仅展示观察到的任务,不认领或重发'
733
+ )
734
+ const task = await taskFor(client, record)
735
+ if (task.status === 'completed') await archiveRepaintResult(record, 'video', task.result, {
736
+ estimated_credit_cost: task.estimated_credit_cost,
737
+ credit_cost: task.credit_cost, billing_status: task.billing_status
738
+ })
739
+ // Fresh file proxy signatures alone must not cause another canvas write.
740
+ if (record.canvasSynced && record.terminalStatus === task.status)
741
+ return summary(record, task)
742
+ const data = {
743
+ ...ctx.generation.data,
744
+ last_task_id: record.taskId,
745
+ status: task.status,
746
+ error_message: text(task.error_message),
747
+ extra: { ...ctx.generation.data.extra, task_no: text(task.task_no) }
748
+ }
749
+ if (task.status === 'completed') {
750
+ data.result = {
751
+ file_ref: task.result.file_ref,
752
+ url: task.result.url,
753
+ mime_type: task.result.mime_type,
754
+ width: task.result.width || null,
755
+ height: task.result.height || null,
756
+ duration: task.result.duration || null,
757
+ task_id: record.taskId,
758
+ status: 'completed'
759
+ }
760
+ data.extra.file_ref = task.result.file_ref
761
+ }
762
+ await saveNode(client, record, ctx, data)
763
+ record.state = task.status
764
+ record.canvasSynced = !pending(task)
765
+ record.terminalStatus = pending(task) ? null : task.status
766
+ await persist(record)
767
+ return summary(record, task)
768
+ }
769
+ export async function waitRepaintVideo(
770
+ client,
771
+ profile,
772
+ operation,
773
+ interval = 3000,
774
+ timeout = 600000
775
+ ) {
776
+ if (![interval, timeout].every((v) => Number.isSafeInteger(v) && v > 0))
777
+ throw new Error('等待参数须为正整数')
778
+ const deadline = Date.now() + timeout
779
+ while (Date.now() < deadline) {
780
+ const result = await recoverRepaintVideo(client, profile, operation)
781
+ if (result.state === 'completed') return result
782
+ if (result.state === 'failed')
783
+ throw fail('TASK_FAILED', '原转绘任务失败;查询原错误和计费,不自动重做')
784
+ if (!result.submission_attempted) return result
785
+ await sleep(
786
+ Math.min(interval, Math.max(1, deadline - Date.now())),
787
+ undefined,
788
+ {
789
+ signal: cancellation.signal
790
+ }
791
+ )
792
+ }
793
+ throw fail('WAIT_TIMEOUT', '原视频仍待查询;继续同operation,不重新提交')
794
+ }
795
+ export async function downloadRepaintVideo(client, profile, operation, output) {
796
+ const { exportRepaintResult } = await import('./repaint-export.js')
797
+ return exportRepaintResult(client, profile, operation, 'video', output)
798
+ }