@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.
- package/.agents/skills/uzoo-anime-cli/SKILL.md +83 -0
- package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +51 -0
- package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
- package/.agents/skills/uzoo-anime-cli/references/commands.md +250 -0
- package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +152 -0
- package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +175 -0
- package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
- package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +123 -0
- package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +95 -0
- package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +94 -0
- package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
- package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +300 -0
- package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +41 -0
- package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +71 -0
- package/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +242 -3
- package/assets/README.md +4 -0
- package/assets/favicon-uzoo.png +0 -0
- package/assets/logo-uzoo.svg +7 -0
- 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
- 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
- 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
- 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
- package/docs/M0-M5/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +89 -0
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs//345/274/200/345/217/221/350/256/241/345/210/222.md +127 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs//351/230/266/346/256/2653B/351/252/214/346/224/266/344/272/244/346/216/245.md +44 -0
- package/docs//351/230/266/346/256/265/344/270/200/351/252/214/346/224/266.md +60 -0
- 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
- 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
- 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
- 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
- package/examples/matting.json +5 -0
- package/examples/real-business/image-request.template.json +7 -0
- package/examples/real-business/node-patch.template.json +11 -0
- package/extensions/uzoo-cli-login/manifest.json +9 -0
- package/extensions/uzoo-cli-login/popup.css +1 -0
- package/extensions/uzoo-cli-login/popup.html +14 -0
- package/extensions/uzoo-cli-login/popup.js +65 -0
- package/package.json +56 -6
- package/scripts/check-package-resources.mjs +56 -0
- package/scripts/check-release.mjs +19 -0
- package/scripts/pack-local.mjs +33 -0
- package/scripts/release-policy.mjs +22 -0
- package/src/agreement-page.js +62 -0
- package/src/agreement.js +419 -0
- package/src/annotation-media.js +386 -0
- package/src/api.js +168 -0
- package/src/browser-login.js +13 -0
- package/src/callback-page.js +44 -0
- package/src/canvas.js +105 -0
- package/src/cli-authorization.js +122 -0
- package/src/cli.js +459 -0
- package/src/download-name.js +17 -0
- package/src/draft-finalize.js +229 -0
- package/src/generation-graph.js +124 -0
- package/src/generation-input.js +472 -0
- package/src/graph-disconnect.js +89 -0
- package/src/graph.js +431 -0
- package/src/group-edit.js +53 -0
- package/src/group-layout.js +113 -0
- package/src/group-membership.js +126 -0
- package/src/history-add.js +119 -0
- package/src/image-annotation.js +246 -0
- package/src/image-crop.js +283 -0
- package/src/image-matting.js +82 -0
- package/src/image-media.js +89 -0
- package/src/image-output.js +385 -0
- package/src/local-output.js +23 -0
- package/src/material-download.js +55 -0
- package/src/material-save.js +254 -0
- package/src/materials.js +147 -0
- package/src/matting-background.js +73 -0
- package/src/matting-jobs.js +63 -0
- package/src/matting-worker.js +49 -0
- package/src/matting.js +170 -0
- package/src/media-copy.js +53 -0
- package/src/media.js +343 -0
- package/src/model-options.js +76 -0
- package/src/multipart.js +28 -0
- package/src/node-copy.js +95 -0
- package/src/node-delete.js +49 -0
- package/src/node-history.js +231 -0
- package/src/normal-login.js +118 -0
- package/src/package-info.js +8 -0
- package/src/png-alpha.js +60 -0
- package/src/protocol.js +83 -0
- package/src/remake.js +738 -0
- package/src/repaint-archive.js +53 -0
- package/src/repaint-delivery.js +783 -0
- package/src/repaint-export.js +63 -0
- package/src/repaint-graph.js +214 -0
- package/src/repaint-history.js +267 -0
- package/src/repaint-materials.js +601 -0
- package/src/repaint-reverse.js +614 -0
- package/src/repaint-sequence.js +132 -0
- package/src/repaint-settings.js +576 -0
- package/src/repaint-video.js +798 -0
- package/src/repaint.js +326 -0
- package/src/resource.js +110 -0
- package/src/review.js +435 -0
- package/src/service.js +85 -0
- package/src/store.js +100 -0
- package/src/video-clip.js +381 -0
- package/src/video-frame.js +296 -0
- package/src/workflows.js +214 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// Independent implementation of the ordinary web node input contract.
|
|
2
|
+
// Development oracles: nodeContent.js, generationActions.js and canvas/requests.js.
|
|
3
|
+
export const generationModes = {
|
|
4
|
+
text2image: [['text'], []],
|
|
5
|
+
image2image: [['text', 'image'], ['image']],
|
|
6
|
+
text_to_video: [['text'], []],
|
|
7
|
+
image_reference: [['text', 'image'], ['image']],
|
|
8
|
+
video_reference: [['text', 'video'], ['video']],
|
|
9
|
+
audio_reference: [['text', 'image', 'video', 'audio'], ['audio']],
|
|
10
|
+
combined_reference: [
|
|
11
|
+
['text', 'image', 'video', 'audio'],
|
|
12
|
+
['image', 'video', 'audio']
|
|
13
|
+
],
|
|
14
|
+
video_editing: [['text', 'image', 'video'], ['video']],
|
|
15
|
+
video_extension: [['text', 'video'], ['video']]
|
|
16
|
+
}
|
|
17
|
+
const labels = { image: '图片', video: '视频', audio: '音频' }
|
|
18
|
+
const text = (value) => String(value ?? '').trim()
|
|
19
|
+
const unique = (values) => [...new Set(values)]
|
|
20
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
export function normalizeMode(value) {
|
|
22
|
+
const aliases = {
|
|
23
|
+
'text-to-image': 'text2image',
|
|
24
|
+
'image-to-image': 'image2image',
|
|
25
|
+
'text-to-video': 'text_to_video',
|
|
26
|
+
text2video: 'text_to_video',
|
|
27
|
+
image2video: 'image_reference',
|
|
28
|
+
'image-to-video': 'image_reference',
|
|
29
|
+
'image-reference': 'image_reference',
|
|
30
|
+
'video-reference': 'video_reference',
|
|
31
|
+
'audio-reference': 'audio_reference',
|
|
32
|
+
'combined-reference': 'combined_reference',
|
|
33
|
+
omni_reference: 'combined_reference',
|
|
34
|
+
multimodal: 'combined_reference',
|
|
35
|
+
'video-editing': 'video_editing',
|
|
36
|
+
'video-extension': 'video_extension'
|
|
37
|
+
}
|
|
38
|
+
const raw = object(value)
|
|
39
|
+
? value.value || value.code || value.mode || value.name || value.generation_mode
|
|
40
|
+
: value
|
|
41
|
+
return aliases[text(raw)] || text(raw)
|
|
42
|
+
}
|
|
43
|
+
export function storedReferences(doc, node) {
|
|
44
|
+
const meta = doc.meta?.node_references?.[node.id]
|
|
45
|
+
const refs = meta ?? node.data?.references ?? node.data?.extra?.references ?? []
|
|
46
|
+
if (!Array.isArray(refs) || refs.some((ref) => !object(ref) || !text(ref.id)))
|
|
47
|
+
throw new Error('节点持久参考无效;先核对画布引用')
|
|
48
|
+
return structuredClone(refs)
|
|
49
|
+
}
|
|
50
|
+
export function nodeReview(data = {}) {
|
|
51
|
+
const review = data.seedanceReview || data.seedance_review || data.extra?.seedance_review
|
|
52
|
+
return object(review) ? { ...review, status: text(review.status).toLowerCase() } : null
|
|
53
|
+
}
|
|
54
|
+
function resultText(result) {
|
|
55
|
+
const status = result?.structured_meta?.status ?? result?.structuredMeta?.status
|
|
56
|
+
const markdown = result?.structured_render?.markdown ?? result?.structuredRender?.markdown
|
|
57
|
+
if (status !== 'failed' && typeof markdown === 'string' && markdown.trim()) return markdown.trim()
|
|
58
|
+
return typeof result?.text === 'string' ? result.text.trim() : ''
|
|
59
|
+
}
|
|
60
|
+
function plainText(value) {
|
|
61
|
+
// Equivalent to the web plain-text projection; common editor entities are decoded.
|
|
62
|
+
return String(value ?? '')
|
|
63
|
+
.replace(/<[^>]*>/g, ' ')
|
|
64
|
+
.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => {
|
|
65
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
|
|
66
|
+
if (entity[0] !== '#') return named[entity.toLowerCase()] ?? match
|
|
67
|
+
const n = /^#x/i.test(entity) ? parseInt(entity.slice(2), 16) : Number(entity.slice(1))
|
|
68
|
+
return n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : match
|
|
69
|
+
})
|
|
70
|
+
.replace(/\s+/g, ' ')
|
|
71
|
+
.trim()
|
|
72
|
+
}
|
|
73
|
+
export function readSource(node) {
|
|
74
|
+
const data = node?.data || {},
|
|
75
|
+
extra = data.extra || {},
|
|
76
|
+
output = data.node_output || {}
|
|
77
|
+
const asset = node?.type?.endsWith('_upload') || extra.node_kind === 'asset'
|
|
78
|
+
const mediaType = asset
|
|
79
|
+
? data.mediaType || output.media_type || node.type.replace('_upload', '')
|
|
80
|
+
: node?.type
|
|
81
|
+
const result = data.result || {}
|
|
82
|
+
const status = data.status || result.status || (data.last_task_id ? 'processing' : '')
|
|
83
|
+
const content =
|
|
84
|
+
typeof data.output === 'string'
|
|
85
|
+
? data.output
|
|
86
|
+
: (output.content ?? (status === 'completed' ? resultText(result) : '')) || data.text || ''
|
|
87
|
+
const fileRef =
|
|
88
|
+
data.fileRef || data.file_ref || extra.file_ref || result.file_ref || output.file_ref
|
|
89
|
+
const url = asset
|
|
90
|
+
? result.url ||
|
|
91
|
+
data.url ||
|
|
92
|
+
output.url ||
|
|
93
|
+
(fileRef ? '/api/v1/files/' + encodeURIComponent(fileRef) : '')
|
|
94
|
+
: result.url || data.resultUrl || output.url || ''
|
|
95
|
+
return {
|
|
96
|
+
id: text(node?.id),
|
|
97
|
+
title: text(data.title || data.name || data.file_name || node?.id),
|
|
98
|
+
mediaType,
|
|
99
|
+
url: text(url),
|
|
100
|
+
text: mediaType === 'text' ? plainText(content) : '',
|
|
101
|
+
uploading:
|
|
102
|
+
data.uploading === true ||
|
|
103
|
+
(data.runtime?.operation === 'upload' &&
|
|
104
|
+
['queued', 'preparing', 'processing'].includes(data.runtime.status)),
|
|
105
|
+
uploadError: text(
|
|
106
|
+
data.uploadError || (data.runtime?.operation === 'upload' ? data.runtime.error : '')
|
|
107
|
+
),
|
|
108
|
+
seedanceReview: nodeReview(data)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function mergeSources(sources, references) {
|
|
112
|
+
const seen = new Set()
|
|
113
|
+
return [...sources, ...references]
|
|
114
|
+
.filter((ref) => {
|
|
115
|
+
const id = text(ref.id)
|
|
116
|
+
if (!id || seen.has(id)) return false
|
|
117
|
+
seen.add(id)
|
|
118
|
+
return true
|
|
119
|
+
})
|
|
120
|
+
.map((ref) => ({
|
|
121
|
+
...ref,
|
|
122
|
+
id: text(ref.id),
|
|
123
|
+
title: text(ref.title || ref.id),
|
|
124
|
+
mediaType: text(ref.mediaType || 'image'),
|
|
125
|
+
url: text(ref.url),
|
|
126
|
+
text: text(ref.text)
|
|
127
|
+
}))
|
|
128
|
+
}
|
|
129
|
+
function usable(source) {
|
|
130
|
+
if (!['text', ...Object.keys(labels)].includes(source.mediaType))
|
|
131
|
+
throw new Error(`参考 ${source.id} 的媒体类型不支持`)
|
|
132
|
+
if (source.uploading || source.uploadError)
|
|
133
|
+
throw new Error(`参考 ${source.id} 尚未上传完成或上传失败`)
|
|
134
|
+
if (source.mediaType === 'text' ? !source.text : !source.url)
|
|
135
|
+
throw new Error(`上游 ${source.id} 尚无${source.mediaType === 'text' ? '文本' : '媒体'}输出`)
|
|
136
|
+
if (source.mediaType !== 'text' && !/^(?:https?:\/\/|\/api\/|asset:\/\/)/i.test(source.url))
|
|
137
|
+
throw new Error(`参考 ${source.id} 不是可提交的服务端媒体地址`)
|
|
138
|
+
}
|
|
139
|
+
export function collectGenerationSources(doc, node, mediaTypes = null) {
|
|
140
|
+
const incoming = doc.edges.filter((edge) => edge.target === node.id)
|
|
141
|
+
const connected = []
|
|
142
|
+
for (const id of unique(incoming.map((edge) => edge.source))) {
|
|
143
|
+
const source = doc.nodes.find((candidate) => candidate.id === id)
|
|
144
|
+
if (!source) throw new Error(`上游 ${id} 节点不存在`)
|
|
145
|
+
const resolved = readSource(source)
|
|
146
|
+
if (mediaTypes && !mediaTypes.includes(resolved.mediaType)) continue
|
|
147
|
+
usable(resolved)
|
|
148
|
+
connected.push(resolved)
|
|
149
|
+
}
|
|
150
|
+
const references = storedReferences(doc, node).filter(
|
|
151
|
+
(ref) => !mediaTypes || mediaTypes.includes(ref.mediaType)
|
|
152
|
+
)
|
|
153
|
+
const all = mergeSources(connected, references)
|
|
154
|
+
all.forEach(usable)
|
|
155
|
+
return { connected, references, all }
|
|
156
|
+
}
|
|
157
|
+
function normalizePrompt(value) {
|
|
158
|
+
return value.replace(/@\{ref:([^}]+)\}/g, (marker, encoded) => {
|
|
159
|
+
let id
|
|
160
|
+
try {
|
|
161
|
+
id = decodeURIComponent(encoded)
|
|
162
|
+
} catch {
|
|
163
|
+
return marker
|
|
164
|
+
}
|
|
165
|
+
const match = /^(image|video|audio)-(\d+)$/.exec(id)
|
|
166
|
+
return match ? `@${labels[match[1]]}${match[2]}` : marker
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
function serializeMentions(value, sources, media, automatic) {
|
|
170
|
+
const markers = new Map(),
|
|
171
|
+
names = new Map()
|
|
172
|
+
for (const source of sources) {
|
|
173
|
+
const index = media[source.mediaType]?.indexOf(text(source.url)) ?? -1
|
|
174
|
+
if (index < 0) continue
|
|
175
|
+
markers.set(source.id, `@${labels[source.mediaType]}${index + 1}`)
|
|
176
|
+
const name = text(source.displayLabel || source.title)
|
|
177
|
+
if (name) names.set(name, names.has(name) ? null : source)
|
|
178
|
+
}
|
|
179
|
+
if (automatic) {
|
|
180
|
+
const candidates = [...names]
|
|
181
|
+
.filter(([, source]) => source)
|
|
182
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
183
|
+
const token = /^(?:@\{ref:[^}]+\}|@(?:(?:图片|音频|视频)|(?:image|audio|video))?\d+)/
|
|
184
|
+
let next = '',
|
|
185
|
+
offset = 0
|
|
186
|
+
while (offset < value.length) {
|
|
187
|
+
const marker = token.exec(value.slice(offset))
|
|
188
|
+
const candidate = marker ? null : candidates.find(([name]) => value.startsWith(name, offset))
|
|
189
|
+
if (marker) {
|
|
190
|
+
next += marker[0]
|
|
191
|
+
offset += marker[0].length
|
|
192
|
+
} else if (candidate) {
|
|
193
|
+
next += `@{ref:${encodeURIComponent(candidate[1].id)}}`
|
|
194
|
+
offset += candidate[0].length
|
|
195
|
+
} else next += value[offset++]
|
|
196
|
+
}
|
|
197
|
+
value = next
|
|
198
|
+
}
|
|
199
|
+
const resolved = value.replace(/@@?\{ref:([^}]+)\}/g, (marker, encoded) => {
|
|
200
|
+
try {
|
|
201
|
+
return markers.get(decodeURIComponent(encoded)) || normalizePrompt(marker)
|
|
202
|
+
} catch {
|
|
203
|
+
return marker
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
if (/@\{ref:[^}]+\}/.test(resolved))
|
|
207
|
+
throw new Error('提示词包含未解析的素材引用;请先核对参考素材')
|
|
208
|
+
return normalizePrompt(resolved)
|
|
209
|
+
}
|
|
210
|
+
export function promptInput(node, connected, references, sources, media) {
|
|
211
|
+
const contexts = [
|
|
212
|
+
...connected.filter((s) => s.mediaType === 'text').map((s) => s.text),
|
|
213
|
+
...references.filter((s) => s.mediaType === 'text').map((s) => text(s.text))
|
|
214
|
+
]
|
|
215
|
+
.map(text)
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
const own = text(node.data?.prompt),
|
|
218
|
+
automatic =
|
|
219
|
+
(node.data?.autoRecognizeReferences ?? node.data?.extra?.auto_recognize_references) !== false
|
|
220
|
+
const preparation = Object.entries(labels).flatMap(([type, label]) =>
|
|
221
|
+
(media[type] || []).flatMap((url, index) => {
|
|
222
|
+
const source = sources.find((s) => s.mediaType === type && text(s.url) === url)
|
|
223
|
+
return source
|
|
224
|
+
? [
|
|
225
|
+
`@${label}${index + 1}是${String(source.title || source.id)
|
|
226
|
+
.replace(/[\r\n]+/g, ' ')
|
|
227
|
+
.trim()}`
|
|
228
|
+
]
|
|
229
|
+
: []
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
const prefix = preparation.length
|
|
233
|
+
? `<!-- flow:auto-material-preparation:start -->\n素材准备:${preparation.join(';')}。\n<!-- flow:auto-material-preparation:end -->\n\n`
|
|
234
|
+
: ''
|
|
235
|
+
return {
|
|
236
|
+
prompt:
|
|
237
|
+
prefix +
|
|
238
|
+
serializeMentions([...contexts, own].filter(Boolean).join('\n\n'), sources, media, automatic),
|
|
239
|
+
nodePrompt: prefix + serializeMentions(own, sources, media, automatic),
|
|
240
|
+
contexts
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function resolveReferences(doc, connected, configured, includeAll) {
|
|
244
|
+
const ordered = includeAll
|
|
245
|
+
? mergeSources(connected, configured)
|
|
246
|
+
: configured.length
|
|
247
|
+
? configured
|
|
248
|
+
: connected
|
|
249
|
+
const media = { image: [], video: [], audio: [] },
|
|
250
|
+
assets = [],
|
|
251
|
+
tasks = [],
|
|
252
|
+
resolved = []
|
|
253
|
+
const seen = new Set()
|
|
254
|
+
for (const reference of ordered) {
|
|
255
|
+
const id = text(reference.id)
|
|
256
|
+
if (seen.has(id) || reference.mediaType === 'text') continue
|
|
257
|
+
seen.add(id)
|
|
258
|
+
const source = doc.nodes.find((n) => n.id === id)
|
|
259
|
+
const current = source ? readSource(source) : null
|
|
260
|
+
const data = source?.data || {},
|
|
261
|
+
extra = data.extra || {}
|
|
262
|
+
const type = current?.mediaType || reference.mediaType || 'image'
|
|
263
|
+
const url = text(current?.url || reference.url)
|
|
264
|
+
const item = { ...reference, id, mediaType: type, url }
|
|
265
|
+
usable(item)
|
|
266
|
+
if (!url.startsWith('asset://')) media[type].push(url)
|
|
267
|
+
if (type !== 'audio') {
|
|
268
|
+
const asset = text(
|
|
269
|
+
reference.assetId || data.sourceAssetId || data.assetId || extra.source_asset_id
|
|
270
|
+
)
|
|
271
|
+
if (asset) assets.push(asset)
|
|
272
|
+
const review = current?.seedanceReview || reference.seedanceReview
|
|
273
|
+
if (text(review?.status).toLowerCase() === 'active' && text(review.taskId)) {
|
|
274
|
+
const reviewedRef = text(review.mediaRef || review.imageRef)
|
|
275
|
+
const currentRef = text(
|
|
276
|
+
data.resultFileRef ||
|
|
277
|
+
data.fileRef ||
|
|
278
|
+
data.file_ref ||
|
|
279
|
+
extra.file_ref ||
|
|
280
|
+
data.result?.file_ref ||
|
|
281
|
+
data.node_output?.file_ref
|
|
282
|
+
)
|
|
283
|
+
if (reviewedRef && currentRef && reviewedRef !== currentRef)
|
|
284
|
+
throw new Error('当前素材与已通过审核的引用不一致;请核对或重新审核,不复用旧关联')
|
|
285
|
+
tasks.push(text(review.taskId))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
resolved.push(item)
|
|
289
|
+
}
|
|
290
|
+
for (const source of connected.filter((s) => s.mediaType === 'audio'))
|
|
291
|
+
media.audio.push(source.url)
|
|
292
|
+
for (const type of Object.keys(media)) media[type] = unique(media[type])
|
|
293
|
+
return {
|
|
294
|
+
media,
|
|
295
|
+
assets: unique(assets),
|
|
296
|
+
tasks: unique(tasks),
|
|
297
|
+
sources: mergeSources(resolved, connected)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function configuredValue(node, key) {
|
|
301
|
+
const data = node.data || {},
|
|
302
|
+
extra = data.extra || {}
|
|
303
|
+
return data[key] ?? extra[key]
|
|
304
|
+
}
|
|
305
|
+
function setting(node, model, key, listKey, fallback = '') {
|
|
306
|
+
const raw = configuredValue(node, key)
|
|
307
|
+
const choices = Array.isArray(model[listKey]) ? model[listKey] : []
|
|
308
|
+
if (raw !== undefined && raw !== null && raw !== '') {
|
|
309
|
+
if (!choices.includes(String(raw))) throw new Error(`${key} 不在当前模型选项中;请先核对参数`)
|
|
310
|
+
return String(raw)
|
|
311
|
+
}
|
|
312
|
+
return choices.includes(fallback) ? fallback : fallback ? choices[0] || '' : ''
|
|
313
|
+
}
|
|
314
|
+
export function buildGenerationRequest(doc, node, model, inputs) {
|
|
315
|
+
const kind = node.type,
|
|
316
|
+
data = node.data || {},
|
|
317
|
+
extra = data.extra || {}
|
|
318
|
+
const { connected, references, all } = inputs
|
|
319
|
+
let media,
|
|
320
|
+
assets = [],
|
|
321
|
+
tasks = [],
|
|
322
|
+
sources = all
|
|
323
|
+
if (kind === 'image') {
|
|
324
|
+
const referencedImages = references
|
|
325
|
+
.filter((s) => s.mediaType === 'image')
|
|
326
|
+
.map((s) => text(s.url))
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
media = {
|
|
329
|
+
image: referencedImages.length
|
|
330
|
+
? referencedImages
|
|
331
|
+
: connected.filter((s) => s.mediaType === 'image').map((s) => s.url),
|
|
332
|
+
video: unique(
|
|
333
|
+
[...connected, ...references].filter((s) => s.mediaType === 'video').map((s) => s.url)
|
|
334
|
+
),
|
|
335
|
+
audio: unique(
|
|
336
|
+
[...references, ...connected].filter((s) => s.mediaType === 'audio').map((s) => s.url)
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
const refs = resolveReferences(doc, connected, references, kind === 'video')
|
|
341
|
+
media = refs.media
|
|
342
|
+
assets = refs.assets
|
|
343
|
+
tasks = refs.tasks
|
|
344
|
+
sources = mergeSources(refs.sources, references)
|
|
345
|
+
}
|
|
346
|
+
const input = promptInput(node, connected, references, sources, media)
|
|
347
|
+
if ((!text(data.prompt) && kind === 'text') || !input.prompt.trim()) throw new Error('缺少提示词')
|
|
348
|
+
const body = {
|
|
349
|
+
doc_id: doc.id,
|
|
350
|
+
node_id: node.id,
|
|
351
|
+
segment_type: kind,
|
|
352
|
+
model_name: model.label,
|
|
353
|
+
prompt: (kind === 'text' ? input.nodePrompt : input.prompt).trim(),
|
|
354
|
+
reference_image_urls: media.image,
|
|
355
|
+
reference_video_urls:
|
|
356
|
+
kind === 'text'
|
|
357
|
+
? unique(
|
|
358
|
+
[...connected, ...references].filter((s) => s.mediaType === 'video').map((s) => s.url)
|
|
359
|
+
)
|
|
360
|
+
: media.video,
|
|
361
|
+
reference_audio_urls: media.audio,
|
|
362
|
+
reference_asset_ids: assets,
|
|
363
|
+
...(kind !== 'text' ? { reference_canvas_task_ids: tasks } : {}),
|
|
364
|
+
duration: null,
|
|
365
|
+
extra: {}
|
|
366
|
+
}
|
|
367
|
+
if (kind === 'text') {
|
|
368
|
+
const max = Number(data.maxTokens),
|
|
369
|
+
temperature = Number(data.temperature)
|
|
370
|
+
body.extra = {
|
|
371
|
+
context_texts: input.contexts,
|
|
372
|
+
max_tokens: Number.isFinite(max) && max > 0 ? Math.floor(max) : 4096,
|
|
373
|
+
temperature:
|
|
374
|
+
data.temperature != null && Number.isFinite(temperature) && temperature >= 0
|
|
375
|
+
? temperature
|
|
376
|
+
: 0.7
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const modeList = (
|
|
380
|
+
model.generation_modes || (kind === 'image' ? ['text2image', 'image2image'] : [])
|
|
381
|
+
).map(normalizeMode)
|
|
382
|
+
// The current image run path resolves mode from topology; video also merges
|
|
383
|
+
// persistent references. Keep that distinction observable in the preview.
|
|
384
|
+
const types = (kind === 'image' ? connected : all).map((s) => s.mediaType)
|
|
385
|
+
const compatible = modeList.filter((mode) => {
|
|
386
|
+
const rule = generationModes[mode]
|
|
387
|
+
return (
|
|
388
|
+
rule &&
|
|
389
|
+
types.every((type) => rule[0].includes(type)) &&
|
|
390
|
+
(!rule[1].length || rule[1].some((type) => types.includes(type)))
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
const selected = normalizeMode(
|
|
394
|
+
extra.generation_mode ||
|
|
395
|
+
data.generation_mode ||
|
|
396
|
+
data[kind === 'image' ? 'imageMode' : 'videoMode']
|
|
397
|
+
)
|
|
398
|
+
const mode = compatible.includes(selected) ? selected : compatible[0]
|
|
399
|
+
if (!mode) throw new Error('目标模型不支持当前上游类型组合或缺少必需素材')
|
|
400
|
+
body.extra.generation_mode = mode
|
|
401
|
+
const aspect = setting(node, model, 'aspect_ratio', 'aspect_ratios')
|
|
402
|
+
const resolution = setting(node, model, 'resolution', 'resolutions')
|
|
403
|
+
const topLevel = kind === 'video' && /^seedance/i.test(model.label)
|
|
404
|
+
if (aspect) (topLevel ? body : body.extra).aspect_ratio = aspect
|
|
405
|
+
if (resolution) (topLevel ? body : body.extra).resolution = resolution
|
|
406
|
+
if (kind === 'image') {
|
|
407
|
+
if (extra.image_to_image_enabled === false && media.image.length)
|
|
408
|
+
throw new Error('节点已关闭图生图,不能携带图片参考')
|
|
409
|
+
const count = Number(data.image_count ?? data.imageCount ?? extra.image_count) || 1
|
|
410
|
+
if (count !== 1) throw new Error('本轮仅支持单张请求准备;请明确数量为 1')
|
|
411
|
+
body.extra.image_count = 1
|
|
412
|
+
const quality = setting(node, model, 'quality', 'quality_options', 'medium')
|
|
413
|
+
if (quality) body.extra.quality = quality
|
|
414
|
+
body.asset_type = data.asset_type || data.assetType || extra.asset_type || 'character'
|
|
415
|
+
body.enable_asset_review = Boolean(data.seedanceAutoReview)
|
|
416
|
+
} else {
|
|
417
|
+
const duration = Number(data.duration ?? extra.duration)
|
|
418
|
+
if (!Number.isFinite(duration) || duration <= 0) throw new Error('请先设置视频时长')
|
|
419
|
+
if (
|
|
420
|
+
(Number.isFinite(model.durations?.min) && duration < model.durations.min) ||
|
|
421
|
+
(Number.isFinite(model.durations?.max) && duration > model.durations.max)
|
|
422
|
+
)
|
|
423
|
+
throw new Error('视频时长超出当前模型范围')
|
|
424
|
+
body.duration = duration
|
|
425
|
+
if (media.audio.length && model.supports_audio_reference !== true)
|
|
426
|
+
throw new Error('目标模型不支持音频参考')
|
|
427
|
+
if (
|
|
428
|
+
media.audio.length &&
|
|
429
|
+
model.audio_reference_requires_visual === true &&
|
|
430
|
+
!media.image.length &&
|
|
431
|
+
!media.video.length
|
|
432
|
+
)
|
|
433
|
+
throw new Error('音频参考需要至少一个图片或视频输入')
|
|
434
|
+
body.enable_asset_review = (data.seedanceAutoReview ?? extra.seedance_auto_review) === true
|
|
435
|
+
if (model.is_sample_mode === true || extra.draft_preview === true) {
|
|
436
|
+
if (['video_editing', 'video_extension'].includes(mode))
|
|
437
|
+
throw new Error('样片模式不支持视频编辑/延长')
|
|
438
|
+
body.extra.draft_preview = true
|
|
439
|
+
if (topLevel) body.resolution = '480p'
|
|
440
|
+
else body.extra.resolution = '480p'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const type of Object.keys(labels)) {
|
|
445
|
+
const count = all.filter((s) => s.mediaType === type).length
|
|
446
|
+
const limit =
|
|
447
|
+
model[`max_${type}_references`] ?? (type === 'image' ? model.max_reference_count : undefined)
|
|
448
|
+
if (Number.isFinite(limit) && count > limit) throw new Error(`超出模型 ${type} 参考数量限制`)
|
|
449
|
+
}
|
|
450
|
+
if (
|
|
451
|
+
Number.isFinite(model.max_reference_count) &&
|
|
452
|
+
all.filter((s) => s.mediaType !== 'text').length > model.max_reference_count
|
|
453
|
+
)
|
|
454
|
+
throw new Error('超出模型参考素材总数量限制')
|
|
455
|
+
if (kind === 'text')
|
|
456
|
+
for (const type of ['image', 'video']) {
|
|
457
|
+
if (media[type].length && model[`${type}_understanding`] !== true)
|
|
458
|
+
throw new Error(`目标文本模型不支持${type === 'image' ? '图片' : '视频'}理解`)
|
|
459
|
+
}
|
|
460
|
+
if (tasks.some((id) => !/^[1-9]\d*$/.test(id)) || assets.some((id) => !/^[1-9]\d*$/.test(id)))
|
|
461
|
+
throw new Error('素材或审核任务关联 ID 无效')
|
|
462
|
+
return { request: body, sources: all }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Client request order only; do not present this as a provider receipt.
|
|
466
|
+
export function referenceMapping(request, sources) {
|
|
467
|
+
return Object.entries(labels).flatMap(([type, label]) =>
|
|
468
|
+
(request['reference_' + type + '_urls'] || []).map((url, index) => ({
|
|
469
|
+
marker: '@' + label + (index + 1), media_type: type, request_field: 'reference_' + type + '_urls',
|
|
470
|
+
request_index: index, source_node_ids: sources.filter(s => s.mediaType === type && text(s.url) === url).map(s => s.id)
|
|
471
|
+
})))
|
|
472
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isDeepStrictEqual as same } from 'node:util'
|
|
2
|
+
import { scopedCanvas } from './materials.js'
|
|
3
|
+
import { save, sourceType } from './graph.js'
|
|
4
|
+
import { generationModes, normalizeMode, storedReferences, readSource } from './generation-input.js'
|
|
5
|
+
|
|
6
|
+
export function disconnectMentions(value, reference) {
|
|
7
|
+
let result = String(value || '').replace(/@@?{ref:[^}]+}/g, marker => {
|
|
8
|
+
const manual = marker.startsWith('@@'), token = manual ? marker.slice(1) : marker
|
|
9
|
+
let id
|
|
10
|
+
try { id = decodeURIComponent(token.slice(6,-1)) } catch { return marker }
|
|
11
|
+
return id === reference.id ? manual ? '' : String(reference.displayLabel || reference.title || '') : marker
|
|
12
|
+
})
|
|
13
|
+
const legacy = String(reference.legacyMarker || reference.marker || '')
|
|
14
|
+
if (legacy && !/^@{ref:[^}]+}$/.test(legacy)) result = result.split(legacy).join('')
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
const derived = n => n?.type === 'video_repain_begain' || n?.data?.repaint || n?.data?.videoRemake ||
|
|
18
|
+
n?.data?.extra?.repaint || n?.data?.extra?.video_remake || n?.data?.extra?.clip_redraw || n?.data?.draftFinalizeGeneration || n?.data?.extra?.draft_finalize
|
|
19
|
+
|
|
20
|
+
export async function disconnectGraph(client, input, confirmed = false) {
|
|
21
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
22
|
+
Object.keys(input).some(k=>!['doc_id','space','team_uid','edge_id','expected_version'].includes(k)) ||
|
|
23
|
+
!['doc_id','edge_id'].every(k=>typeof input[k]==='string' && input[k].trim()))
|
|
24
|
+
throw new Error('断线输入需要doc_id/space/team_uid/edge_id,可选expected_version')
|
|
25
|
+
if (input.expected_version !== undefined && (!Number.isSafeInteger(input.expected_version) || input.expected_version < 1))
|
|
26
|
+
throw new Error('expected_version必须为正整数')
|
|
27
|
+
const doc = await scopedCanvas(client,input.doc_id,input.space,input.team_uid)
|
|
28
|
+
if (!Number.isSafeInteger(doc.version) || doc.version < 1 || !Array.isArray(doc.nodes) || !Array.isArray(doc.edges) ||
|
|
29
|
+
!doc.meta || typeof doc.meta !== 'object' || Array.isArray(doc.meta) ||
|
|
30
|
+
!doc.viewport || typeof doc.viewport !== 'object' || Array.isArray(doc.viewport) || typeof doc.name !== 'string' ||
|
|
31
|
+
doc.nodes.some(n=>!n || typeof n.id!=='string') || doc.edges.some(e=>!e || typeof e.id!=='string') ||
|
|
32
|
+
new Set(doc.nodes.map(n=>n.id)).size !== doc.nodes.length || new Set(doc.edges.map(e=>e.id)).size !== doc.edges.length)
|
|
33
|
+
throw new Error('画布快照不完整或ID重复')
|
|
34
|
+
if (input.expected_version !== undefined && input.expected_version !== doc.version) throw new Error('画布已变化,重新预览')
|
|
35
|
+
const edge = doc.edges.find(e=>e.id===input.edge_id)
|
|
36
|
+
if (!edge) throw new Error('连线不存在,先读回核对')
|
|
37
|
+
const source=doc.nodes.find(n=>n.id===edge.source), target=doc.nodes.find(n=>n.id===edge.target)
|
|
38
|
+
if (!source || !target) throw new Error('连线端点缺失')
|
|
39
|
+
if (edge.data?.repaintSystem || edge.data?.role || derived(source) || derived(target))
|
|
40
|
+
throw new Error('仅支持普通连线;系统线与派生流程不支持')
|
|
41
|
+
if (target.data?.generation_config || target.data?.node_output) throw new Error('目标配置协议尚未接入断线')
|
|
42
|
+
const next=structuredClone(doc), nextTarget=next.nodes.find(n=>n.id===target.id)
|
|
43
|
+
next.edges=next.edges.filter(e=>e.id!==edge.id)
|
|
44
|
+
const stillConnected=next.edges.some(e=>e.source===edge.source && e.target===edge.target)
|
|
45
|
+
if (!stillConnected) {
|
|
46
|
+
const refs=storedReferences(doc,target), reference=readSource(source)
|
|
47
|
+
const retained=refs.filter(r=>String(r.id)!==source.id)
|
|
48
|
+
nextTarget.data ||= {}
|
|
49
|
+
nextTarget.data.prompt=disconnectMentions(target.data?.prompt,reference)
|
|
50
|
+
if (Object.hasOwn(target.data||{},'references')) nextTarget.data.references=retained
|
|
51
|
+
if (target.data?.extra && Object.hasOwn(target.data.extra,'references')) nextTarget.data.extra.references=retained
|
|
52
|
+
if (doc.meta.node_references && Object.hasOwn(doc.meta.node_references,target.id)) next.meta.node_references[target.id]=retained
|
|
53
|
+
}
|
|
54
|
+
if (['image','video'].includes(target.type)) {
|
|
55
|
+
const catalog=await client.raw('/canvas/models'+(doc.team_uid?'?team_uid='+encodeURIComponent(doc.team_uid):''))
|
|
56
|
+
const model=catalog.items?.find(m=>m.label===target.data?.model_name && m.content_type===target.type && !m.member_locked)
|
|
57
|
+
if (!model) throw new Error('请先选择目标节点当前空间可用模型')
|
|
58
|
+
const types=next.edges.filter(e=>e.target===target.id).map(e=>sourceType(doc.nodes.find(n=>n.id===e.source)))
|
|
59
|
+
// Match the mounted node's mode normalization after removeEdge: retained
|
|
60
|
+
// persistent material references still constrain the eventual saved mode.
|
|
61
|
+
types.push(...storedReferences(next,nextTarget).map(r=>r.mediaType||sourceType(next.nodes.find(n=>n.id===r.id))).filter(Boolean))
|
|
62
|
+
const supported=(model.generation_modes||[]).map(normalizeMode).filter(m=>generationModes[m])
|
|
63
|
+
const compatible=supported.filter(m=>types.every(t=>generationModes[m][0].includes(t)) &&
|
|
64
|
+
(!generationModes[m][1].length || generationModes[m][1].some(t=>types.includes(t))))
|
|
65
|
+
// Model choice is a user decision: do not silently switch to another model.
|
|
66
|
+
if (!compatible.length) throw new Error('断线后当前模型无兼容模式,请先明确选择模型')
|
|
67
|
+
const current=normalizeMode(target.data?.extra?.generation_mode || target.data?.generation_mode || target.data?.imageMode || target.data?.videoMode)
|
|
68
|
+
const mode=compatible.includes(current)?current:compatible[0]
|
|
69
|
+
nextTarget.data.extra={...nextTarget.data.extra,generation_mode:mode}
|
|
70
|
+
if (Object.hasOwn(nextTarget.data,'generation_mode')) nextTarget.data.generation_mode=mode
|
|
71
|
+
}
|
|
72
|
+
const preview={doc_id:doc.id,edge_id:edge.id,source_node_id:source.id,target_node_id:target.id,
|
|
73
|
+
expected_version:doc.version,still_connected:stillConnected,prompt_before:target.data?.prompt||'',
|
|
74
|
+
prompt_after:nextTarget.data?.prompt||'',references_before:storedReferences(doc,target).map(r=>r.id),
|
|
75
|
+
references_after:storedReferences(next,nextTarget).map(r=>r.id),
|
|
76
|
+
mode_before:target.data?.extra?.generation_mode,mode_after:nextTarget.data?.extra?.generation_mode,
|
|
77
|
+
canvas_written:false,generation_submitted:false}
|
|
78
|
+
if (!confirmed) return preview
|
|
79
|
+
if (input.expected_version===undefined) throw new Error('执行须提供预览expected_version')
|
|
80
|
+
const saved=await save(client,doc,{nodes:next.nodes,edges:next.edges,meta:next.meta})
|
|
81
|
+
let actual
|
|
82
|
+
try { actual=await scopedCanvas(client,input.doc_id,input.space,input.team_uid) }
|
|
83
|
+
catch(cause){throw new Error('断线保存已尝试但读回未确认,不自动重放',{cause})}
|
|
84
|
+
if (!Number.isSafeInteger(actual.version) || actual.version < saved.version ||
|
|
85
|
+
!same(actual.viewport,doc.viewport) || actual.name !== doc.name ||
|
|
86
|
+
!same(actual.nodes,next.nodes) || !same(actual.edges,next.edges) || !same(actual.meta,next.meta))
|
|
87
|
+
throw new Error('断线读回不一致,保留当前画布,不自动重放')
|
|
88
|
+
return {...preview,version:actual.version,canvas_written:true,disconnect_verified:true}
|
|
89
|
+
}
|