@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,126 @@
|
|
|
1
|
+
// Frontend membership semantics; no runtime frontend dependency.
|
|
2
|
+
const DEFAULT_NODE_WIDTH = 200
|
|
3
|
+
const DEFAULT_NODE_HEIGHT = 100
|
|
4
|
+
|
|
5
|
+
function readNodeSize(node) {
|
|
6
|
+
const width = Number(node?.computedWidth) || Number(node?.width)
|
|
7
|
+
const height = Number(node?.computedHeight) || Number(node?.height)
|
|
8
|
+
return {
|
|
9
|
+
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_NODE_WIDTH,
|
|
10
|
+
height: Number.isFinite(height) && height > 0 ? height : DEFAULT_NODE_HEIGHT,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getNodeGroupBox(node) {
|
|
15
|
+
const position = node?.position || { x: 0, y: 0 }
|
|
16
|
+
const size = readNodeSize(node)
|
|
17
|
+
return {
|
|
18
|
+
x: Number(position.x) || 0,
|
|
19
|
+
y: Number(position.y) || 0,
|
|
20
|
+
width: size.width,
|
|
21
|
+
height: size.height,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeGroupBox(group) {
|
|
26
|
+
const position = group?.position || {}
|
|
27
|
+
const size = group?.size || {}
|
|
28
|
+
return {
|
|
29
|
+
x: Number(position.x) || 0,
|
|
30
|
+
y: Number(position.y) || 0,
|
|
31
|
+
width: Math.max(Number(size.width) || 0, 0),
|
|
32
|
+
height: Math.max(Number(size.height) || 0, 0),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 节点中心点是否落在组框内。
|
|
38
|
+
*/
|
|
39
|
+
export function isNodeInsideGroup(node, group) {
|
|
40
|
+
const box = normalizeGroupBox(group)
|
|
41
|
+
if (box.width <= 0 || box.height <= 0) return false
|
|
42
|
+
|
|
43
|
+
const nodeBox = getNodeGroupBox(node)
|
|
44
|
+
const centerX = nodeBox.x + nodeBox.width / 2
|
|
45
|
+
const centerY = nodeBox.y + nodeBox.height / 2
|
|
46
|
+
return (
|
|
47
|
+
centerX >= box.x &&
|
|
48
|
+
centerX <= box.x + box.width &&
|
|
49
|
+
centerY >= box.y &&
|
|
50
|
+
centerY <= box.y + box.height
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function syncGroupMemberships(state) {
|
|
55
|
+
if (!state || !Array.isArray(state.groups) || !Array.isArray(state.nodes)) return false
|
|
56
|
+
|
|
57
|
+
const existingNodeIds = new Set(state.nodes.map((node) => String(node?.id || '')).filter(Boolean))
|
|
58
|
+
const claimedNodeIds = new Set()
|
|
59
|
+
let changed = false
|
|
60
|
+
|
|
61
|
+
for (const group of state.groups) {
|
|
62
|
+
const activeIds = (Array.isArray(group.nodeIds) ? group.nodeIds : [])
|
|
63
|
+
.map((id) => String(id || ''))
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
const exitedIds = (Array.isArray(group.exitedNodeIds) ? group.exitedNodeIds : [])
|
|
66
|
+
.map((id) => String(id || ''))
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
const aliveIds = new Set([...activeIds, ...exitedIds].filter((id) => existingNodeIds.has(id)))
|
|
69
|
+
|
|
70
|
+
const nextActive = []
|
|
71
|
+
const nextExited = []
|
|
72
|
+
|
|
73
|
+
for (const id of activeIds) {
|
|
74
|
+
if (!aliveIds.has(id) || claimedNodeIds.has(id)) {
|
|
75
|
+
changed = true
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const node = state.nodes.find((item) => String(item?.id || '') === id)
|
|
79
|
+
if (isNodeInsideGroup(node, group)) {
|
|
80
|
+
nextActive.push(id)
|
|
81
|
+
claimedNodeIds.add(id)
|
|
82
|
+
} else {
|
|
83
|
+
nextExited.push(id)
|
|
84
|
+
changed = true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const id of exitedIds) {
|
|
89
|
+
if (!aliveIds.has(id) || claimedNodeIds.has(id)) {
|
|
90
|
+
changed = true
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
const node = state.nodes.find((item) => String(item?.id || '') === id)
|
|
94
|
+
if (isNodeInsideGroup(node, group)) {
|
|
95
|
+
nextActive.push(id)
|
|
96
|
+
claimedNodeIds.add(id)
|
|
97
|
+
changed = true
|
|
98
|
+
} else {
|
|
99
|
+
nextExited.push(id)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const finalActive = [...new Set(nextActive)]
|
|
104
|
+
const finalExited = [...new Set(nextExited)]
|
|
105
|
+
const currentActive = [...new Set(activeIds)]
|
|
106
|
+
const currentExited = [...new Set(exitedIds)]
|
|
107
|
+
const activeChanged =
|
|
108
|
+
currentActive.length !== finalActive.length ||
|
|
109
|
+
currentActive.some((id, index) => id !== finalActive[index])
|
|
110
|
+
const exitedChanged =
|
|
111
|
+
currentExited.length !== finalExited.length ||
|
|
112
|
+
currentExited.some((id, index) => id !== finalExited[index])
|
|
113
|
+
|
|
114
|
+
if (activeChanged) {
|
|
115
|
+
group.nodeIds = finalActive
|
|
116
|
+
changed = true
|
|
117
|
+
}
|
|
118
|
+
if (exitedChanged) {
|
|
119
|
+
group.exitedNodeIds = finalExited
|
|
120
|
+
changed = true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return changed
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { isDeepStrictEqual as same } from 'node:util'
|
|
3
|
+
import { atomic, json, root, safeName } from './store.js'
|
|
4
|
+
import { nodeHistoryInput, historyContext, selectedHistory } from './node-history.js'
|
|
5
|
+
import { requireServiceReady } from './service.js'
|
|
6
|
+
import { captureMediaHistory } from './media-copy.js'
|
|
7
|
+
import { sameImageReferences, sameVideoReferences } from './media.js'
|
|
8
|
+
|
|
9
|
+
const plain = v => JSON.parse(JSON.stringify(v))
|
|
10
|
+
export const historyAddPath = (profile, operation) => path.join(root(), 'history-add', safeName(profile), safeName(operation) + '.json')
|
|
11
|
+
const persist = r => atomic(historyAddPath(r.profile, r.operation), r)
|
|
12
|
+
export function historyAddInput(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(k =>
|
|
14
|
+
!['doc_id','node_id','space','team_uid','task_id','new_node_id','position','title','expected_version'].includes(k))) throw Error('历史添加输入无效')
|
|
15
|
+
const { new_node_id, position, title, expected_version, ...selection } = value
|
|
16
|
+
const input = nodeHistoryInput(selection)
|
|
17
|
+
if (typeof new_node_id !== 'string' || !new_node_id.trim() || new_node_id === input.node_id ||
|
|
18
|
+
!position || Object.keys(position).some(k => !['x','y'].includes(k)) || !Number.isFinite(position.x) || !Number.isFinite(position.y)) throw Error('历史添加需要不同的new_node_id及有效position')
|
|
19
|
+
if (title !== undefined && (typeof title !== 'string' || !title.trim())) throw Error('title无效')
|
|
20
|
+
if (expected_version !== undefined && (!Number.isSafeInteger(expected_version) || expected_version < 1)) throw Error('expected_version无效')
|
|
21
|
+
return {...input,new_node_id,position:{x:Math.round(position.x),y:Math.round(position.y)},...(title === undefined ? {} : {title}),...(expected_version === undefined ? {} : {expected_version})}
|
|
22
|
+
}
|
|
23
|
+
function promptOnly(item) {
|
|
24
|
+
const prompt = String(item.prompt || '').trim(), names = new Map()
|
|
25
|
+
const block = prompt.match(/<!-- flow:auto-material-preparation:start -->\r?\n([\s\S]*?)\r?\n<!-- flow:auto-material-preparation:end -->/)?.[1] || ''
|
|
26
|
+
for (const match of block.matchAll(/(@(?:图片|视频|音频)\d+)是([^;;\r\n]+?)(?=[;;。]|$)/g)) names.set(match[1],match[2])
|
|
27
|
+
const allowed = new Set(['image','video','audio'].flatMap(type => (item[`reference_${type}_urls`] || []).map((_,i) => '@'+({image:'图片',video:'视频',audio:'音频'}[type])+(i+1))))
|
|
28
|
+
return prompt.replace(/<!-- flow:auto-material-preparation:start -->\r?\n[\s\S]*?\r?\n<!-- flow:auto-material-preparation:end -->(?:\r?\n){0,2}/g,'')
|
|
29
|
+
.replace(/@(?:图片|视频|音频)\d+/g, marker => allowed.has(marker) ? names.get(marker) || marker : marker)
|
|
30
|
+
}
|
|
31
|
+
// Counterpart of buildCompletedHistoryNodeData: new ordinary generation node,
|
|
32
|
+
// output only, no inherited inputs, review authorization, draft or running task.
|
|
33
|
+
// Store provenance in copied history so web serialization retains sourceTaskId.
|
|
34
|
+
export function buildHistoryAddedNode(source, item, input) {
|
|
35
|
+
const type = source.type, result = structuredClone(item.result)
|
|
36
|
+
for (const key of ['task_id','taskId','task_no','taskNo']) delete result[key]
|
|
37
|
+
const data = {title:input.title || `${type === 'image' ? '图片历史' : '视频历史'}${item.task_id}`,
|
|
38
|
+
prompt:promptOnly(item),model_name:String(item.model_name || ''),status:'completed',last_task_id:null,
|
|
39
|
+
duration:Number(item.duration ?? result.duration) || null,result:{...result,status:result.status || 'completed'},error_message:'',
|
|
40
|
+
extra:{node_kind:'generation',file_ref:result.file_ref,source_task_id:String(item.task_id),
|
|
41
|
+
source_doc_id:item.doc_id,source_node_id:item.node_id,
|
|
42
|
+
...(item.aspect_ratio || item.extra?.aspect_ratio ? {aspect_ratio:item.aspect_ratio || item.extra.aspect_ratio} : {}),
|
|
43
|
+
...(item.resolution || item.extra?.resolution ? {resolution:item.resolution || item.extra.resolution} : {}),
|
|
44
|
+
image_count:Number(item.extra?.image_count) || 1,
|
|
45
|
+
...(type === 'image' ? {image_to_image_enabled:true,asset_type:'character'} : {})}}
|
|
46
|
+
const node = {id:input.new_node_id,type,position:input.position,width:486,height:420,data}
|
|
47
|
+
data.extra.copied_generation_history = captureMediaHistory({...node,id:item.node_id})
|
|
48
|
+
const history = data.extra.copied_generation_history[0]
|
|
49
|
+
Object.assign(history,{docId:item.doc_id,nodeId:item.node_id,segmentType:type,contentType:type,taskNo:String(item.task_no || ''),
|
|
50
|
+
mode:String(item.generation_mode || item.extra?.generation_mode || ''),prompt:String(item.prompt || ''),
|
|
51
|
+
createdAt:item.created_at || '',completedAt:item.completed_at || '',generatedAt:item.completed_at || item.created_at || '',
|
|
52
|
+
raw:{...structuredClone(item),id:String(item.task_id),source:'copied'}})
|
|
53
|
+
return plain(node)
|
|
54
|
+
}
|
|
55
|
+
function identity(item) {
|
|
56
|
+
const copy = plain(item)
|
|
57
|
+
for (const key of ['url','file_ref','preview_url','preview_file_ref']) delete copy.result[key]
|
|
58
|
+
return copy
|
|
59
|
+
}
|
|
60
|
+
const summary = r => ({operation:r.operation,state:r.state,doc_id:r.input.doc_id,source_node_id:r.input.node_id,
|
|
61
|
+
source_task_id:r.input.task_id,new_node_id:r.input.new_node_id,canvas_written:false,generation_submitted:false,library_saved:false,
|
|
62
|
+
add_verified:r.state === 'applied'})
|
|
63
|
+
async function bound(client, profile, operation) {
|
|
64
|
+
const r = await json(historyAddPath(profile,operation),null)
|
|
65
|
+
if (!r || r.schema !== 1 || r.profile !== profile || r.operation !== operation || r.baseUrl !== client.baseUrl ||
|
|
66
|
+
!r.before || !r.pendingNode || r.pendingNode.id !== r.input?.new_node_id) throw Error('历史添加operation不存在、不完整或服务不匹配')
|
|
67
|
+
historyAddInput(r.input)
|
|
68
|
+
const ctx = await historyContext(client,r.input)
|
|
69
|
+
if (ctx.owner !== r.owner) throw Error('历史添加账号不匹配')
|
|
70
|
+
return {r,ctx}
|
|
71
|
+
}
|
|
72
|
+
export async function addNodeHistory(client, profile, value, operation, confirmed = false) {
|
|
73
|
+
const input = historyAddInput(value)
|
|
74
|
+
await requireServiceReady(client)
|
|
75
|
+
const old = await json(historyAddPath(profile,operation),null)
|
|
76
|
+
if (old && (!same(old.input,input) || old.baseUrl !== client.baseUrl || old.profile !== profile)) throw Error('原历史添加operation绑定不可更改')
|
|
77
|
+
if (old?.attempted) return recoverHistoryAdd(client,profile,operation)
|
|
78
|
+
const ctx = await historyContext(client,input)
|
|
79
|
+
if (old && ctx.owner !== old.owner) throw Error('历史添加账号不匹配')
|
|
80
|
+
if (ctx.doc.nodes.some(n => n.id === input.new_node_id)) throw Error('新节点ID已存在;不覆盖')
|
|
81
|
+
if (input.expected_version !== undefined && input.expected_version !== ctx.doc.version) throw Error('画布版本已变化,重新预览')
|
|
82
|
+
const item = await selectedHistory(client,input,ctx)
|
|
83
|
+
if (!confirmed) {
|
|
84
|
+
const r = {schema:1,profile,operation,baseUrl:client.baseUrl,owner:ctx.owner,input,before:ctx.doc,
|
|
85
|
+
identity:identity(item),media:{url:item.result.url,file_ref:item.result.file_ref},pendingNode:buildHistoryAddedNode(ctx.node,item,input),state:'prepared'}
|
|
86
|
+
await persist(r)
|
|
87
|
+
return {...summary(r),expected_version:ctx.doc.version,node:r.pendingNode,source_unchanged:true,references_restored:false}
|
|
88
|
+
}
|
|
89
|
+
const {r,ctx:checked} = await bound(client,profile,operation)
|
|
90
|
+
if (!same(checked.doc,r.before) || !same(identity(item),r.identity)) throw Error('预览后画布或历史已变化;不覆盖')
|
|
91
|
+
if (!same(r.media,{url:item.result.url,file_ref:item.result.file_ref}) &&
|
|
92
|
+
!await (ctx.node.type === 'image' ? sameImageReferences : sameVideoReferences)(client,r.media.file_ref,item.result.file_ref)) throw Error('历史更新引用不能确认是原媒体')
|
|
93
|
+
r.pendingNode = buildHistoryAddedNode(ctx.node,item,input)
|
|
94
|
+
r.attempted = true; r.state = 'saving'; await persist(r)
|
|
95
|
+
const doc = checked.doc
|
|
96
|
+
try {
|
|
97
|
+
const saved = await client.raw('/canvas/docs/'+encodeURIComponent(input.doc_id),{method:'PUT',body:{version:doc.version,name:doc.name,
|
|
98
|
+
viewport:doc.viewport,nodes:[...doc.nodes,r.pendingNode],edges:doc.edges,meta:doc.meta}},{retry:false})
|
|
99
|
+
if (saved?.version !== doc.version+1) throw Error('保存响应版本不完整')
|
|
100
|
+
const result = await recoverHistoryAdd(client,profile,operation)
|
|
101
|
+
if (!result.add_verified || !result.canvas_preserved) throw Error('历史添加保存后读回不一致')
|
|
102
|
+
return {...result,canvas_written:true}
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
r.state = cause.status === 409 || cause.apiCode === 3002 ? 'conflict' : 'save_unknown'; await persist(r)
|
|
105
|
+
throw Object.assign(Error('历史添加保存未确认;仅用原history-add-recover核对,不重发保存',{cause}),{code:'NODE_HISTORY_SAVE_PENDING'})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function recoverHistoryAdd(client,profile,operation) {
|
|
109
|
+
await requireServiceReady(client)
|
|
110
|
+
const {r,ctx} = await bound(client,profile,operation)
|
|
111
|
+
if (!r.attempted) return summary(r)
|
|
112
|
+
const matches = same(ctx.doc.nodes.find(n => n.id === r.input.new_node_id),r.pendingNode)
|
|
113
|
+
const preserved = same(ctx.doc.nodes.filter(n => n.id !== r.input.new_node_id),r.before.nodes) &&
|
|
114
|
+
['edges','viewport','meta','name'].every(k => same(ctx.doc[k],r.before[k]))
|
|
115
|
+
if (matches) {r.state='applied';await persist(r)}
|
|
116
|
+
return {...summary(r),state:!matches && r.state === 'applied' ? 'canvas_changed' : r.state,
|
|
117
|
+
add_verified:matches,canvas_preserved:preserved,source_unchanged:same(ctx.node,r.before.nodes.find(n => n.id === r.input.node_id)),
|
|
118
|
+
version:ctx.doc.version,next:matches ? '原历史结果新增已核验;其他内容后续变化分别报告' : '保留当前画布;不自动覆盖、重建或重发保存'}
|
|
119
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { atomic, json, root, safeName } from './store.js'
|
|
5
|
+
import { downloadImage, hashFile } from './media.js'
|
|
6
|
+
import { readImageSource } from './image-crop.js'
|
|
7
|
+
import {
|
|
8
|
+
annotationCommands,
|
|
9
|
+
annotationGeometry,
|
|
10
|
+
annotationTools,
|
|
11
|
+
annotateImage
|
|
12
|
+
} from './annotation-media.js'
|
|
13
|
+
import { imageOutputWorkflow } from './image-output.js'
|
|
14
|
+
|
|
15
|
+
const mediaDefault = { annotationTools, annotateImage, downloadImage }
|
|
16
|
+
const digest = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
17
|
+
export const annotationPath = (profile, operation) =>
|
|
18
|
+
path.join(root(), 'image-annotations', safeName(profile), safeName(operation) + '.json')
|
|
19
|
+
export function annotationInput(value) {
|
|
20
|
+
if (
|
|
21
|
+
!value ||
|
|
22
|
+
typeof value !== 'object' ||
|
|
23
|
+
Array.isArray(value) ||
|
|
24
|
+
Object.keys(value).some(
|
|
25
|
+
(k) => !['doc_id', 'node_id', 'space', 'team_uid', 'commands', 'font_file'].includes(k)
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
throw new Error('标注输入无效或包含未接入字段')
|
|
29
|
+
if (
|
|
30
|
+
typeof value.doc_id !== 'string' ||
|
|
31
|
+
!value.doc_id.trim() ||
|
|
32
|
+
typeof value.node_id !== 'string' ||
|
|
33
|
+
!value.node_id.trim()
|
|
34
|
+
)
|
|
35
|
+
throw new Error('标注缺少doc_id/node_id')
|
|
36
|
+
if (
|
|
37
|
+
value.font_file !== undefined &&
|
|
38
|
+
(typeof value.font_file !== 'string' || !value.font_file.trim())
|
|
39
|
+
)
|
|
40
|
+
throw new Error('font_file须为已有本机字体路径')
|
|
41
|
+
return { ...value, commands: annotationCommands(value.commands) }
|
|
42
|
+
}
|
|
43
|
+
async function identity(client) {
|
|
44
|
+
const me = await client.raw('/auth/me'),
|
|
45
|
+
uid = String(me.uid ?? me.id ?? '')
|
|
46
|
+
if (!uid) throw new Error('标注账号缺少UID')
|
|
47
|
+
return uid
|
|
48
|
+
}
|
|
49
|
+
async function context(client, input) {
|
|
50
|
+
const current = await readImageSource(client, input)
|
|
51
|
+
return {
|
|
52
|
+
...current,
|
|
53
|
+
geometry: annotationGeometry(current.source, input.commands),
|
|
54
|
+
binding: { ...current.binding, source_type: current.node.type }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const summary = (record) => ({
|
|
58
|
+
operation: record.operation,
|
|
59
|
+
state: record.state,
|
|
60
|
+
generation_submitted: false,
|
|
61
|
+
library_saved: false,
|
|
62
|
+
doc_id: record.input.doc_id,
|
|
63
|
+
source_node_id: record.input.node_id,
|
|
64
|
+
...record.geometry,
|
|
65
|
+
output_type: record.outputType,
|
|
66
|
+
command_count: record.input.commands.length,
|
|
67
|
+
command_types: [...new Set(record.input.commands.map((c) => c.type))],
|
|
68
|
+
items: record.items.map((item) => ({
|
|
69
|
+
node_id: item.nodeId,
|
|
70
|
+
state: item.state,
|
|
71
|
+
canvas_bound: item.bound === true,
|
|
72
|
+
task_id: item.response?.task_id || null,
|
|
73
|
+
width: item.local?.width || null,
|
|
74
|
+
height: item.local?.height || null
|
|
75
|
+
})),
|
|
76
|
+
next:
|
|
77
|
+
record.state === 'completed'
|
|
78
|
+
? '读取画布/PNG核验;不自动入库或生成'
|
|
79
|
+
: '先查后端和原任务;annotate-recover原operation --yes恢复已知阶段,不重发未知complete'
|
|
80
|
+
})
|
|
81
|
+
const { persistNodes, runUploads } = imageOutputWorkflow({
|
|
82
|
+
recordPath: annotationPath,
|
|
83
|
+
readContext: context,
|
|
84
|
+
nodeTitle: (item) => item.title,
|
|
85
|
+
nodePosition: (_nodes, source) => ({
|
|
86
|
+
x: Number(source.position?.x || 0) + Number(source.width || 320) + 80,
|
|
87
|
+
y: Number(source.position?.y || 0)
|
|
88
|
+
}),
|
|
89
|
+
nodeType: (record) => record.outputType,
|
|
90
|
+
edgeRole: 'image-edit-link',
|
|
91
|
+
edgePrefix: 'annotation-link-',
|
|
92
|
+
codePrefix: 'ANNOTATION',
|
|
93
|
+
mime: 'image/png',
|
|
94
|
+
summarize: summary
|
|
95
|
+
})
|
|
96
|
+
async function bound(client, profile, operation) {
|
|
97
|
+
const record = await json(annotationPath(profile, operation), null)
|
|
98
|
+
if (!record) throw new Error('未找到标注operation')
|
|
99
|
+
if (record.baseUrl !== client.baseUrl || record.owner !== (await identity(client)))
|
|
100
|
+
throw new Error('标注operation账号或服务不匹配')
|
|
101
|
+
const current = await context(client, record.input)
|
|
102
|
+
if (
|
|
103
|
+
digest(current.binding) !== digest(record.binding) ||
|
|
104
|
+
digest(current.geometry) !== digest(record.geometry)
|
|
105
|
+
)
|
|
106
|
+
throw new Error('标注原图/节点类型/任务已变化,停止恢复')
|
|
107
|
+
return { record, current }
|
|
108
|
+
}
|
|
109
|
+
async function prepareLocal(client, profile, record, current, options, media) {
|
|
110
|
+
const item = record.items[0]
|
|
111
|
+
if (item.local) return
|
|
112
|
+
const tools = await media.annotationTools(record.input.commands, {
|
|
113
|
+
...options,
|
|
114
|
+
fontFile: record.rendering?.fontFile || record.input.font_file
|
|
115
|
+
})
|
|
116
|
+
if (record.rendering?.fontHash && digest(record.rendering.fontHash) !== digest(tools.fontHash))
|
|
117
|
+
throw new Error('原标注字体文件已变化,停止恢复')
|
|
118
|
+
await fs.mkdir(record.directory, { recursive: true })
|
|
119
|
+
const source = path.join(record.directory, 'source.img')
|
|
120
|
+
if (!record.download) {
|
|
121
|
+
await fs.rm(source, { force: true })
|
|
122
|
+
record.download = await media.downloadImage(client, current.url, source)
|
|
123
|
+
await atomic(annotationPath(profile, record.operation), record)
|
|
124
|
+
} else if (digest(await hashFile(source)) !== digest(record.download))
|
|
125
|
+
throw new Error('本地标注原图缓存已变化')
|
|
126
|
+
const target = path.join(record.directory, item.fileName)
|
|
127
|
+
await fs.rm(target, { force: true })
|
|
128
|
+
item.local = await media.annotateImage(
|
|
129
|
+
source,
|
|
130
|
+
target,
|
|
131
|
+
record.geometry,
|
|
132
|
+
record.input.commands,
|
|
133
|
+
tools
|
|
134
|
+
)
|
|
135
|
+
record.state = 'local_ready'
|
|
136
|
+
await atomic(annotationPath(profile, record.operation), record)
|
|
137
|
+
}
|
|
138
|
+
export async function startAnnotation(
|
|
139
|
+
client,
|
|
140
|
+
profile,
|
|
141
|
+
value,
|
|
142
|
+
operation,
|
|
143
|
+
confirmed = false,
|
|
144
|
+
options = {},
|
|
145
|
+
media = mediaDefault
|
|
146
|
+
) {
|
|
147
|
+
const input = annotationInput(value),
|
|
148
|
+
file = annotationPath(profile, operation),
|
|
149
|
+
old = await json(file, null)
|
|
150
|
+
if (old) {
|
|
151
|
+
if (digest(old.input) !== digest(input)) throw new Error('原标注operation不可改变输入')
|
|
152
|
+
return getAnnotation(client, profile, operation)
|
|
153
|
+
}
|
|
154
|
+
const tools = await media.annotationTools(input.commands, {
|
|
155
|
+
...options,
|
|
156
|
+
fontFile: input.font_file
|
|
157
|
+
}),
|
|
158
|
+
current = await context(client, input),
|
|
159
|
+
owner = await identity(client)
|
|
160
|
+
const outputType = current.node.type === 'image' ? 'image' : 'image_upload'
|
|
161
|
+
if (!confirmed)
|
|
162
|
+
return {
|
|
163
|
+
operation,
|
|
164
|
+
submitted: false,
|
|
165
|
+
generation_submitted: false,
|
|
166
|
+
library_saved: false,
|
|
167
|
+
source_task_id: current.binding.task_id,
|
|
168
|
+
...current.geometry,
|
|
169
|
+
output_format: 'image/png',
|
|
170
|
+
output_type: outputType,
|
|
171
|
+
commands: input.commands,
|
|
172
|
+
media_tools: 'available',
|
|
173
|
+
next: '核对标注位置和源图,暂停同画布编辑后加--yes;本地合成、真实上传并新增下游节点,不生成/入库'
|
|
174
|
+
}
|
|
175
|
+
const title = current.node.data?.title || '图片'
|
|
176
|
+
const record = {
|
|
177
|
+
operation,
|
|
178
|
+
input,
|
|
179
|
+
owner,
|
|
180
|
+
baseUrl: client.baseUrl,
|
|
181
|
+
binding: current.binding,
|
|
182
|
+
geometry: current.geometry,
|
|
183
|
+
outputType,
|
|
184
|
+
rendering: {
|
|
185
|
+
engine: 'ffmpeg-drawvg/drawtext',
|
|
186
|
+
...(tools.fontFile ? { fontFile: tools.fontFile, fontHash: tools.fontHash } : {})
|
|
187
|
+
},
|
|
188
|
+
directory: path.join(path.dirname(file), safeName(operation) + '-files'),
|
|
189
|
+
createdAt: new Date().toISOString(),
|
|
190
|
+
state: 'preparing',
|
|
191
|
+
items: [
|
|
192
|
+
{
|
|
193
|
+
nodeId: 'annotate-' + crypto.randomUUID(),
|
|
194
|
+
title: title + '(标注)',
|
|
195
|
+
fileName:
|
|
196
|
+
(title.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '_').slice(0, 80) || 'edited-image') +
|
|
197
|
+
'.png',
|
|
198
|
+
state: 'prepared'
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
await atomic(file, record)
|
|
203
|
+
await prepareLocal(client, profile, record, current, tools, media)
|
|
204
|
+
await persistNodes(client, profile, record, (await bound(client, profile, operation)).current)
|
|
205
|
+
return runUploads(client, profile, record)
|
|
206
|
+
}
|
|
207
|
+
export async function getAnnotation(client, profile, operation) {
|
|
208
|
+
const { record, current } = await bound(client, profile, operation),
|
|
209
|
+
result = summary(record)
|
|
210
|
+
for (let i = 0; i < record.items.length; i++) {
|
|
211
|
+
const item = record.items[i],
|
|
212
|
+
node = current.doc.nodes.find((n) => n.id === item.nodeId)
|
|
213
|
+
if (
|
|
214
|
+
item.bound &&
|
|
215
|
+
(node?.type !== record.outputType ||
|
|
216
|
+
node.data?.extra?.node_kind !== (record.outputType === 'image' ? 'generation' : 'asset') ||
|
|
217
|
+
node.data?.extra?.file_ref !== item.response.result.file_ref ||
|
|
218
|
+
!current.doc.edges.some(
|
|
219
|
+
(e) =>
|
|
220
|
+
e.source === record.input.node_id &&
|
|
221
|
+
e.target === item.nodeId &&
|
|
222
|
+
e.data?.role === 'image-edit-link'
|
|
223
|
+
))
|
|
224
|
+
) {
|
|
225
|
+
result.state = 'canvas_changed'
|
|
226
|
+
result.items[i].state = 'canvas_changed'
|
|
227
|
+
result.items[i].canvas_bound = false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result
|
|
231
|
+
}
|
|
232
|
+
export async function recoverAnnotation(
|
|
233
|
+
client,
|
|
234
|
+
profile,
|
|
235
|
+
operation,
|
|
236
|
+
confirmed = false,
|
|
237
|
+
options = {},
|
|
238
|
+
media = mediaDefault
|
|
239
|
+
) {
|
|
240
|
+
if (!confirmed) return getAnnotation(client, profile, operation)
|
|
241
|
+
const { record, current } = await bound(client, profile, operation)
|
|
242
|
+
if (record.state === 'completed') return getAnnotation(client, profile, operation)
|
|
243
|
+
await prepareLocal(client, profile, record, current, options, media)
|
|
244
|
+
await persistNodes(client, profile, record, (await bound(client, profile, operation)).current)
|
|
245
|
+
return runUploads(client, profile, record)
|
|
246
|
+
}
|