@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,283 @@
|
|
|
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 { scopedCanvas } from './materials.js'
|
|
6
|
+
import { readSource } from './generation-input.js'
|
|
7
|
+
import { segment } from './workflows.js'
|
|
8
|
+
import { mediaTools, downloadImage, hashFile, sameImageReferences } from './media.js'
|
|
9
|
+
import { cropImage, cropGeometry } from './image-media.js'
|
|
10
|
+
import { imageOutputWorkflow } from './image-output.js'
|
|
11
|
+
|
|
12
|
+
const mediaDefault = { mediaTools, downloadImage, cropImage }
|
|
13
|
+
const text = (value) => String(value ?? '').trim()
|
|
14
|
+
const digest = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
15
|
+
export const cropPath = (profile, operation) =>
|
|
16
|
+
path.join(root(), 'image-crops', safeName(profile), safeName(operation) + '.json')
|
|
17
|
+
export function cropInput(value) {
|
|
18
|
+
if (
|
|
19
|
+
!value ||
|
|
20
|
+
typeof value !== 'object' ||
|
|
21
|
+
Array.isArray(value) ||
|
|
22
|
+
Object.keys(value).some(
|
|
23
|
+
(key) => !['doc_id', 'node_id', 'space', 'team_uid', 'rect', 'aspect_ratio'].includes(key)
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
throw new Error('裁剪输入无效或包含未接入字段')
|
|
27
|
+
if (!text(value.doc_id) || !text(value.node_id)) throw new Error('裁剪缺少 doc_id/node_id')
|
|
28
|
+
if (Object.hasOwn(value, 'rect') === Object.hasOwn(value, 'aspect_ratio'))
|
|
29
|
+
throw new Error('裁剪须提供 rect 或居中 aspect_ratio,不能同时提供')
|
|
30
|
+
if (Object.hasOwn(value, 'rect')) {
|
|
31
|
+
const rect = value.rect
|
|
32
|
+
if (
|
|
33
|
+
!rect ||
|
|
34
|
+
typeof rect !== 'object' ||
|
|
35
|
+
Array.isArray(rect) ||
|
|
36
|
+
Object.keys(rect).length !== 4 ||
|
|
37
|
+
Object.keys(rect).some((k) => !['left', 'top', 'width', 'height'].includes(k)) ||
|
|
38
|
+
!Object.values(rect).every(Number.isSafeInteger) ||
|
|
39
|
+
rect.left < 0 ||
|
|
40
|
+
rect.top < 0 ||
|
|
41
|
+
rect.width <= 0 ||
|
|
42
|
+
rect.height <= 0
|
|
43
|
+
)
|
|
44
|
+
throw new Error('rect 使用原图整数像素 left/top/width/height,起点非负且尺寸为正')
|
|
45
|
+
return {
|
|
46
|
+
...value,
|
|
47
|
+
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ratio = text(value.aspect_ratio).replace(/:/g, ':').replace(/\s/g, '')
|
|
51
|
+
const match = ratio.match(/^(\d+(?:\.\d+)?)[:/xX*](\d+(?:\.\d+)?)$/)
|
|
52
|
+
const number = match ? Number(match[1]) / Number(match[2]) : NaN
|
|
53
|
+
if (!Number.isFinite(number) || number < 0.05 || number > 20 || Number(match?.[1]) <= 0)
|
|
54
|
+
throw new Error('aspect_ratio 须为 3:4 等宽高比,范围 1:20–20:1;按原图最大居中区域裁剪')
|
|
55
|
+
return { ...value, aspect_ratio: ratio }
|
|
56
|
+
}
|
|
57
|
+
async function identity(client) {
|
|
58
|
+
const me = await client.raw('/auth/me'),
|
|
59
|
+
uid = text(me.uid ?? me.id)
|
|
60
|
+
if (!uid) throw new Error('裁剪账号响应缺少 UID')
|
|
61
|
+
return uid
|
|
62
|
+
}
|
|
63
|
+
export async function readImageSource(client, input) {
|
|
64
|
+
const doc = await scopedCanvas(client, input.doc_id, input.space, input.team_uid)
|
|
65
|
+
const node = doc.nodes.find((n) => n.id === input.node_id),
|
|
66
|
+
data = node?.data || {}
|
|
67
|
+
if (
|
|
68
|
+
!node ||
|
|
69
|
+
!['image', 'image_upload'].includes(node.type) ||
|
|
70
|
+
data.extra?.repaint ||
|
|
71
|
+
data.extra?.video_remake ||
|
|
72
|
+
data.extra?.clip_redraw
|
|
73
|
+
)
|
|
74
|
+
throw new Error('图片处理仅支持普通图片生成/图片素材节点')
|
|
75
|
+
const source = readSource(node)
|
|
76
|
+
const refs = [
|
|
77
|
+
data.resultFileRef,
|
|
78
|
+
data.fileRef,
|
|
79
|
+
data.file_ref,
|
|
80
|
+
data.extra?.file_ref,
|
|
81
|
+
data.result?.file_ref,
|
|
82
|
+
data.node_output?.file_ref
|
|
83
|
+
]
|
|
84
|
+
.map(text)
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
if (!source.url || source.uploading || source.uploadError || new Set(refs).size !== 1)
|
|
87
|
+
throw new Error('原图未上传完成或媒体引用不一致')
|
|
88
|
+
const taskId = text(
|
|
89
|
+
data.sourceTaskId || data.extra?.source_task_id || data.last_task_id || data.lastTaskId
|
|
90
|
+
)
|
|
91
|
+
if (!/^[1-9]\d*$/.test(taskId))
|
|
92
|
+
throw new Error('当前图片处理要求同画布真实图片任务;无任务素材来源待接入')
|
|
93
|
+
const task = await client.raw('/canvas/generations/' + segment(taskId))
|
|
94
|
+
if (
|
|
95
|
+
String(task.task_id ?? task.id) !== taskId ||
|
|
96
|
+
task.doc_id !== doc.id ||
|
|
97
|
+
task.segment_type !== 'image' ||
|
|
98
|
+
task.status !== 'completed'
|
|
99
|
+
)
|
|
100
|
+
throw new Error('图片来源与同画布已完成图片任务不匹配')
|
|
101
|
+
if (!['image/png', 'image/jpeg'].includes(task.result?.mime_type))
|
|
102
|
+
throw new Error('当前图片处理仅接入静态 PNG/JPEG 原图')
|
|
103
|
+
if (!(await sameImageReferences(client, refs[0], task.result?.file_ref)))
|
|
104
|
+
throw new Error('原图与任务结果不是同一媒体')
|
|
105
|
+
const geometry = cropGeometry(task.result, {
|
|
106
|
+
aspect_ratio: task.result.width + ':' + task.result.height
|
|
107
|
+
})
|
|
108
|
+
return {
|
|
109
|
+
doc,
|
|
110
|
+
node,
|
|
111
|
+
source: geometry.source,
|
|
112
|
+
url: client.baseUrl + '/files/' + encodeURIComponent(refs[0]),
|
|
113
|
+
binding: { task_id: taskId, file_ref: refs[0], ...geometry.source }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function context(client, input) {
|
|
117
|
+
const current = await readImageSource(client, input)
|
|
118
|
+
return { ...current, geometry: cropGeometry(current.source, input) }
|
|
119
|
+
}
|
|
120
|
+
const summary = (record) => ({
|
|
121
|
+
operation: record.operation,
|
|
122
|
+
state: record.state,
|
|
123
|
+
generation_submitted: false,
|
|
124
|
+
library_saved: false,
|
|
125
|
+
doc_id: record.input.doc_id,
|
|
126
|
+
source_node_id: record.input.node_id,
|
|
127
|
+
...record.geometry,
|
|
128
|
+
items: record.items.map((item) => ({
|
|
129
|
+
node_id: item.nodeId,
|
|
130
|
+
state: item.state,
|
|
131
|
+
canvas_bound: item.bound === true,
|
|
132
|
+
task_id: item.response?.task_id || null,
|
|
133
|
+
width: item.local?.width || null,
|
|
134
|
+
height: item.local?.height || null
|
|
135
|
+
})),
|
|
136
|
+
next:
|
|
137
|
+
record.state === 'completed'
|
|
138
|
+
? '读取画布与 PNG 核验;不会自动入库'
|
|
139
|
+
: '先检查后端和原节点;crop-recover 原 operation --yes 只恢复已知阶段,不重发未知 complete'
|
|
140
|
+
})
|
|
141
|
+
const { persistNodes, runUploads } = imageOutputWorkflow({
|
|
142
|
+
recordPath: cropPath,
|
|
143
|
+
readContext: context,
|
|
144
|
+
nodeTitle: (item) => item.title,
|
|
145
|
+
nodePosition: (_nodes, source) => ({
|
|
146
|
+
x: Number(source.position?.x || 0) + Number(source.width || 320) + 80,
|
|
147
|
+
y: Number(source.position?.y || 0)
|
|
148
|
+
}),
|
|
149
|
+
edgeRole: 'image-edit-link',
|
|
150
|
+
edgePrefix: 'crop-link-',
|
|
151
|
+
codePrefix: 'CROP',
|
|
152
|
+
mime: 'image/png',
|
|
153
|
+
summarize: summary
|
|
154
|
+
})
|
|
155
|
+
async function bound(client, profile, operation) {
|
|
156
|
+
const record = await json(cropPath(profile, operation), null)
|
|
157
|
+
if (!record) throw new Error('未找到裁剪 operation')
|
|
158
|
+
if (record.baseUrl !== client.baseUrl || record.owner !== (await identity(client)))
|
|
159
|
+
throw new Error('裁剪 operation 账号或服务不匹配')
|
|
160
|
+
const current = await context(client, record.input)
|
|
161
|
+
if (
|
|
162
|
+
digest(record.binding) !== digest(current.binding) ||
|
|
163
|
+
digest(record.geometry) !== digest(current.geometry)
|
|
164
|
+
)
|
|
165
|
+
throw new Error('裁剪原图/任务/区域已变化,停止恢复')
|
|
166
|
+
return { record, current }
|
|
167
|
+
}
|
|
168
|
+
async function prepareLocal(client, profile, record, current, options, media) {
|
|
169
|
+
const item = record.items[0]
|
|
170
|
+
if (item.local) return
|
|
171
|
+
const tools = await media.mediaTools(options)
|
|
172
|
+
await fs.mkdir(record.directory, { recursive: true })
|
|
173
|
+
const source = path.join(record.directory, 'source.img')
|
|
174
|
+
if (!record.download) {
|
|
175
|
+
await fs.rm(source, { force: true })
|
|
176
|
+
record.download = await media.downloadImage(client, current.url, source)
|
|
177
|
+
await atomic(cropPath(profile, record.operation), record)
|
|
178
|
+
} else if (digest(await hashFile(source)) !== digest(record.download))
|
|
179
|
+
throw new Error('本地原图缓存已改变')
|
|
180
|
+
const output = path.join(record.directory, item.fileName)
|
|
181
|
+
await fs.rm(output, { force: true })
|
|
182
|
+
item.local = await media.cropImage(source, output, record.geometry, tools)
|
|
183
|
+
record.state = 'local_ready'
|
|
184
|
+
await atomic(cropPath(profile, record.operation), record)
|
|
185
|
+
}
|
|
186
|
+
export async function startCrop(
|
|
187
|
+
client,
|
|
188
|
+
profile,
|
|
189
|
+
value,
|
|
190
|
+
operation,
|
|
191
|
+
confirmed = false,
|
|
192
|
+
options = {},
|
|
193
|
+
media = mediaDefault
|
|
194
|
+
) {
|
|
195
|
+
const input = cropInput(value),
|
|
196
|
+
file = cropPath(profile, operation),
|
|
197
|
+
old = await json(file, null)
|
|
198
|
+
if (old) {
|
|
199
|
+
if (digest(old.input) !== digest(input)) throw new Error('原裁剪 operation 不可修改输入')
|
|
200
|
+
return getCrop(client, profile, operation)
|
|
201
|
+
}
|
|
202
|
+
const tools = await media.mediaTools(options),
|
|
203
|
+
current = await context(client, input),
|
|
204
|
+
owner = await identity(client)
|
|
205
|
+
if (!confirmed)
|
|
206
|
+
return {
|
|
207
|
+
operation,
|
|
208
|
+
submitted: false,
|
|
209
|
+
generation_submitted: false,
|
|
210
|
+
library_saved: false,
|
|
211
|
+
source_task_id: current.binding.task_id,
|
|
212
|
+
...current.geometry,
|
|
213
|
+
output_format: 'image/png',
|
|
214
|
+
media_tools: 'available',
|
|
215
|
+
next: '核对原图像素区域并暂停画布编辑后加 --yes;保留原图,新增图片素材及来源线,不入库'
|
|
216
|
+
}
|
|
217
|
+
const title = current.node.data?.title || '图片'
|
|
218
|
+
const record = {
|
|
219
|
+
operation,
|
|
220
|
+
input,
|
|
221
|
+
owner,
|
|
222
|
+
baseUrl: client.baseUrl,
|
|
223
|
+
binding: current.binding,
|
|
224
|
+
geometry: current.geometry,
|
|
225
|
+
directory: path.join(path.dirname(file), safeName(operation) + '-files'),
|
|
226
|
+
state: 'preparing',
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
items: [
|
|
229
|
+
{
|
|
230
|
+
nodeId: 'crop-' + crypto.randomUUID(),
|
|
231
|
+
title: title + '(裁剪)',
|
|
232
|
+
fileName:
|
|
233
|
+
(title.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '_').slice(0, 80) || 'edited-image') +
|
|
234
|
+
'.png',
|
|
235
|
+
state: 'prepared'
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
await atomic(file, record)
|
|
240
|
+
await prepareLocal(client, profile, record, current, tools, media)
|
|
241
|
+
const fresh = (await bound(client, profile, operation)).current
|
|
242
|
+
await persistNodes(client, profile, record, fresh)
|
|
243
|
+
return runUploads(client, profile, record)
|
|
244
|
+
}
|
|
245
|
+
export async function getCrop(client, profile, operation) {
|
|
246
|
+
const { record, current } = await bound(client, profile, operation),
|
|
247
|
+
result = summary(record)
|
|
248
|
+
for (let i = 0; i < record.items.length; i++) {
|
|
249
|
+
const item = record.items[i],
|
|
250
|
+
node = current.doc.nodes.find((n) => n.id === item.nodeId)
|
|
251
|
+
if (
|
|
252
|
+
item.bound &&
|
|
253
|
+
(node?.type !== 'image_upload' ||
|
|
254
|
+
node.data?.extra?.file_ref !== item.response.result.file_ref ||
|
|
255
|
+
!current.doc.edges.some(
|
|
256
|
+
(e) =>
|
|
257
|
+
e.source === record.input.node_id &&
|
|
258
|
+
e.target === item.nodeId &&
|
|
259
|
+
e.data?.role === 'image-edit-link'
|
|
260
|
+
))
|
|
261
|
+
) {
|
|
262
|
+
result.state = 'canvas_changed'
|
|
263
|
+
result.items[i].state = 'canvas_changed'
|
|
264
|
+
result.items[i].canvas_bound = false
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
export async function recoverCrop(
|
|
270
|
+
client,
|
|
271
|
+
profile,
|
|
272
|
+
operation,
|
|
273
|
+
confirmed = false,
|
|
274
|
+
options = {},
|
|
275
|
+
media = mediaDefault
|
|
276
|
+
) {
|
|
277
|
+
if (!confirmed) return getCrop(client, profile, operation)
|
|
278
|
+
const { record, current } = await bound(client, profile, operation)
|
|
279
|
+
if (record.state === 'completed') return getCrop(client, profile, operation)
|
|
280
|
+
await prepareLocal(client, profile, record, current, options, media)
|
|
281
|
+
await persistNodes(client, profile, record, (await bound(client, profile, operation)).current)
|
|
282
|
+
return runUploads(client, profile, record)
|
|
283
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readImageSource } from './image-crop.js'
|
|
2
|
+
import { materialScope } from './materials.js'
|
|
3
|
+
import { requireServiceReady } from './service.js'
|
|
4
|
+
|
|
5
|
+
const text = (value) => String(value ?? '').trim()
|
|
6
|
+
|
|
7
|
+
export function mattingInput(value) {
|
|
8
|
+
if (
|
|
9
|
+
!value ||
|
|
10
|
+
typeof value !== 'object' ||
|
|
11
|
+
Array.isArray(value) ||
|
|
12
|
+
Object.keys(value).some((key) => !['doc_id', 'node_id', 'space', 'team_uid'].includes(key)) ||
|
|
13
|
+
typeof value.doc_id !== 'string' ||
|
|
14
|
+
!text(value.doc_id) ||
|
|
15
|
+
typeof value.node_id !== 'string' ||
|
|
16
|
+
!text(value.node_id)
|
|
17
|
+
)
|
|
18
|
+
throw new Error('抠图预检仅接受 doc_id/node_id/space/team_uid;不支持自定义模型或阈值')
|
|
19
|
+
materialScope(value.space, value.team_uid)
|
|
20
|
+
return { ...value, doc_id: text(value.doc_id), node_id: text(value.node_id) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read-only discovery. Matting is a free product service, but availability and
|
|
24
|
+
// server lookup for unknown submissions is not available. POST /matting performs inference.
|
|
25
|
+
export async function checkMatting(client, value) {
|
|
26
|
+
const input = mattingInput(value)
|
|
27
|
+
const service = await requireServiceReady(client)
|
|
28
|
+
const current = await readImageSource(client, input)
|
|
29
|
+
const account = await client.raw('/auth/me')
|
|
30
|
+
if (!text(account.uid)) throw new Error('抠图预检账号响应缺少 UID')
|
|
31
|
+
const team =
|
|
32
|
+
input.space === 'team'
|
|
33
|
+
? account.teams?.find((item) => text(item.uid) === input.team_uid && item.status === 'active')
|
|
34
|
+
: null
|
|
35
|
+
if (input.space === 'team' && !team) throw new Error('当前账号没有目标团队的有效成员身份')
|
|
36
|
+
const balance = (team || account).oo_balance ?? (team || account).credit_balance
|
|
37
|
+
return {
|
|
38
|
+
status: 'preflight_only',
|
|
39
|
+
preflight: 'passed',
|
|
40
|
+
submission_available: false,
|
|
41
|
+
processing_submitted: false,
|
|
42
|
+
generation_submitted: false,
|
|
43
|
+
canvas_written: false,
|
|
44
|
+
library_saved: false,
|
|
45
|
+
service_status: service.status,
|
|
46
|
+
doc_id: current.doc.id,
|
|
47
|
+
canvas_version: current.doc.version,
|
|
48
|
+
source_node_id: current.node.id,
|
|
49
|
+
source_task_id: current.binding.task_id,
|
|
50
|
+
source: current.source,
|
|
51
|
+
parameters: { model: 'general', ...materialScope(input.space, input.team_uid) },
|
|
52
|
+
wallet: {
|
|
53
|
+
scope: input.space,
|
|
54
|
+
...(team
|
|
55
|
+
? { team_uid: input.team_uid, team_name: String(team.name || ''), shared: true }
|
|
56
|
+
: {}),
|
|
57
|
+
balance_snapshot: typeof balance === 'number' && Number.isFinite(balance) ? balance : null,
|
|
58
|
+
currency: 'OO'
|
|
59
|
+
},
|
|
60
|
+
estimated_credit_cost: null,
|
|
61
|
+
pricing_policy: { mode: 'free', connected_service_verified: false },
|
|
62
|
+
dependencies: { processing: 'server', local_tools_required: false },
|
|
63
|
+
expected_result: {
|
|
64
|
+
node_type: 'image_upload',
|
|
65
|
+
mime_type: 'image/png',
|
|
66
|
+
edge_role: 'matting-link'
|
|
67
|
+
},
|
|
68
|
+
blockers: [
|
|
69
|
+
{
|
|
70
|
+
code: 'MATTING_DEPLOYMENT_UNVERIFIED',
|
|
71
|
+
message: '尚未接入当前部署抠图可用性的只读核验;后端ready不代表免费抠图服务已开放,不能以推理POST探测健康'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: 'MATTING_RECOVERY_UNAVAILABLE',
|
|
75
|
+
message: '未知同步PNG响应没有可靠服务端查询;已知结果引用及本地缓存可通过原operation恢复,不重发推理'
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
],
|
|
79
|
+
deployment_feature_enabled: null,
|
|
80
|
+
next: '仅只读预检;实际处理使用image matting先预览后确认。已知结果引用/缓存可恢复,未知提交不能查回且绝不重发。不安装本机工具、不自动入库。'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import { runMedia, probeMedia, hashFile } from './media.js'
|
|
3
|
+
|
|
4
|
+
export function cropGeometry(size, input) {
|
|
5
|
+
const width = Number(size.width),
|
|
6
|
+
height = Number(size.height)
|
|
7
|
+
if (![width, height].every((n) => Number.isSafeInteger(n) && n > 0) || width * height > 64000000)
|
|
8
|
+
throw new Error('裁剪原图尺寸无效或超过 6400 万像素')
|
|
9
|
+
let rect
|
|
10
|
+
if (input.rect) {
|
|
11
|
+
rect = { ...input.rect }
|
|
12
|
+
if (rect.left + rect.width > width || rect.top + rect.height > height)
|
|
13
|
+
throw new Error('裁剪区域超出原图像素边界')
|
|
14
|
+
} else {
|
|
15
|
+
const [a, b] = input.aspect_ratio
|
|
16
|
+
.replace(/:/g, ':')
|
|
17
|
+
.split(/[:/xX*]/)
|
|
18
|
+
.map(Number)
|
|
19
|
+
const ratio = a / b
|
|
20
|
+
let frameWidth = width,
|
|
21
|
+
frameHeight = width / ratio
|
|
22
|
+
if (frameHeight > height) {
|
|
23
|
+
frameHeight = height
|
|
24
|
+
frameWidth = height * ratio
|
|
25
|
+
}
|
|
26
|
+
const left = Math.min(width - 1, Math.round((width - frameWidth) / 2))
|
|
27
|
+
const top = Math.min(height - 1, Math.round((height - frameHeight) / 2))
|
|
28
|
+
rect = {
|
|
29
|
+
left,
|
|
30
|
+
top,
|
|
31
|
+
width: Math.max(1, Math.min(width - left, Math.round(frameWidth))),
|
|
32
|
+
height: Math.max(1, Math.min(height - top, Math.round(frameHeight)))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const scale = Math.min(
|
|
36
|
+
1,
|
|
37
|
+
4096 / Math.max(rect.width, rect.height),
|
|
38
|
+
Math.sqrt(16000000 / (rect.width * rect.height))
|
|
39
|
+
)
|
|
40
|
+
return {
|
|
41
|
+
source: { width, height },
|
|
42
|
+
rect,
|
|
43
|
+
output: {
|
|
44
|
+
width: Math.max(1, Math.floor(rect.width * scale)),
|
|
45
|
+
height: Math.max(1, Math.floor(rect.height * scale))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function cropImage(source, target, geometry, tools) {
|
|
51
|
+
const image = await probeMedia(source, tools)
|
|
52
|
+
if (!['png', 'mjpeg'].includes(image.codec) || image.rotation || image.orientation > 1)
|
|
53
|
+
throw new Error('当前裁剪仅接入静态 PNG/JPEG;旋转/镜像方向或其他格式待验证')
|
|
54
|
+
if (image.width !== geometry.source.width || image.height !== geometry.source.height)
|
|
55
|
+
throw new Error('实际原图尺寸与任务元数据不一致;停止,不按缩略图尺寸裁剪')
|
|
56
|
+
const { rect, output } = geometry
|
|
57
|
+
await runMedia(tools.ffmpeg, [
|
|
58
|
+
'-nostdin',
|
|
59
|
+
'-v',
|
|
60
|
+
'error',
|
|
61
|
+
'-protocol_whitelist',
|
|
62
|
+
'file,pipe',
|
|
63
|
+
'-i',
|
|
64
|
+
source,
|
|
65
|
+
'-map',
|
|
66
|
+
'0:v:0',
|
|
67
|
+
'-vf',
|
|
68
|
+
`crop=${rect.width}:${rect.height}:${rect.left}:${rect.top}:exact=1,scale=${output.width}:${output.height}:flags=lanczos,format=rgba`,
|
|
69
|
+
'-frames:v',
|
|
70
|
+
'1',
|
|
71
|
+
'-c:v',
|
|
72
|
+
'png',
|
|
73
|
+
'-n',
|
|
74
|
+
target
|
|
75
|
+
])
|
|
76
|
+
const handle = await fs.open(target, 'r')
|
|
77
|
+
try {
|
|
78
|
+
const signature = Buffer.alloc(8)
|
|
79
|
+
await handle.read(signature, 0, 8, 0)
|
|
80
|
+
if (!signature.equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
81
|
+
throw new Error('裁剪导出不是有效 PNG')
|
|
82
|
+
} finally {
|
|
83
|
+
await handle.close()
|
|
84
|
+
}
|
|
85
|
+
const actual = await probeMedia(target, tools)
|
|
86
|
+
if (actual.width !== output.width || actual.height !== output.height)
|
|
87
|
+
throw new Error('裁剪导出尺寸不一致')
|
|
88
|
+
return { width: actual.width, height: actual.height, ...(await hashFile(target)) }
|
|
89
|
+
}
|