evomeme-dev 0.1.7
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/README.md +247 -0
- package/bin/evomeme +28 -0
- package/dist/canvas-public/app.js +5857 -0
- package/dist/canvas-public/cycle-detect.js +166 -0
- package/dist/canvas-public/index.html +395 -0
- package/dist/canvas-public/sequence-sheet.js +59 -0
- package/dist/canvas-public/smart-split.js +185 -0
- package/dist/canvas-public/styles.css +2276 -0
- package/dist/canvas-public/vendor/painterro.min.js +3 -0
- package/dist/index.js +25586 -0
- package/package.json +69 -0
- package/skills/evomeme-cloud-fs/SKILL.md +176 -0
- package/skills/evomeme-cloud-fs/agents/openai.yaml +7 -0
- package/skills/evomeme-cover/SKILL.md +132 -0
- package/skills/evomeme-cover/agents/openai.yaml +7 -0
- package/skills/evomeme-game-maker/SKILL.md +214 -0
- package/skills/evomeme-game-maker/agents/openai.yaml +4 -0
- package/skills/evomeme-login/SKILL.md +92 -0
- package/skills/evomeme-login/agents/openai.yaml +7 -0
- package/skills/evomeme-native-bridge-sdk/SKILL.md +286 -0
- package/skills/evomeme-native-bridge-sdk/agents/openai.yaml +7 -0
- package/skills/evomeme-tos-upload/SKILL.md +46 -0
- package/skills/evomeme-tos-upload/agents/openai.yaml +4 -0
- package/skills/game-art/SKILL.md +121 -0
- package/skills/game-art/agents/openai.yaml +7 -0
- package/skills/game-asset-image-generator/SKILL.md +104 -0
- package/skills/game-asset-image-generator/agents/openai.yaml +7 -0
- package/skills/game-asset-image-generator/references/background.md +32 -0
- package/skills/game-asset-image-generator/references/static-game-assets.md +38 -0
- package/skills/game-audio/SKILL.md +82 -0
- package/skills/game-audio/agents/openai.yaml +4 -0
- package/skills/game-audio/references/bgm-cdn.md +127 -0
- package/skills/game-audio/references/sfx-cdn.md +77 -0
- package/skills/game-demo/SKILL.md +103 -0
- package/skills/game-demo/agents/openai.yaml +7 -0
- package/skills/game-maker/SKILL.md +111 -0
- package/skills/game-maker/agents/openai.yaml +4 -0
- package/skills/game-test/SKILL.md +58 -0
- package/skills/game-test/agents/openai.yaml +7 -0
- package/skills/game-test/references/checklists/playtest-qa.md +12 -0
- package/skills/game-ui/SKILL.md +169 -0
- package/skills/game-ui/agents/openai.yaml +4 -0
- package/skills/game-ui/references/checklists/game-ui-quality.md +14 -0
- package/skills/game-ui/references/checklists/hud-readability.md +12 -0
- package/skills/game-ui/references/checklists/mobile-input.md +12 -0
- package/skills/game-ui/references/checklists/responsive-ui-fit.md +12 -0
- package/skills/game-ui/references/ui-patterns.md +140 -0
- package/skills/game-ui/references/ui-prompt-templates.md +63 -0
- package/skills/phaser-2d-game/SKILL.md +127 -0
- package/skills/phaser-2d-game/agents/openai.yaml +7 -0
- package/skills/phaser-2d-game/references/engine-performance.md +64 -0
- package/skills/phaser-2d-game/references/engine-physics.md +139 -0
- package/skills/phaser-2d-game/references/engine-runtime.md +129 -0
- package/skills/phaser-2d-game/references/engine-spritesheets.md +99 -0
- package/skills/phaser-2d-game/references/engine-tilemaps.md +80 -0
- package/skills/phaser-2d-game/references/file-structure.md +94 -0
- package/skills/phaser-2d-graphics-builder/SKILL.md +68 -0
- package/skills/phaser-2d-graphics-builder/agents/openai.yaml +7 -0
- package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-1.jpeg +0 -0
- package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-2.jpeg +0 -0
- package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/compose-quality.md +10 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/performance-safe-visual-detail.md +9 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/premium-game-quality-gate.md +15 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/sprite-quality.md +10 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/technical-art-quality.md +9 -0
- package/skills/phaser-2d-graphics-builder/references/checklists/visual-scorecard.md +9 -0
- package/skills/phaser-2d-graphics-builder/references/compose-recipes.md +71 -0
- package/skills/phaser-2d-graphics-builder/references/graphics-prompt-templates.md +33 -0
- package/skills/phaser-2d-graphics-builder/references/implementation-blueprint.md +82 -0
- package/skills/phaser-2d-graphics-builder/references/sprite-recipes.md +90 -0
- package/skills/phaser-2d-graphics-builder/references/technical-art.md +67 -0
- package/skills/phaser-2d-graphics-builder/references/vfx-recipes.md +67 -0
- package/skills/phaser-2d-graphics-builder/references/visual-scorecard.md +129 -0
- package/skills/threejs-3d-game/SKILL.md +113 -0
- package/skills/threejs-3d-game/agents/openai.yaml +7 -0
- package/skills/threejs-3d-game/references/engine-asset-pipeline.md +116 -0
- package/skills/threejs-3d-game/references/engine-audio.md +70 -0
- package/skills/threejs-3d-game/references/engine-input-camera.md +84 -0
- package/skills/threejs-3d-game/references/engine-performance.md +96 -0
- package/skills/threejs-3d-game/references/engine-physics.md +142 -0
- package/skills/threejs-3d-game/references/engine-runtime.md +199 -0
- package/skills/threejs-3d-game/references/file-structure.md +353 -0
- package/skills/threejs-aaa-graphics-builder/SKILL.md +62 -0
- package/skills/threejs-aaa-graphics-builder/agents/openai.yaml +7 -0
- package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-1.jpg +0 -0
- package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-2.jpg +0 -0
- package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/aaa-game-quality-gate.md +23 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/aaa-visual-scorecard.md +19 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/material-lighting-quality.md +14 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/performance-safe-visual-detail.md +14 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/procedural-model-quality.md +15 -0
- package/skills/threejs-aaa-graphics-builder/references/checklists/technical-art-quality.md +17 -0
- package/skills/threejs-aaa-graphics-builder/references/graphics-prompt-templates.md +236 -0
- package/skills/threejs-aaa-graphics-builder/references/implementation-blueprint.md +161 -0
- package/skills/threejs-aaa-graphics-builder/references/model-recipes.md +146 -0
- package/skills/threejs-aaa-graphics-builder/references/render-recipes.md +90 -0
- package/skills/threejs-aaa-graphics-builder/references/shader-cookbook.md +292 -0
- package/skills/threejs-aaa-graphics-builder/references/technical-art.md +167 -0
- package/skills/threejs-aaa-graphics-builder/references/visual-scorecard.md +147 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器端:缩略灰度 + z-score + 点积,从前向后找最早高相似帧。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function zscoreInPlace(vec) {
|
|
6
|
+
let sum = 0
|
|
7
|
+
for (let i = 0; i < vec.length; i++) sum += vec[i]
|
|
8
|
+
const mean = sum / vec.length
|
|
9
|
+
let varSum = 0
|
|
10
|
+
for (let i = 0; i < vec.length; i++) {
|
|
11
|
+
const d = vec[i] - mean
|
|
12
|
+
varSum += d * d
|
|
13
|
+
}
|
|
14
|
+
const std = Math.sqrt(varSum / vec.length) + 1e-8
|
|
15
|
+
for (let i = 0; i < vec.length; i++) vec[i] = (vec[i] - mean) / std
|
|
16
|
+
return vec
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function featureFromImageData(imageData) {
|
|
20
|
+
const { data, width, height } = imageData
|
|
21
|
+
const n = width * height
|
|
22
|
+
const vec = new Float32Array(n)
|
|
23
|
+
for (let i = 0, p = 0; i < n; i++, p += 4) {
|
|
24
|
+
// ITU-R BT.601 亮度
|
|
25
|
+
vec[i] = data[p] * 0.299 + data[p + 1] * 0.587 + data[p + 2] * 0.114
|
|
26
|
+
}
|
|
27
|
+
return zscoreInPlace(vec)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nccScore(a, b) {
|
|
31
|
+
const n = Math.min(a.length, b.length)
|
|
32
|
+
if (!n) return 0
|
|
33
|
+
let sum = 0
|
|
34
|
+
for (let i = 0; i < n; i++) sum += a[i] * b[i]
|
|
35
|
+
return sum / n
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function waitEvent(target, event, { signal } = {}) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
cleanup()
|
|
46
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
47
|
+
}
|
|
48
|
+
const onOk = () => {
|
|
49
|
+
cleanup()
|
|
50
|
+
resolve()
|
|
51
|
+
}
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
target.removeEventListener(event, onOk)
|
|
54
|
+
signal?.removeEventListener('abort', onAbort)
|
|
55
|
+
}
|
|
56
|
+
target.addEventListener(event, onOk, { once: true })
|
|
57
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function seekVideo(video, time, signal) {
|
|
62
|
+
const duration = Number(video.duration)
|
|
63
|
+
const maxT = Number.isFinite(duration) && duration > 0 ? Math.max(0, duration - 1e-4) : time
|
|
64
|
+
const t = Math.min(Math.max(0, time), maxT)
|
|
65
|
+
if (Math.abs((video.currentTime || 0) - t) < 1e-4) return
|
|
66
|
+
const p = waitEvent(video, 'seeked', { signal })
|
|
67
|
+
video.currentTime = t
|
|
68
|
+
await p
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function drawFeature(video, canvas, ctx, thumbWidth) {
|
|
72
|
+
const vw = video.videoWidth || 1
|
|
73
|
+
const vh = video.videoHeight || 1
|
|
74
|
+
const w = Math.max(8, Math.round(thumbWidth))
|
|
75
|
+
const h = Math.max(8, Math.round((vh / vw) * w))
|
|
76
|
+
if (canvas.width !== w) canvas.width = w
|
|
77
|
+
if (canvas.height !== h) canvas.height = h
|
|
78
|
+
ctx.drawImage(video, 0, 0, w, h)
|
|
79
|
+
return featureFromImageData(ctx.getImageData(0, 0, w, h))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} videoUrl
|
|
84
|
+
* @param {{
|
|
85
|
+
* threshold?: number,
|
|
86
|
+
* minGapSec?: number,
|
|
87
|
+
* thumbWidth?: number,
|
|
88
|
+
* onProgress?: (info: object) => void,
|
|
89
|
+
* signal?: AbortSignal,
|
|
90
|
+
* }} [options]
|
|
91
|
+
* @returns {Promise<{ start: number, end: number, score: number, fps: number, scanned: number }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function detectFirstCycle(videoUrl, options = {}) {
|
|
94
|
+
const threshold = Math.min(0.999, Math.max(0.5, Number(options.threshold) || 0.98))
|
|
95
|
+
const minGapSec = Math.max(0.05, Number(options.minGapSec) || 0.4)
|
|
96
|
+
const thumbWidth = Math.max(16, Math.round(Number(options.thumbWidth) || 96))
|
|
97
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null
|
|
98
|
+
const signal = options.signal
|
|
99
|
+
|
|
100
|
+
const video = document.createElement('video')
|
|
101
|
+
video.muted = true
|
|
102
|
+
video.playsInline = true
|
|
103
|
+
video.preload = 'auto'
|
|
104
|
+
video.crossOrigin = 'anonymous'
|
|
105
|
+
|
|
106
|
+
const canvas = document.createElement('canvas')
|
|
107
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
|
108
|
+
if (!ctx) throw new Error('无法创建 canvas')
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const loaded = waitEvent(video, 'loadedmetadata', { signal })
|
|
112
|
+
video.src = videoUrl
|
|
113
|
+
video.load()
|
|
114
|
+
await loaded
|
|
115
|
+
if (!video.videoWidth || !Number.isFinite(video.duration) || video.duration <= 0) {
|
|
116
|
+
throw new Error('无法读取视频元数据')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const duration = video.duration
|
|
120
|
+
const step = 1 / 24
|
|
121
|
+
const minT = Math.min(duration - step, minGapSec)
|
|
122
|
+
if (!(minT > step)) {
|
|
123
|
+
throw new Error('视频太短,无法提取周期')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await seekVideo(video, 0, signal)
|
|
127
|
+
// 部分浏览器 seek(0) 后需一帧稳定
|
|
128
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
|
129
|
+
const ref = drawFeature(video, canvas, ctx, thumbWidth)
|
|
130
|
+
|
|
131
|
+
let scanned = 0
|
|
132
|
+
for (let t = minT; t < duration - 1e-4; t += step) {
|
|
133
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
134
|
+
await seekVideo(video, t, signal)
|
|
135
|
+
const feat = drawFeature(video, canvas, ctx, thumbWidth)
|
|
136
|
+
const score = nccScore(ref, feat)
|
|
137
|
+
scanned += 1
|
|
138
|
+
onProgress?.({
|
|
139
|
+
time: t,
|
|
140
|
+
score,
|
|
141
|
+
scanned,
|
|
142
|
+
duration,
|
|
143
|
+
threshold,
|
|
144
|
+
})
|
|
145
|
+
if (score >= threshold) {
|
|
146
|
+
return {
|
|
147
|
+
start: 0,
|
|
148
|
+
end: Math.min(t, duration),
|
|
149
|
+
score,
|
|
150
|
+
fps: 24,
|
|
151
|
+
scanned,
|
|
152
|
+
threshold,
|
|
153
|
+
minGapSec,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new Error(
|
|
159
|
+
`未找到相似度 ≥ ${(threshold * 100).toFixed(0)}% 的周期终点(已扫 ${scanned} 帧,最短间隔 ${minGapSec}s)`,
|
|
160
|
+
)
|
|
161
|
+
} finally {
|
|
162
|
+
video.pause()
|
|
163
|
+
video.removeAttribute('src')
|
|
164
|
+
video.load()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Image Canvas</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="welcome" id="welcome">
|
|
17
|
+
<div class="welcome-card">
|
|
18
|
+
<p class="welcome-brand">Image Canvas</p>
|
|
19
|
+
<h1>打开工程目录</h1>
|
|
20
|
+
<p class="welcome-desc">
|
|
21
|
+
选择一个本地文件夹作为工程。画布、上传与生成结果会保存在该目录下的
|
|
22
|
+
<code>.image-canvas/</code> 中。
|
|
23
|
+
</p>
|
|
24
|
+
<button type="button" class="primary-btn" id="welcome-open-btn">打开工程…</button>
|
|
25
|
+
<div class="welcome-recent" id="welcome-recent" hidden>
|
|
26
|
+
<h2>最近打开</h2>
|
|
27
|
+
<ul id="welcome-recent-list"></ul>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="welcome-error" id="welcome-error" hidden></p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="app" id="app" hidden>
|
|
34
|
+
<div class="canvas-pane">
|
|
35
|
+
<aside class="inspector" id="inspector" hidden>
|
|
36
|
+
<header class="inspector-header">
|
|
37
|
+
<h2 id="inspector-title">属性</h2>
|
|
38
|
+
<button type="button" class="ghost-btn" id="inspector-close" title="取消选中">×</button>
|
|
39
|
+
</header>
|
|
40
|
+
<div class="inspector-body" id="inspector-body"></div>
|
|
41
|
+
</aside>
|
|
42
|
+
<div class="canvas-main">
|
|
43
|
+
<div class="canvas-shell">
|
|
44
|
+
<div class="canvas-toolbar">
|
|
45
|
+
<div class="project-bar">
|
|
46
|
+
<span class="project-name" id="project-name" title="">未打开工程</span>
|
|
47
|
+
<button type="button" class="ghost-btn" id="switch-project-btn">切换工程</button>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="toolbar-actions" id="toolbar-actions">
|
|
50
|
+
<button type="button" class="tool-btn" id="tool-add-assets">添加素材</button>
|
|
51
|
+
<button type="button" class="tool-btn" id="tool-image-gen" title="拖到画布指定位置创建图片生成元素">图片生成</button>
|
|
52
|
+
<button type="button" class="tool-btn" id="tool-video-gen" title="拖到画布指定位置创建视频生成元素">视频生成</button>
|
|
53
|
+
<button type="button" class="ghost-btn" id="reset-view">重置视图</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="canvas-viewport" id="viewport">
|
|
57
|
+
<div class="canvas-world" id="world"></div>
|
|
58
|
+
<div class="marquee" id="marquee" hidden></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="banner-error" id="banner-error" hidden></div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="ctx-menu" id="ctx-menu" hidden>
|
|
66
|
+
<button type="button" data-action="copy">复制</button>
|
|
67
|
+
<button type="button" data-action="paste" hidden>粘贴</button>
|
|
68
|
+
<button type="button" data-action="cutout" hidden>智能抠图</button>
|
|
69
|
+
<button type="button" data-action="interactive-edit" hidden>交互编辑</button>
|
|
70
|
+
<button type="button" data-action="layer-decomposition" hidden>大模型图层分离</button>
|
|
71
|
+
<button type="button" data-action="video-trim" hidden>裁剪视频</button>
|
|
72
|
+
<button type="button" data-action="video-cycle" hidden>智能周期提取</button>
|
|
73
|
+
<button type="button" data-action="video-to-sequence" hidden>序列帧化</button>
|
|
74
|
+
<button type="button" data-action="smart-split">智能切分</button>
|
|
75
|
+
<button type="button" data-action="manual-split">手动切分</button>
|
|
76
|
+
<button type="button" data-action="edit-sequence" hidden>在左侧编辑序列</button>
|
|
77
|
+
<button type="button" data-action="sequence-to-sheet" hidden>拼成单图</button>
|
|
78
|
+
<button type="button" data-action="ungroup-sequence" hidden>拆散为单图</button>
|
|
79
|
+
<button type="button" data-action="combine-sequence" hidden>组合为序列帧</button>
|
|
80
|
+
<button type="button" data-action="delete" class="ctx-danger">删除</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="modal-backdrop modal-backdrop-dark" id="interactive-edit-modal" hidden>
|
|
84
|
+
<div class="modal-dialog modal-dialog-interactive-edit" role="dialog" aria-modal="true" aria-labelledby="interactive-edit-title">
|
|
85
|
+
<div class="seq-panel-head">
|
|
86
|
+
<strong id="interactive-edit-title">交互编辑</strong>
|
|
87
|
+
<button type="button" class="ghost-btn" id="interactive-edit-close">关闭</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="interactive-editor-host" id="interactive-editor-host"></div>
|
|
90
|
+
<div class="interactive-edit-compose">
|
|
91
|
+
<label class="layer-decomposition-prompt" for="interactive-edit-prompt">
|
|
92
|
+
编辑要求
|
|
93
|
+
<textarea
|
|
94
|
+
id="interactive-edit-prompt"
|
|
95
|
+
rows="3"
|
|
96
|
+
placeholder="例如:将框选区域替换为木质宝箱,并保持其他内容不变"
|
|
97
|
+
></textarea>
|
|
98
|
+
</label>
|
|
99
|
+
<p class="modal-hint">画面标记和文字会合并到参考图中,并使用 Pro 模型生成。</p>
|
|
100
|
+
<p class="modal-hint" id="interactive-edit-status" hidden></p>
|
|
101
|
+
<div class="modal-actions">
|
|
102
|
+
<button type="button" class="ghost-btn" id="interactive-edit-cancel">取消</button>
|
|
103
|
+
<button type="button" class="ghost-btn" id="interactive-edit-save">保存到画布</button>
|
|
104
|
+
<button type="button" class="tool-btn" id="interactive-edit-confirm">生成</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="modal-backdrop modal-backdrop-dark" id="layer-decomposition-modal" hidden>
|
|
111
|
+
<div class="modal-dialog modal-dialog-layer-decomposition" role="dialog" aria-modal="true" aria-labelledby="layer-decomposition-title">
|
|
112
|
+
<div class="seq-panel-head">
|
|
113
|
+
<strong id="layer-decomposition-title">大模型图层分离</strong>
|
|
114
|
+
<button type="button" class="ghost-btn" id="layer-decomposition-close">关闭</button>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="layer-decomposition-preview">
|
|
117
|
+
<img id="layer-decomposition-image" alt="待分离图片预览" />
|
|
118
|
+
</div>
|
|
119
|
+
<label class="layer-decomposition-prompt" for="layer-decomposition-prompt">
|
|
120
|
+
拆分要求
|
|
121
|
+
<textarea
|
|
122
|
+
id="layer-decomposition-prompt"
|
|
123
|
+
rows="4"
|
|
124
|
+
placeholder="例如:将人物、背景、文字分别拆分为可独立编辑的图层"
|
|
125
|
+
></textarea>
|
|
126
|
+
</label>
|
|
127
|
+
<p class="modal-hint">模型会输出一张底图和最多 16 个透明 PNG 图层。</p>
|
|
128
|
+
<p class="modal-hint" id="layer-decomposition-status" hidden></p>
|
|
129
|
+
<div class="modal-actions">
|
|
130
|
+
<button type="button" class="ghost-btn" id="layer-decomposition-cancel">取消</button>
|
|
131
|
+
<button type="button" class="tool-btn" id="layer-decomposition-confirm">开始分离</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="modal-backdrop modal-backdrop-dark" id="video-cycle-modal" hidden>
|
|
137
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="video-cycle-title">
|
|
138
|
+
<div class="seq-panel-head">
|
|
139
|
+
<strong id="video-cycle-title">智能周期提取</strong>
|
|
140
|
+
<button type="button" class="ghost-btn" id="video-cycle-close">关闭</button>
|
|
141
|
+
</div>
|
|
142
|
+
<p class="modal-hint">
|
|
143
|
+
以首帧为参考,从前向后寻找最早相似度达标的一帧并立即停止,再裁切为首个周期视频。
|
|
144
|
+
</p>
|
|
145
|
+
<label class="seq-field frame-extract-field">
|
|
146
|
+
相似度阈值
|
|
147
|
+
<input
|
|
148
|
+
type="number"
|
|
149
|
+
id="video-cycle-threshold"
|
|
150
|
+
min="50"
|
|
151
|
+
max="99.9"
|
|
152
|
+
step="0.1"
|
|
153
|
+
value="98"
|
|
154
|
+
/>
|
|
155
|
+
<span class="frame-extract-range">%</span>
|
|
156
|
+
</label>
|
|
157
|
+
<label class="seq-field frame-extract-field">
|
|
158
|
+
最短周期
|
|
159
|
+
<input
|
|
160
|
+
type="number"
|
|
161
|
+
id="video-cycle-min-gap"
|
|
162
|
+
min="0.1"
|
|
163
|
+
max="10"
|
|
164
|
+
step="0.1"
|
|
165
|
+
value="0.4"
|
|
166
|
+
/>
|
|
167
|
+
<span class="frame-extract-range">秒(避免开头相邻帧误命中)</span>
|
|
168
|
+
</label>
|
|
169
|
+
<p class="modal-hint" id="video-cycle-status" hidden></p>
|
|
170
|
+
<div class="modal-actions">
|
|
171
|
+
<button type="button" class="ghost-btn" id="video-cycle-cancel">取消</button>
|
|
172
|
+
<button type="button" class="tool-btn" id="video-cycle-confirm">开始提取</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="modal-backdrop modal-backdrop-dark" id="smart-split-modal" hidden>
|
|
178
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="smart-split-title">
|
|
179
|
+
<div class="seq-panel-head">
|
|
180
|
+
<strong id="smart-split-title">智能切分</strong>
|
|
181
|
+
<button type="button" class="ghost-btn" id="smart-split-close">关闭</button>
|
|
182
|
+
</div>
|
|
183
|
+
<p class="modal-hint">
|
|
184
|
+
切分结果统一为最大主体的宽度和高度。选择主体在扩展区域中的固定基准点;主体不会缩放。
|
|
185
|
+
</p>
|
|
186
|
+
<div class="split-anchor-grid" id="smart-split-anchor-grid" role="radiogroup" aria-label="扩展基准点">
|
|
187
|
+
<button type="button" class="split-anchor-btn" data-anchor="top-left" role="radio" aria-checked="false"><span></span>左上</button>
|
|
188
|
+
<button type="button" class="split-anchor-btn" data-anchor="top" role="radio" aria-checked="false"><span></span>上</button>
|
|
189
|
+
<button type="button" class="split-anchor-btn" data-anchor="top-right" role="radio" aria-checked="false"><span></span>右上</button>
|
|
190
|
+
<button type="button" class="split-anchor-btn" data-anchor="left" role="radio" aria-checked="false"><span></span>左</button>
|
|
191
|
+
<button type="button" class="split-anchor-btn is-selected" data-anchor="center" role="radio" aria-checked="true"><span></span>中心</button>
|
|
192
|
+
<button type="button" class="split-anchor-btn" data-anchor="right" role="radio" aria-checked="false"><span></span>右</button>
|
|
193
|
+
<button type="button" class="split-anchor-btn" data-anchor="bottom-left" role="radio" aria-checked="false"><span></span>左下</button>
|
|
194
|
+
<button type="button" class="split-anchor-btn" data-anchor="bottom" role="radio" aria-checked="false"><span></span>下</button>
|
|
195
|
+
<button type="button" class="split-anchor-btn" data-anchor="bottom-right" role="radio" aria-checked="false"><span></span>右下</button>
|
|
196
|
+
</div>
|
|
197
|
+
<p class="modal-hint" id="smart-split-status" hidden></p>
|
|
198
|
+
<div class="modal-actions">
|
|
199
|
+
<button type="button" class="ghost-btn" id="smart-split-cancel">取消</button>
|
|
200
|
+
<button type="button" class="tool-btn" id="smart-split-confirm">开始切分</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="modal-backdrop modal-backdrop-dark" id="manual-split-modal" hidden>
|
|
206
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="manual-split-title">
|
|
207
|
+
<div class="seq-panel-head">
|
|
208
|
+
<strong id="manual-split-title">手动切分</strong>
|
|
209
|
+
<button type="button" class="ghost-btn" id="manual-split-close">关闭</button>
|
|
210
|
+
</div>
|
|
211
|
+
<p class="modal-hint">
|
|
212
|
+
按行列将原图完整均分,从左到右、从上到下生成图片,不缩放原图。
|
|
213
|
+
</p>
|
|
214
|
+
<p class="modal-hint" id="manual-split-info"></p>
|
|
215
|
+
<div class="seq-sheet-fields">
|
|
216
|
+
<label class="seq-field frame-extract-field">
|
|
217
|
+
行数
|
|
218
|
+
<input type="number" id="manual-split-rows" min="1" max="64" step="1" value="2" />
|
|
219
|
+
</label>
|
|
220
|
+
<label class="seq-field frame-extract-field">
|
|
221
|
+
列数
|
|
222
|
+
<input type="number" id="manual-split-cols" min="1" max="64" step="1" value="2" />
|
|
223
|
+
</label>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="seq-sheet-preview" id="manual-split-preview" aria-hidden="true"></div>
|
|
226
|
+
<p class="modal-hint" id="manual-split-status" hidden></p>
|
|
227
|
+
<div class="modal-actions">
|
|
228
|
+
<button type="button" class="ghost-btn" id="manual-split-cancel">取消</button>
|
|
229
|
+
<button type="button" class="tool-btn" id="manual-split-confirm">开始切分</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="modal-backdrop modal-backdrop-dark" id="frame-extract-modal" hidden>
|
|
235
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="frame-extract-title">
|
|
236
|
+
<div class="seq-panel-head">
|
|
237
|
+
<strong id="frame-extract-title">序列帧化</strong>
|
|
238
|
+
<button type="button" class="ghost-btn" id="frame-extract-close">关闭</button>
|
|
239
|
+
</div>
|
|
240
|
+
<p class="modal-hint">
|
|
241
|
+
按总帧数计算步长均匀取帧:首帧与尾帧必含,中间为 1 + n×⌊T/(C-1)⌋。
|
|
242
|
+
</p>
|
|
243
|
+
<label class="seq-field frame-extract-field">
|
|
244
|
+
序列帧长度
|
|
245
|
+
<input type="number" id="frame-extract-count" min="2" max="30" step="1" value="10" />
|
|
246
|
+
<span class="frame-extract-range">2 – 30</span>
|
|
247
|
+
</label>
|
|
248
|
+
<p class="modal-hint" id="frame-extract-status" hidden></p>
|
|
249
|
+
<div class="modal-actions">
|
|
250
|
+
<button type="button" class="ghost-btn" id="frame-extract-cancel">取消</button>
|
|
251
|
+
<button type="button" class="tool-btn" id="frame-extract-confirm">开始抽帧</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="modal-backdrop modal-backdrop-dark" id="video-trim-modal" hidden>
|
|
257
|
+
<div class="modal-dialog modal-dialog-wide" role="dialog" aria-modal="true" aria-labelledby="video-trim-title">
|
|
258
|
+
<div class="seq-panel-head">
|
|
259
|
+
<strong id="video-trim-title">裁剪视频</strong>
|
|
260
|
+
<button type="button" class="ghost-btn" id="video-trim-close">关闭</button>
|
|
261
|
+
</div>
|
|
262
|
+
<p class="modal-hint">一条时间轴:拖白点定位画面,拖青绿柄设入/出点;滚轮可微调(Shift=1ms)。</p>
|
|
263
|
+
<div class="trim-player">
|
|
264
|
+
<div class="trim-stage">
|
|
265
|
+
<video id="video-trim-preview" muted playsinline preload="auto"></video>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="trim-bar">
|
|
268
|
+
<button type="button" class="trim-play-btn" id="video-trim-play" title="播放/暂停">▶</button>
|
|
269
|
+
<span class="trim-clock">
|
|
270
|
+
<strong id="video-trim-now-label">0:00.000</strong>
|
|
271
|
+
<span>/</span>
|
|
272
|
+
<span id="video-trim-total-label">0:00.000</span>
|
|
273
|
+
</span>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="trim-timeline" id="video-trim-timeline">
|
|
276
|
+
<div class="trim-track" id="video-trim-track">
|
|
277
|
+
<div class="trim-range" id="video-trim-range"></div>
|
|
278
|
+
<input
|
|
279
|
+
type="range"
|
|
280
|
+
class="trim-scrub-input"
|
|
281
|
+
id="video-trim-scrub"
|
|
282
|
+
min="0"
|
|
283
|
+
max="0"
|
|
284
|
+
step="0.001"
|
|
285
|
+
value="0"
|
|
286
|
+
aria-label="播放进度"
|
|
287
|
+
/>
|
|
288
|
+
<div
|
|
289
|
+
class="trim-handle trim-handle-start"
|
|
290
|
+
id="video-trim-handle-start"
|
|
291
|
+
title="入点"
|
|
292
|
+
role="slider"
|
|
293
|
+
tabindex="0"
|
|
294
|
+
></div>
|
|
295
|
+
<div
|
|
296
|
+
class="trim-handle trim-handle-end"
|
|
297
|
+
id="video-trim-handle-end"
|
|
298
|
+
title="出点"
|
|
299
|
+
role="slider"
|
|
300
|
+
tabindex="0"
|
|
301
|
+
></div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="trim-times">
|
|
305
|
+
<span>入点 <strong id="video-trim-start-label">0:00.000</strong></span>
|
|
306
|
+
<span>选区 <strong id="video-trim-dur-label">0:00.000</strong></span>
|
|
307
|
+
<span>出点 <strong id="video-trim-end-label">0:00.000</strong></span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<p class="modal-hint" id="video-trim-status" hidden></p>
|
|
311
|
+
<div class="modal-actions">
|
|
312
|
+
<button type="button" class="ghost-btn" id="video-trim-cancel">取消</button>
|
|
313
|
+
<button type="button" class="tool-btn" id="video-trim-confirm">裁剪并生成</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div class="modal-backdrop modal-backdrop-dark" id="seq-sheet-modal" hidden>
|
|
319
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="seq-sheet-title">
|
|
320
|
+
<div class="seq-panel-head">
|
|
321
|
+
<strong id="seq-sheet-title">拼成单图</strong>
|
|
322
|
+
<button type="button" class="ghost-btn" id="seq-sheet-close">关闭</button>
|
|
323
|
+
</div>
|
|
324
|
+
<p class="modal-hint">
|
|
325
|
+
按行列排布序列帧:先从左到右填满一行,再换到下一行;多出的格子保持透明。
|
|
326
|
+
</p>
|
|
327
|
+
<p class="modal-hint" id="seq-sheet-frame-info">共 0 帧</p>
|
|
328
|
+
<div class="seq-sheet-fields">
|
|
329
|
+
<label class="seq-field frame-extract-field">
|
|
330
|
+
行数
|
|
331
|
+
<input type="number" id="seq-sheet-rows" min="1" max="64" step="1" value="1" />
|
|
332
|
+
</label>
|
|
333
|
+
<label class="seq-field frame-extract-field">
|
|
334
|
+
列数
|
|
335
|
+
<input type="number" id="seq-sheet-cols" min="1" max="64" step="1" value="1" />
|
|
336
|
+
</label>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="seq-sheet-preview" id="seq-sheet-preview" aria-hidden="true"></div>
|
|
339
|
+
<p class="modal-hint" id="seq-sheet-status" hidden></p>
|
|
340
|
+
<div class="modal-actions">
|
|
341
|
+
<button type="button" class="ghost-btn" id="seq-sheet-cancel">取消</button>
|
|
342
|
+
<button type="button" class="tool-btn" id="seq-sheet-confirm">生成单图</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="modal-backdrop" id="seq-modal" hidden>
|
|
348
|
+
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="seq-panel-title">
|
|
349
|
+
<div class="seq-panel-head">
|
|
350
|
+
<strong id="seq-panel-title">编辑序列</strong>
|
|
351
|
+
<button type="button" class="ghost-btn" id="seq-panel-close">关闭</button>
|
|
352
|
+
</div>
|
|
353
|
+
<p class="modal-hint">调整播放速度、循环,以及帧顺序。</p>
|
|
354
|
+
<label class="seq-field">
|
|
355
|
+
播放速度 (FPS)
|
|
356
|
+
<input type="range" id="seq-fps" min="1" max="24" value="8" />
|
|
357
|
+
<span id="seq-fps-val">8</span>
|
|
358
|
+
</label>
|
|
359
|
+
<label class="seq-field seq-check">
|
|
360
|
+
<input type="checkbox" id="seq-loop" checked />
|
|
361
|
+
循环播放
|
|
362
|
+
</label>
|
|
363
|
+
<div class="seq-frames" id="seq-frames"></div>
|
|
364
|
+
<div class="modal-actions">
|
|
365
|
+
<button type="button" class="ghost-btn" id="seq-panel-done">完成</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<input
|
|
371
|
+
type="file"
|
|
372
|
+
id="asset-file-input"
|
|
373
|
+
accept="image/*,video/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg,.mp4,.webm,.mov,.m4v"
|
|
374
|
+
multiple
|
|
375
|
+
hidden
|
|
376
|
+
/>
|
|
377
|
+
<input
|
|
378
|
+
type="file"
|
|
379
|
+
id="file-input"
|
|
380
|
+
accept="image/png,image/jpeg,image/jpg,image/webp,image/gif,image/bmp,image/avif,image/svg+xml,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg"
|
|
381
|
+
multiple
|
|
382
|
+
hidden
|
|
383
|
+
/>
|
|
384
|
+
<input
|
|
385
|
+
type="file"
|
|
386
|
+
id="media-file-input"
|
|
387
|
+
accept="image/*,video/*,audio/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg,.mp4,.webm,.mov,.m4v,.mp3,.wav,.m4a"
|
|
388
|
+
multiple
|
|
389
|
+
hidden
|
|
390
|
+
/>
|
|
391
|
+
</div>
|
|
392
|
+
<script src="/vendor/painterro.min.js"></script>
|
|
393
|
+
<script type="module" src="/app.js"></script>
|
|
394
|
+
</body>
|
|
395
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将序列帧拼成单张透明底图:按「先左到右、再换行向下」填格。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function loadImage(url) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const img = new Image()
|
|
8
|
+
img.onload = () => resolve(img)
|
|
9
|
+
img.onerror = () => reject(new Error(`无法加载帧图:${url}`))
|
|
10
|
+
img.src = url
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string[]} frameUrls
|
|
16
|
+
* @param {{ rows: number, cols: number }} grid
|
|
17
|
+
* @returns {Promise<string>} PNG data URL
|
|
18
|
+
*/
|
|
19
|
+
export async function packSequenceSheet(frameUrls, { rows, cols }) {
|
|
20
|
+
const r = Math.round(Number(rows))
|
|
21
|
+
const c = Math.round(Number(cols))
|
|
22
|
+
if (!Number.isFinite(r) || !Number.isFinite(c) || r < 1 || c < 1) {
|
|
23
|
+
throw new Error('行数与列数需为正整数')
|
|
24
|
+
}
|
|
25
|
+
const frames = Array.isArray(frameUrls) ? frameUrls.filter(Boolean) : []
|
|
26
|
+
if (!frames.length) throw new Error('序列没有可拼接的帧')
|
|
27
|
+
if (r * c < frames.length) {
|
|
28
|
+
throw new Error(`格子不足:${r}×${c}=${r * c},但有 ${frames.length} 帧`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const images = await Promise.all(frames.map((url) => loadImage(url)))
|
|
32
|
+
const cellW = Math.max(...images.map((img) => img.naturalWidth || img.width || 0))
|
|
33
|
+
const cellH = Math.max(...images.map((img) => img.naturalHeight || img.height || 0))
|
|
34
|
+
if (!(cellW > 0 && cellH > 0)) throw new Error('无法读取帧尺寸')
|
|
35
|
+
|
|
36
|
+
const canvas = document.createElement('canvas')
|
|
37
|
+
canvas.width = cellW * c
|
|
38
|
+
canvas.height = cellH * r
|
|
39
|
+
const ctx = canvas.getContext('2d')
|
|
40
|
+
if (!ctx) throw new Error('无法创建画布')
|
|
41
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < images.length; i++) {
|
|
44
|
+
// 先从左到右填满一行,再换到下一行
|
|
45
|
+
const row = Math.floor(i / c)
|
|
46
|
+
const col = i % c
|
|
47
|
+
ctx.drawImage(images[i], col * cellW, row * cellH)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return canvas.toDataURL('image/png')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 根据帧数给出接近方形的默认行列(行优先填充)。 */
|
|
54
|
+
export function suggestSheetGrid(frameCount) {
|
|
55
|
+
const n = Math.max(1, Math.round(Number(frameCount) || 1))
|
|
56
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(n)))
|
|
57
|
+
const rows = Math.max(1, Math.ceil(n / cols))
|
|
58
|
+
return { rows, cols }
|
|
59
|
+
}
|