nai-aclab 1.0.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/README.md +164 -0
- package/app.py +1247 -0
- package/bin/nai-artist-lab.js +115 -0
- package/launcher.py +53 -0
- package/package.json +25 -0
- package/web/index.html +413 -0
- package/web/main.js +1529 -0
- package/web/styles.css +1089 -0
- package/web_app.py +511 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
9
|
+
const launcherPath = path.join(rootDir, "launcher.py");
|
|
10
|
+
const packageJson = require(path.join(rootDir, "package.json"));
|
|
11
|
+
|
|
12
|
+
function userDataDir() {
|
|
13
|
+
if (process.env.NAI_ARTIST_LAB_USER_DIR) {
|
|
14
|
+
return process.env.NAI_ARTIST_LAB_USER_DIR;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
return path.join(process.env.APPDATA || os.homedir(), "NAI Artist Combination Lab");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (process.platform === "darwin") {
|
|
22
|
+
return path.join(os.homedir(), "Library", "Application Support", "NAI Artist Combination Lab");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return path.join(
|
|
26
|
+
process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"),
|
|
27
|
+
"nai-artist-combination-lab"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(`NAI Artist Combination Lab ${packageJson.version}
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
nai-aclab
|
|
36
|
+
nai-artist-lab
|
|
37
|
+
npm start
|
|
38
|
+
|
|
39
|
+
Requirements:
|
|
40
|
+
Node.js 18+
|
|
41
|
+
Python 3.10+
|
|
42
|
+
|
|
43
|
+
Data folder:
|
|
44
|
+
${userDataDir()}
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pythonCandidates() {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
if (process.env.PYTHON) {
|
|
51
|
+
candidates.push({ command: process.env.PYTHON, args: [] });
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === "win32") {
|
|
54
|
+
candidates.push({ command: "py", args: ["-3"] });
|
|
55
|
+
}
|
|
56
|
+
candidates.push({ command: "python3", args: [] });
|
|
57
|
+
candidates.push({ command: "python", args: [] });
|
|
58
|
+
return candidates;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runWithCandidate(candidates, index) {
|
|
62
|
+
if (index >= candidates.length) {
|
|
63
|
+
console.error("Python 3.10+을 찾을 수 없습니다. Python을 설치한 뒤 다시 실행해주세요.");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const candidate = candidates[index];
|
|
68
|
+
const env = {
|
|
69
|
+
...process.env,
|
|
70
|
+
NAI_ARTIST_LAB_USER_DIR: userDataDir(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
fs.mkdirSync(env.NAI_ARTIST_LAB_USER_DIR, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const child = spawn(candidate.command, [...candidate.args, launcherPath], {
|
|
76
|
+
cwd: rootDir,
|
|
77
|
+
env,
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
windowsHide: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let failedToStart = false;
|
|
83
|
+
child.on("error", (error) => {
|
|
84
|
+
failedToStart = true;
|
|
85
|
+
if (error.code === "ENOENT") {
|
|
86
|
+
runWithCandidate(candidates, index + 1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.error(error.message);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.on("exit", (code, signal) => {
|
|
94
|
+
if (failedToStart) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (signal) {
|
|
98
|
+
process.kill(process.pid, signal);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
process.exit(code || 0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
106
|
+
printHelp();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
111
|
+
console.log(packageJson.version);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runWithCandidate(pythonCandidates(), 0);
|
package/launcher.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import threading
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
import ctypes
|
|
8
|
+
from http.server import ThreadingHTTPServer
|
|
9
|
+
|
|
10
|
+
from web_app import Handler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
APP_NAME = "NAI Artist Combination Lab"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_port(start: int = 8777) -> int:
|
|
17
|
+
for port in range(start, start + 100):
|
|
18
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
19
|
+
try:
|
|
20
|
+
sock.bind(("127.0.0.1", port))
|
|
21
|
+
except OSError:
|
|
22
|
+
continue
|
|
23
|
+
return port
|
|
24
|
+
raise RuntimeError("No available local port found.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
port = find_port()
|
|
29
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
30
|
+
url = f"http://127.0.0.1:{port}/"
|
|
31
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
32
|
+
thread.start()
|
|
33
|
+
|
|
34
|
+
webbrowser.open(url)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
if sys.platform.startswith("win"):
|
|
38
|
+
ctypes.windll.user32.MessageBoxW(
|
|
39
|
+
None,
|
|
40
|
+
f"브라우저에서 {APP_NAME}가 실행 중입니다.\n\n{url}\n\n앱 사용을 마친 뒤 확인을 누르면 서버가 종료됩니다.",
|
|
41
|
+
APP_NAME,
|
|
42
|
+
0,
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
print(f"{APP_NAME} is running at {url}")
|
|
46
|
+
input("Press Enter to stop the server...")
|
|
47
|
+
finally:
|
|
48
|
+
server.shutdown()
|
|
49
|
+
server.server_close()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nai-aclab",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local web UI for experimenting with NovelAI artist tag weight combinations.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nai-aclab": "bin/nai-artist-lab.js",
|
|
9
|
+
"nai-artist-lab": "bin/nai-artist-lab.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node bin/nai-artist-lab.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"app.py",
|
|
16
|
+
"web_app.py",
|
|
17
|
+
"launcher.py",
|
|
18
|
+
"web/",
|
|
19
|
+
"bin/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/web/index.html
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>NAI Artist Combination Lab</title>
|
|
7
|
+
<link rel="stylesheet" href="/web/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app-shell">
|
|
11
|
+
<aside class="sidebar">
|
|
12
|
+
<div class="brand">
|
|
13
|
+
<div class="brand-mark">NAI</div>
|
|
14
|
+
<div>
|
|
15
|
+
<strong>Artist Lab</strong>
|
|
16
|
+
<span>Prompt mixing workspace</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<nav class="nav" aria-label="Main">
|
|
20
|
+
<button class="nav-item active" data-tab="generate">⌘ 생성</button>
|
|
21
|
+
<button class="nav-item" data-tab="batting">▦ 타율 테스트</button>
|
|
22
|
+
<button class="nav-item" data-tab="artists"># 작가 태그</button>
|
|
23
|
+
<button class="nav-item" data-tab="presets">▣ 프리셋</button>
|
|
24
|
+
<button class="nav-item" data-tab="settings">⚙ API 설정</button>
|
|
25
|
+
<button class="nav-item" data-tab="compare">≋ 가중치 비교</button>
|
|
26
|
+
<button class="nav-item" data-tab="history">◈ 히스토리</button>
|
|
27
|
+
</nav>
|
|
28
|
+
<div class="sidebar-footer">
|
|
29
|
+
<span id="saveState">준비됨</span>
|
|
30
|
+
</div>
|
|
31
|
+
</aside>
|
|
32
|
+
|
|
33
|
+
<main class="workspace">
|
|
34
|
+
<header class="topbar">
|
|
35
|
+
<div>
|
|
36
|
+
<h1 id="pageTitle">생성</h1>
|
|
37
|
+
<p id="pageSubtitle">프리셋, 작가 조합, 생성 상태를 한 화면에서 조정합니다.</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="top-actions">
|
|
40
|
+
<button class="ghost-button" id="refreshButton">새로고침</button>
|
|
41
|
+
<button class="primary-button" id="generateButton">이미지 생성</button>
|
|
42
|
+
</div>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
<section id="generate" class="tab-page active">
|
|
46
|
+
<section class="panel live-preview-panel">
|
|
47
|
+
<div class="panel-header">
|
|
48
|
+
<div>
|
|
49
|
+
<h2>실시간 이미지 미리보기</h2>
|
|
50
|
+
<p>생성이 끝나는 즉시 이미지가 여기에 추가됩니다.</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div id="liveGallery" class="live-gallery">
|
|
54
|
+
<p>아직 생성된 이미지가 없습니다.</p>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<div class="grid two-one">
|
|
59
|
+
<section class="panel">
|
|
60
|
+
<div class="panel-header">
|
|
61
|
+
<div>
|
|
62
|
+
<h2>생성 옵션</h2>
|
|
63
|
+
<p>마지막 선택값은 자동 저장됩니다.</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-grid">
|
|
67
|
+
<label>
|
|
68
|
+
<span>베이스 + 퀄리티 프리셋</span>
|
|
69
|
+
<select id="baseSelect"></select>
|
|
70
|
+
</label>
|
|
71
|
+
<label>
|
|
72
|
+
<span>캐릭터 프리셋</span>
|
|
73
|
+
<select id="charSelect"></select>
|
|
74
|
+
</label>
|
|
75
|
+
<label>
|
|
76
|
+
<span>생성 개수</span>
|
|
77
|
+
<input id="countInput" type="number" min="1" max="200" />
|
|
78
|
+
</label>
|
|
79
|
+
<label>
|
|
80
|
+
<span>이미지 사이즈</span>
|
|
81
|
+
<select id="imageSizeSelect"></select>
|
|
82
|
+
</label>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="fixed-artist-panel" id="fixedArtistPanel">
|
|
85
|
+
<div class="fixed-artist-head">
|
|
86
|
+
<div class="mode-status" id="fixedArtistModeLabel">랜덤 가중치 모드</div>
|
|
87
|
+
<button class="ghost-button" id="clearFixedArtistsButton" type="button">고정 해제</button>
|
|
88
|
+
</div>
|
|
89
|
+
<p id="fixedArtistSummary">작가 태그는 생성할 때마다 랜덤으로 선택됩니다.</p>
|
|
90
|
+
<div id="fixedArtistList" class="artist-weight-list fixed-artist-list"></div>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="button-row">
|
|
93
|
+
<button class="ghost-button" id="previewButton">프롬프트 새로 뽑기</button>
|
|
94
|
+
<button class="primary-button" id="generateButtonInline">이미지 생성</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="progress-wrap">
|
|
97
|
+
<div class="progress-label">
|
|
98
|
+
<span id="progressText">대기 중</span>
|
|
99
|
+
<span id="progressCount">0 / 0</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="progress"><div id="progressBar"></div></div>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
<section class="panel">
|
|
106
|
+
<div class="panel-header">
|
|
107
|
+
<div>
|
|
108
|
+
<h2>실행 로그</h2>
|
|
109
|
+
<p>생성 결과와 API 오류를 바로 확인합니다.</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<pre class="log" id="jobLog"></pre>
|
|
113
|
+
</section>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<section class="panel preview-panel">
|
|
117
|
+
<div class="panel-header">
|
|
118
|
+
<div>
|
|
119
|
+
<h2>최종 프롬프트 미리보기</h2>
|
|
120
|
+
<p>베이스 → 작가 → 퀄리티 | 캐릭터 순서로 조립됩니다.</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="prompt-preview">
|
|
124
|
+
<div>
|
|
125
|
+
<strong>Prompt</strong>
|
|
126
|
+
<pre id="promptPreview"></pre>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<strong>Negative</strong>
|
|
130
|
+
<pre id="negativePreview"></pre>
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<strong>UC</strong>
|
|
134
|
+
<pre id="ucPreview"></pre>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<section id="batting" class="tab-page">
|
|
142
|
+
<section class="panel">
|
|
143
|
+
<div class="panel-header">
|
|
144
|
+
<div>
|
|
145
|
+
<h2>타율 테스트</h2>
|
|
146
|
+
<p>고정 작가가중치를 유지한 채 여러 씬의 프리셋 조합을 한 번에 테스트합니다.</p>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="button-row">
|
|
149
|
+
<button class="ghost-button" id="addBattingSceneButton" type="button">씬 추가</button>
|
|
150
|
+
<button class="primary-button" id="startBattingButton" type="button">타율 테스트 시작</button>
|
|
151
|
+
<button class="danger-button" id="stopBattingButton" type="button" disabled>생성 중지</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="fixed-artist-panel">
|
|
155
|
+
<div class="fixed-artist-head">
|
|
156
|
+
<div class="mode-status" id="battingFixedArtistModeLabel">랜덤 가중치 모드</div>
|
|
157
|
+
</div>
|
|
158
|
+
<p id="battingFixedArtistSummary">히스토리나 가중치 비교에서 마음에 드는 이미지의 가중치를 먼저 불러오세요.</p>
|
|
159
|
+
<div id="battingFixedArtistList" class="artist-weight-list fixed-artist-list"></div>
|
|
160
|
+
</div>
|
|
161
|
+
<div id="battingSceneList" class="batting-scene-list"></div>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<section class="panel live-preview-panel">
|
|
165
|
+
<div class="panel-header">
|
|
166
|
+
<div>
|
|
167
|
+
<h2>타율 테스트 실시간 미리보기</h2>
|
|
168
|
+
<p>각 씬에서 생성된 이미지가 순서대로 추가됩니다.</p>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div id="battingLiveGallery" class="live-gallery">
|
|
172
|
+
<p>아직 생성된 이미지가 없습니다.</p>
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
<div class="grid two-one">
|
|
177
|
+
<section class="panel">
|
|
178
|
+
<div class="panel-header">
|
|
179
|
+
<div>
|
|
180
|
+
<h2>진행 상태</h2>
|
|
181
|
+
<p>전체 씬 기준 생성 진행률입니다.</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="progress-wrap">
|
|
185
|
+
<div class="progress-label">
|
|
186
|
+
<span id="battingProgressText">대기 중</span>
|
|
187
|
+
<span id="battingProgressCount">0 / 0</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="progress" id="battingProgress"><div id="battingProgressBar"></div></div>
|
|
190
|
+
</div>
|
|
191
|
+
</section>
|
|
192
|
+
<section class="panel">
|
|
193
|
+
<div class="panel-header">
|
|
194
|
+
<div>
|
|
195
|
+
<h2>실행 로그</h2>
|
|
196
|
+
<p>씬별 생성 결과와 API 오류를 확인합니다.</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<pre class="log" id="battingJobLog"></pre>
|
|
200
|
+
</section>
|
|
201
|
+
</div>
|
|
202
|
+
</section>
|
|
203
|
+
|
|
204
|
+
<section id="artists" class="tab-page">
|
|
205
|
+
<div class="split">
|
|
206
|
+
<section class="panel list-panel">
|
|
207
|
+
<div class="panel-header">
|
|
208
|
+
<h2>카테고리</h2>
|
|
209
|
+
<button class="icon-button" id="addCategoryButton" title="새 카테고리">+</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div id="categoryList" class="item-list"></div>
|
|
212
|
+
</section>
|
|
213
|
+
<section class="panel editor-panel">
|
|
214
|
+
<div class="panel-header">
|
|
215
|
+
<div>
|
|
216
|
+
<h2>작가 태그 편집</h2>
|
|
217
|
+
<p>선택 태그 수가 비어 있으면 모든 태그를 포함합니다.</p>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="form-grid compact">
|
|
221
|
+
<label><span>카테고리명</span><input id="catName" /></label>
|
|
222
|
+
<label><span>최소 가중치</span><input id="catMin" type="number" step="0.01" /></label>
|
|
223
|
+
<label><span>최대 가중치</span><input id="catMax" type="number" step="0.01" /></label>
|
|
224
|
+
<label><span>Granule</span><input id="catGranule" type="number" step="0.01" /></label>
|
|
225
|
+
<label><span>선택 태그 수</span><input id="catPicks" type="number" min="1" placeholder="전체" /></label>
|
|
226
|
+
</div>
|
|
227
|
+
<label class="text-label">
|
|
228
|
+
<span>태그 목록</span>
|
|
229
|
+
<textarea id="catTags" spellcheck="false"></textarea>
|
|
230
|
+
</label>
|
|
231
|
+
<div class="recognized-box">
|
|
232
|
+
<div>
|
|
233
|
+
<strong>인식된 artist 태그</strong>
|
|
234
|
+
<span id="recognizedCount">0개</span>
|
|
235
|
+
</div>
|
|
236
|
+
<div id="recognizedTags" class="tag-chips"></div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="button-row">
|
|
239
|
+
<button class="danger-button" id="deleteCategoryButton">카테고리 삭제</button>
|
|
240
|
+
</div>
|
|
241
|
+
</section>
|
|
242
|
+
</div>
|
|
243
|
+
</section>
|
|
244
|
+
|
|
245
|
+
<section id="presets" class="tab-page">
|
|
246
|
+
<div class="grid half">
|
|
247
|
+
<section class="panel">
|
|
248
|
+
<div class="panel-header">
|
|
249
|
+
<h2>베이스 + 퀄리티</h2>
|
|
250
|
+
<div class="preset-header-actions">
|
|
251
|
+
<button class="ghost-button" id="basePresetAllButton" type="button">전체 목록</button>
|
|
252
|
+
<button class="icon-button" id="addBaseButton" title="새 베이스">+</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div id="baseList" class="item-list horizontal"></div>
|
|
256
|
+
<label class="text-label short"><span>프리셋 이름</span><input id="baseName" /></label>
|
|
257
|
+
<label class="text-label"><span>베이스 프롬프트</span><textarea id="basePrompt" spellcheck="false"></textarea></label>
|
|
258
|
+
<label class="text-label"><span>퀄리티 프롬프트</span><textarea id="qualityPrompt" spellcheck="false"></textarea></label>
|
|
259
|
+
<label class="text-label"><span>공용 퀄리티 프롬프트 Override</span><textarea id="qualityOverridePrompt" spellcheck="false" placeholder="비워두면 선택한 베이스 + 퀄리티 프리셋의 퀄리티 프롬프트를 사용합니다."></textarea></label>
|
|
260
|
+
<button class="danger-button" id="deleteBaseButton">베이스 프리셋 삭제</button>
|
|
261
|
+
</section>
|
|
262
|
+
<section class="panel">
|
|
263
|
+
<div class="panel-header">
|
|
264
|
+
<div>
|
|
265
|
+
<h2>캐릭터</h2>
|
|
266
|
+
<p>생성 시 캐릭터는 쉼표가 아니라 | 로 분리됩니다.</p>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="preset-header-actions">
|
|
269
|
+
<button class="ghost-button" id="charPresetAllButton" type="button">전체 목록</button>
|
|
270
|
+
<button class="icon-button" id="addCharButton" title="새 캐릭터">+</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div id="charList" class="item-list horizontal"></div>
|
|
274
|
+
<label class="text-label short"><span>프리셋 이름</span><input id="charName" /></label>
|
|
275
|
+
<div id="characterEditors" class="character-editors"></div>
|
|
276
|
+
<button class="danger-button" id="deleteCharButton">캐릭터 프리셋 삭제</button>
|
|
277
|
+
</section>
|
|
278
|
+
</div>
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
<section id="settings" class="tab-page">
|
|
282
|
+
<div class="grid two-one">
|
|
283
|
+
<section class="panel">
|
|
284
|
+
<div class="panel-header">
|
|
285
|
+
<h2>API 설정</h2>
|
|
286
|
+
<p>생성 시 자동 저장됩니다.</p>
|
|
287
|
+
</div>
|
|
288
|
+
<div id="apiForm" class="form-grid compact"></div>
|
|
289
|
+
<label class="switch-row">
|
|
290
|
+
<input id="mockMode" type="checkbox" />
|
|
291
|
+
<span>목업 모드 사용</span>
|
|
292
|
+
</label>
|
|
293
|
+
<button class="primary-button" id="saveSettingsButton">설정 저장</button>
|
|
294
|
+
</section>
|
|
295
|
+
<section class="panel">
|
|
296
|
+
<div class="panel-header">
|
|
297
|
+
<h2>Undesired Content</h2>
|
|
298
|
+
<p>V4 negative와 legacy UC를 분리해서 보냅니다.</p>
|
|
299
|
+
</div>
|
|
300
|
+
<label class="text-label"><span>공통 네거티브 프롬프트</span><textarea id="negativePrompt" spellcheck="false"></textarea></label>
|
|
301
|
+
<label class="text-label"><span>UC 프롬프트 (parameters.uc / negative_prompt)</span><textarea id="ucPrompt" spellcheck="false"></textarea></label>
|
|
302
|
+
</section>
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
|
|
306
|
+
<section id="history" class="tab-page">
|
|
307
|
+
<div class="split">
|
|
308
|
+
<section class="panel list-panel">
|
|
309
|
+
<div class="panel-header">
|
|
310
|
+
<div>
|
|
311
|
+
<h2>생성 히스토리</h2>
|
|
312
|
+
<p>항목을 체크해서 정리할 수 있습니다.</p>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="history-toolbar">
|
|
316
|
+
<label class="switch-row compact-switch">
|
|
317
|
+
<input id="deleteFilesToggle" type="checkbox" />
|
|
318
|
+
<span>출력 파일도 삭제</span>
|
|
319
|
+
</label>
|
|
320
|
+
<div class="button-row">
|
|
321
|
+
<button class="ghost-button" id="selectAllHistoryButton">전체 선택</button>
|
|
322
|
+
<button class="danger-button" id="deleteSelectedHistoryButton">선택 삭제</button>
|
|
323
|
+
<button class="danger-button" id="clearHistoryButton">전체 삭제</button>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div id="historyList" class="item-list"></div>
|
|
327
|
+
</section>
|
|
328
|
+
<section class="panel">
|
|
329
|
+
<div class="panel-header">
|
|
330
|
+
<div>
|
|
331
|
+
<h2>결과 / 월드컵</h2>
|
|
332
|
+
<p>이미지를 비교해서 선호 조합을 찾습니다.</p>
|
|
333
|
+
</div>
|
|
334
|
+
<button class="ghost-button" id="startWorldcupButton">월드컵 시작</button>
|
|
335
|
+
</div>
|
|
336
|
+
<div id="historyDetail" class="history-detail"></div>
|
|
337
|
+
<div id="worldcupStatus" class="worldcup-status"></div>
|
|
338
|
+
<div id="worldcup" class="worldcup"></div>
|
|
339
|
+
</section>
|
|
340
|
+
</div>
|
|
341
|
+
</section>
|
|
342
|
+
|
|
343
|
+
<section id="compare" class="tab-page">
|
|
344
|
+
<section class="panel">
|
|
345
|
+
<div class="panel-header">
|
|
346
|
+
<div>
|
|
347
|
+
<h2>작가태그 가중치 비교</h2>
|
|
348
|
+
<p>생성 이미지별로 어떤 artist 태그와 가중치가 들어갔는지 가로로 훑어봅니다.</p>
|
|
349
|
+
</div>
|
|
350
|
+
<label class="compare-select">
|
|
351
|
+
<span>비교 대상</span>
|
|
352
|
+
<select id="compareHistorySelect"></select>
|
|
353
|
+
</label>
|
|
354
|
+
</div>
|
|
355
|
+
<div id="compareStrip" class="compare-strip"></div>
|
|
356
|
+
</section>
|
|
357
|
+
|
|
358
|
+
<section class="panel preview-panel">
|
|
359
|
+
<div class="panel-header">
|
|
360
|
+
<div>
|
|
361
|
+
<h2>가중치 매트릭스</h2>
|
|
362
|
+
<p>행은 artist 태그, 열은 이미지입니다. 빈칸은 해당 이미지에 사용되지 않은 태그입니다.</p>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div id="compareMatrix" class="compare-matrix"></div>
|
|
366
|
+
</section>
|
|
367
|
+
</section>
|
|
368
|
+
</main>
|
|
369
|
+
</div>
|
|
370
|
+
<div id="imageModal" class="image-modal" hidden>
|
|
371
|
+
<div class="image-modal-backdrop" id="imageModalBackdrop"></div>
|
|
372
|
+
<section class="image-modal-panel" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
373
|
+
<div class="image-modal-view">
|
|
374
|
+
<img id="modalImage" alt="selected image" />
|
|
375
|
+
</div>
|
|
376
|
+
<aside class="image-modal-info">
|
|
377
|
+
<div class="panel-header">
|
|
378
|
+
<div>
|
|
379
|
+
<h2 id="modalTitle">이미지</h2>
|
|
380
|
+
<p id="modalSubtitle"></p>
|
|
381
|
+
</div>
|
|
382
|
+
<button class="icon-button" id="modalCloseButton" title="닫기">×</button>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="modal-actions">
|
|
385
|
+
<button class="primary-button" id="modalGenerateButton">가중치 불러오기</button>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="modal-section">
|
|
388
|
+
<strong>작가 가중치</strong>
|
|
389
|
+
<div id="modalArtists" class="artist-weight-list"></div>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="modal-section">
|
|
392
|
+
<strong>Prompt</strong>
|
|
393
|
+
<pre id="modalPrompt"></pre>
|
|
394
|
+
</div>
|
|
395
|
+
</aside>
|
|
396
|
+
</section>
|
|
397
|
+
</div>
|
|
398
|
+
<div id="presetPickerModal" class="image-modal" hidden>
|
|
399
|
+
<div class="image-modal-backdrop" id="presetPickerBackdrop"></div>
|
|
400
|
+
<section class="preset-picker-panel" role="dialog" aria-modal="true" aria-labelledby="presetPickerTitle">
|
|
401
|
+
<div class="panel-header">
|
|
402
|
+
<div>
|
|
403
|
+
<h2 id="presetPickerTitle">프리셋 전체 목록</h2>
|
|
404
|
+
<p id="presetPickerSubtitle">사용할 프리셋을 선택하세요.</p>
|
|
405
|
+
</div>
|
|
406
|
+
<button class="icon-button" id="presetPickerCloseButton" title="닫기">×</button>
|
|
407
|
+
</div>
|
|
408
|
+
<div id="presetPickerList" class="preset-picker-list"></div>
|
|
409
|
+
</section>
|
|
410
|
+
</div>
|
|
411
|
+
<script src="/web/main.js"></script>
|
|
412
|
+
</body>
|
|
413
|
+
</html>
|