nai-aclab 1.0.12 → 1.1.1
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 +4 -3
- package/app.py +16 -25
- package/package.json +1 -1
- package/web/index.html +121 -86
- package/web/main.js +507 -204
- package/web/styles.css +294 -0
- package/web_app.py +10 -12
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ nai-aclab
|
|
|
66
66
|
|
|
67
67
|
API 토큰을 아직 넣고 싶지 않다면 `목업 모드 사용`을 켜둔 채로 UI만 먼저 테스트할 수 있습니다. 목업 모드에서는 실제 NovelAI API를 쓰지 않고 테스트용 이미지를 만듭니다.
|
|
68
68
|
|
|
69
|
-
대부분의 사용자는 `Endpoint`, `Model`, `Sampler`, `Noise Schedule` 같은 값은 기본값 그대로 두면 됩니다.
|
|
69
|
+
대부분의 사용자는 `Endpoint`, `Model`, `Sampler`, `Noise Schedule` 같은 값은 기본값 그대로 두면 됩니다. 처음 설치하면 이미지 모델은 `NAI Diffusion V5 Full`로 설정됩니다.
|
|
70
70
|
|
|
71
71
|

|
|
72
72
|
|
|
@@ -103,7 +103,8 @@ artist:sample_a
|
|
|
103
103
|
- 퀄리티 프롬프트: 품질 관련 태그
|
|
104
104
|
- 캐릭터 프롬프트: 캐릭터별 외형과 행동
|
|
105
105
|
- 네거티브 프롬프트: 피하고 싶은 요소
|
|
106
|
-
|
|
106
|
+
|
|
107
|
+
공통 네거티브 프롬프트는 NovelAI의 UC에도 같은 내용으로 자동 전송됩니다.
|
|
107
108
|
|
|
108
109
|
최종 프롬프트는 아래 순서로 조립됩니다.
|
|
109
110
|
|
|
@@ -232,7 +233,7 @@ NovelAI V4+의 멀티 캐릭터 프롬프트는 캐릭터를 쉼표로 합치지
|
|
|
232
233
|
|
|
233
234
|
API 토큰은 일반 설정 파일에 저장하지 않고 별도 secret store에 저장합니다. 앱 화면으로 다시 내려보내지도 않습니다.
|
|
234
235
|
|
|
235
|
-
생성 히스토리에는 이미지 표시와 작가 가중치 비교에 필요한 최소 정보만 저장합니다. 최종 프롬프트, 네거티브 프롬프트,
|
|
236
|
+
생성 히스토리에는 이미지 표시와 작가 가중치 비교에 필요한 최소 정보만 저장합니다. 최종 프롬프트, 네거티브 프롬프트, 생성 요청 JSON, 로컬 절대 경로는 별도로 저장하지 않습니다.
|
|
236
237
|
|
|
237
238
|
NovelAI가 이미지 파일 안에 저장하는 생성 정보는 그대로 남을 수 있습니다. 이 정보는 이미지 자체의 메타데이터입니다.
|
|
238
239
|
|
package/app.py
CHANGED
|
@@ -131,6 +131,7 @@ def cleanup_saved_request_jsons() -> None:
|
|
|
131
131
|
|
|
132
132
|
def state_dict_without_secrets(state: AppState | dict) -> dict:
|
|
133
133
|
data = asdict(state) if not isinstance(state, dict) else dict(state)
|
|
134
|
+
data.pop("uc_prompt", None)
|
|
134
135
|
api = dict(data.get("api", {}) or {})
|
|
135
136
|
api["token"] = ""
|
|
136
137
|
data["api"] = api
|
|
@@ -286,7 +287,7 @@ class ApiSettings:
|
|
|
286
287
|
token: str = ""
|
|
287
288
|
endpoint: str = "https://image.novelai.net/ai/generate-image"
|
|
288
289
|
user_agent: str = DEFAULT_USER_AGENT
|
|
289
|
-
model: str = "nai-diffusion-
|
|
290
|
+
model: str = "nai-diffusion-5-full"
|
|
290
291
|
width: int = 832
|
|
291
292
|
height: int = 1216
|
|
292
293
|
steps: int = 28
|
|
@@ -327,7 +328,6 @@ class AppState:
|
|
|
327
328
|
character_presets: list[CharacterPreset] = field(default_factory=list)
|
|
328
329
|
quality_override_prompt: str = ""
|
|
329
330
|
negative_prompt: str = "lowres, bad anatomy, bad hands, text, error, missing fingers"
|
|
330
|
-
uc_prompt: str = ""
|
|
331
331
|
api: ApiSettings = field(default_factory=ApiSettings)
|
|
332
332
|
generation: GenerationSettings = field(default_factory=GenerationSettings)
|
|
333
333
|
batting_scenes: list[BattingScene] = field(default_factory=list)
|
|
@@ -337,8 +337,8 @@ class AppState:
|
|
|
337
337
|
def default_state() -> AppState:
|
|
338
338
|
return AppState(
|
|
339
339
|
categories=[
|
|
340
|
-
Category("메인 그림체 작가", [
|
|
341
|
-
Category("그림체 안정화 작가", [
|
|
340
|
+
Category("메인 그림체 작가", [], 1.0, 1.4, 0.05, 0),
|
|
341
|
+
Category("그림체 안정화 작가", [], 0.4, 0.9, 0.1, 0),
|
|
342
342
|
],
|
|
343
343
|
base_presets=[
|
|
344
344
|
PromptPreset("기본", "", "masterpiece, best quality, very aesthetic, detailed illustration"),
|
|
@@ -392,7 +392,6 @@ def load_state() -> AppState:
|
|
|
392
392
|
character_presets=[CharacterPreset(**item) for item in data.get("character_presets", [])],
|
|
393
393
|
quality_override_prompt=quality_override,
|
|
394
394
|
negative_prompt=data.get("negative_prompt", ""),
|
|
395
|
-
uc_prompt=data.get("uc_prompt", data.get("negative_prompt", "")),
|
|
396
395
|
api=ApiSettings(**api_data),
|
|
397
396
|
generation=GenerationSettings(**data.get("generation", {})),
|
|
398
397
|
batting_scenes=[BattingScene(**item) for item in data.get("batting_scenes", [])],
|
|
@@ -412,7 +411,7 @@ class NovelAIClient:
|
|
|
412
411
|
def __init__(self, settings: ApiSettings):
|
|
413
412
|
self.settings = settings
|
|
414
413
|
|
|
415
|
-
def build_payload(self, prompt: str, negative_prompt: str,
|
|
414
|
+
def build_payload(self, prompt: str, negative_prompt: str, seed: int | None = None) -> dict:
|
|
416
415
|
actual_seed = seed if seed is not None else (
|
|
417
416
|
self.settings.seed if self.settings.seed >= 0 else random.randint(0, 2**32 - 1)
|
|
418
417
|
)
|
|
@@ -468,18 +467,18 @@ class NovelAIClient:
|
|
|
468
467
|
"wonky_vibe_correlation": True,
|
|
469
468
|
"stream": "none",
|
|
470
469
|
"version": 1,
|
|
471
|
-
"uc":
|
|
472
|
-
"negative_prompt":
|
|
470
|
+
"uc": negative_prompt,
|
|
471
|
+
"negative_prompt": negative_prompt,
|
|
473
472
|
"request_type": "PromptGenerateRequest",
|
|
474
473
|
},
|
|
475
474
|
}
|
|
476
475
|
|
|
477
|
-
def generate(self, prompt: str, negative_prompt: str,
|
|
476
|
+
def generate(self, prompt: str, negative_prompt: str, output_path: Path) -> None:
|
|
478
477
|
if self.settings.mock_mode or not self.settings.token.strip():
|
|
479
478
|
write_placeholder_png(output_path, prompt, self.settings.width, self.settings.height)
|
|
480
479
|
return
|
|
481
480
|
|
|
482
|
-
payload = self.build_payload(prompt, negative_prompt
|
|
481
|
+
payload = self.build_payload(prompt, negative_prompt)
|
|
483
482
|
req = urllib.request.Request(
|
|
484
483
|
self.settings.endpoint,
|
|
485
484
|
data=json.dumps(payload).encode("utf-8"),
|
|
@@ -794,11 +793,7 @@ class App(tk.Tk if tk else object):
|
|
|
794
793
|
neg_box.pack(fill=BOTH, expand=True, pady=12)
|
|
795
794
|
self.negative_text = tk.Text(neg_box, height=8, wrap="word")
|
|
796
795
|
self.negative_text.pack(fill=BOTH, expand=True)
|
|
797
|
-
|
|
798
|
-
uc_box.pack(fill=BOTH, expand=True, pady=(0, 12))
|
|
799
|
-
self.uc_text = tk.Text(uc_box, height=5, wrap="word")
|
|
800
|
-
self.uc_text.pack(fill=BOTH, expand=True)
|
|
801
|
-
ttk.Button(uc_box, text="네거티브 / UC 저장", command=self.save_negative).pack(anchor="e", pady=6)
|
|
796
|
+
ttk.Button(neg_box, text="네거티브 프롬프트 저장", command=self.save_negative).pack(anchor="e", pady=6)
|
|
802
797
|
|
|
803
798
|
def refresh_all(self) -> None:
|
|
804
799
|
self.refresh_categories()
|
|
@@ -1171,8 +1166,6 @@ class App(tk.Tk if tk else object):
|
|
|
1171
1166
|
var.set(getattr(api, key))
|
|
1172
1167
|
self.negative_text.delete("1.0", END)
|
|
1173
1168
|
self.negative_text.insert("1.0", self.state_data.negative_prompt)
|
|
1174
|
-
self.uc_text.delete("1.0", END)
|
|
1175
|
-
self.uc_text.insert("1.0", self.state_data.uc_prompt)
|
|
1176
1169
|
|
|
1177
1170
|
def save_api_settings(self, show_message: bool = True) -> None:
|
|
1178
1171
|
api = self.state_data.api
|
|
@@ -1184,7 +1177,6 @@ class App(tk.Tk if tk else object):
|
|
|
1184
1177
|
|
|
1185
1178
|
def save_negative(self) -> None:
|
|
1186
1179
|
self.state_data.negative_prompt = self.negative_text.get("1.0", END).strip()
|
|
1187
|
-
self.state_data.uc_prompt = self.uc_text.get("1.0", END).strip()
|
|
1188
1180
|
save_state(self.state_data)
|
|
1189
1181
|
|
|
1190
1182
|
def random_artist_tags(self) -> list[dict]:
|
|
@@ -1200,7 +1192,7 @@ class App(tk.Tk if tk else object):
|
|
|
1200
1192
|
result.append({"category": cat.name, "tag": tag, "weight": weight, "prompt": weight_tag(tag, weight)})
|
|
1201
1193
|
return result
|
|
1202
1194
|
|
|
1203
|
-
def build_prompt(self) -> tuple[str, str,
|
|
1195
|
+
def build_prompt(self) -> tuple[str, str, list[dict]]:
|
|
1204
1196
|
base = self.find_base()
|
|
1205
1197
|
char = self.find_char()
|
|
1206
1198
|
artists = self.random_artist_tags()
|
|
@@ -1226,11 +1218,10 @@ class App(tk.Tk if tk else object):
|
|
|
1226
1218
|
if char:
|
|
1227
1219
|
negative.extend(n.strip() for n in char.negatives if n.strip())
|
|
1228
1220
|
negative_prompt = ", ".join(n for n in negative if n)
|
|
1229
|
-
|
|
1230
|
-
return " | ".join(prompt_parts), negative_prompt, uc_prompt, artists
|
|
1221
|
+
return " | ".join(prompt_parts), negative_prompt, artists
|
|
1231
1222
|
|
|
1232
1223
|
def preview_prompt(self) -> None:
|
|
1233
|
-
prompt, negative,
|
|
1224
|
+
prompt, negative, artists = self.build_prompt()
|
|
1234
1225
|
base = self.find_base()
|
|
1235
1226
|
char = self.find_char()
|
|
1236
1227
|
self.prompt_preview.delete("1.0", END)
|
|
@@ -1249,7 +1240,7 @@ class App(tk.Tk if tk else object):
|
|
|
1249
1240
|
for idx, char_prompt in enumerate(char.prompts, start=1):
|
|
1250
1241
|
if char_prompt.strip():
|
|
1251
1242
|
self.prompt_preview.insert(END, f"{idx}. {char_prompt.strip()}\n")
|
|
1252
|
-
self.prompt_preview.insert(END, f"\n[Negative]\n{negative}\n
|
|
1243
|
+
self.prompt_preview.insert(END, f"\n[Negative / UC]\n{negative}\n")
|
|
1253
1244
|
|
|
1254
1245
|
def start_generation(self) -> None:
|
|
1255
1246
|
count = max(1, int(self.count_var.get()))
|
|
@@ -1274,7 +1265,7 @@ class App(tk.Tk if tk else object):
|
|
|
1274
1265
|
client = NovelAIClient(self.state_data.api)
|
|
1275
1266
|
items = []
|
|
1276
1267
|
for idx in range(count):
|
|
1277
|
-
prompt, negative,
|
|
1268
|
+
prompt, negative, artists = self.build_prompt()
|
|
1278
1269
|
path = out_dir / f"image_{idx + 1:03}.png"
|
|
1279
1270
|
metadata = {
|
|
1280
1271
|
"path": output_ref(str(path)),
|
|
@@ -1282,7 +1273,7 @@ class App(tk.Tk if tk else object):
|
|
|
1282
1273
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
1283
1274
|
}
|
|
1284
1275
|
try:
|
|
1285
|
-
client.generate(prompt, negative,
|
|
1276
|
+
client.generate(prompt, negative, path)
|
|
1286
1277
|
items.append(metadata)
|
|
1287
1278
|
self.after(0, self.log_line, f"{idx + 1}/{count} 완료: {path.name}")
|
|
1288
1279
|
except Exception as exc:
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
<aside class="sidebar">
|
|
12
12
|
<div class="brand">
|
|
13
13
|
<div class="brand-mark">NAI</div>
|
|
14
|
-
<div>
|
|
15
|
-
<strong>Artist Lab</strong>
|
|
16
|
-
<span>
|
|
14
|
+
<div>
|
|
15
|
+
<strong>Artist Lab</strong>
|
|
16
|
+
<span>NovelAI 작가 조합 실험실</span>
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
19
|
<nav class="nav" aria-label="Main">
|
|
20
|
-
<button class="nav-item active" data-tab="generate">⌘ 생성</button>
|
|
20
|
+
<button class="nav-item active" data-tab="generate">⌘ 이미지 생성</button>
|
|
21
21
|
<button class="nav-item" data-tab="batting">▦ 타율 테스트</button>
|
|
22
22
|
<button class="nav-item" data-tab="artists"># 작가 태그</button>
|
|
23
|
-
<button class="nav-item" data-tab="presets">▣ 프리셋</button>
|
|
23
|
+
<button class="nav-item" data-tab="presets">▣ 프롬프트 프리셋</button>
|
|
24
24
|
<button class="nav-item" data-tab="compare">≋ 가중치 비교</button>
|
|
25
|
-
<button class="nav-item" data-tab="history">◈
|
|
25
|
+
<button class="nav-item" data-tab="history">◈ 생성 기록</button>
|
|
26
26
|
</nav>
|
|
27
27
|
<div class="sidebar-tools">
|
|
28
28
|
<button class="nav-item settings-nav-item" data-tab="settings">⚙ API 설정</button>
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
</aside>
|
|
34
34
|
|
|
35
35
|
<main class="workspace">
|
|
36
|
-
<header class="topbar">
|
|
36
|
+
<header class="topbar">
|
|
37
37
|
<div>
|
|
38
38
|
<h1 id="pageTitle">생성</h1>
|
|
39
39
|
<p id="pageSubtitle">프리셋, 작가 조합, 생성 상태를 한 화면에서 조정합니다.</p>
|
|
40
|
-
</div>
|
|
40
|
+
</div>
|
|
41
41
|
<div class="top-actions">
|
|
42
|
-
<button class="
|
|
42
|
+
<button class="icon-button" id="refreshButton" type="button" title="저장된 내용을 다시 불러오기" aria-label="저장된 내용을 다시 불러오기">↻</button>
|
|
43
43
|
</div>
|
|
44
|
-
</header>
|
|
44
|
+
</header>
|
|
45
45
|
|
|
46
46
|
<section id="generate" class="tab-page active">
|
|
47
|
-
<section class="panel live-preview-panel">
|
|
47
|
+
<section class="panel live-preview-panel">
|
|
48
48
|
<div class="panel-header">
|
|
49
49
|
<div>
|
|
50
50
|
<h2>실시간 이미지 미리보기</h2>
|
|
@@ -53,16 +53,19 @@
|
|
|
53
53
|
</div>
|
|
54
54
|
<div id="liveGallery" class="live-gallery">
|
|
55
55
|
<p>아직 생성된 이미지가 없습니다.</p>
|
|
56
|
-
</div>
|
|
57
|
-
</section>
|
|
58
|
-
|
|
59
|
-
<
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section id="setupGuide" class="setup-guide" aria-label="생성 준비 상태"></section>
|
|
60
|
+
|
|
61
|
+
<div class="grid two-one">
|
|
60
62
|
<section class="panel">
|
|
61
|
-
<div class="panel-header">
|
|
62
|
-
<div>
|
|
63
|
-
<h2>생성 옵션</h2>
|
|
64
|
-
<p
|
|
65
|
-
</div>
|
|
63
|
+
<div class="panel-header">
|
|
64
|
+
<div>
|
|
65
|
+
<h2>생성 옵션</h2>
|
|
66
|
+
<p>프리셋과 이미지 수를 고른 뒤 바로 생성할 수 있습니다.</p>
|
|
67
|
+
</div>
|
|
68
|
+
<span class="mode-status" id="generationModeBadge"></span>
|
|
66
69
|
</div>
|
|
67
70
|
<div class="form-grid">
|
|
68
71
|
<label>
|
|
@@ -90,9 +93,9 @@
|
|
|
90
93
|
<p id="fixedArtistSummary">작가 태그는 생성할 때마다 랜덤으로 선택됩니다.</p>
|
|
91
94
|
<div id="fixedArtistList" class="artist-weight-list fixed-artist-list"></div>
|
|
92
95
|
</div>
|
|
93
|
-
<div class="button-row">
|
|
94
|
-
<button class="ghost-button" id="previewButton"
|
|
95
|
-
<button class="primary-button" id="generateButtonInline">이미지 생성</button>
|
|
96
|
+
<div class="button-row">
|
|
97
|
+
<button class="ghost-button" id="previewButton" type="button" title="이미지는 만들지 않고 랜덤 작가 조합과 최종 프롬프트만 다시 계산합니다.">작가 조합 다시 뽑기</button>
|
|
98
|
+
<button class="primary-button" id="generateButtonInline">이미지 생성</button>
|
|
96
99
|
<button class="danger-button" id="stopGenerateButton" type="button" disabled>생성 중지</button>
|
|
97
100
|
</div>
|
|
98
101
|
<div class="progress-wrap">
|
|
@@ -127,14 +130,10 @@
|
|
|
127
130
|
<strong>Prompt</strong>
|
|
128
131
|
<pre id="promptPreview"></pre>
|
|
129
132
|
</div>
|
|
130
|
-
<div>
|
|
131
|
-
<strong>Negative</strong>
|
|
132
|
-
<pre id="negativePreview"></pre>
|
|
133
|
-
</div>
|
|
134
|
-
<div>
|
|
135
|
-
<strong>UC</strong>
|
|
136
|
-
<pre id="ucPreview"></pre>
|
|
137
|
-
</div>
|
|
133
|
+
<div>
|
|
134
|
+
<strong>Negative / UC</strong>
|
|
135
|
+
<pre id="negativePreview"></pre>
|
|
136
|
+
</div>
|
|
138
137
|
</div>
|
|
139
138
|
</section>
|
|
140
139
|
|
|
@@ -153,14 +152,16 @@
|
|
|
153
152
|
<button class="danger-button" id="stopBattingButton" type="button" disabled>생성 중지</button>
|
|
154
153
|
</div>
|
|
155
154
|
</div>
|
|
156
|
-
<div class="fixed-artist-panel">
|
|
157
|
-
<div class="fixed-artist-head">
|
|
158
|
-
<div class="mode-status" id="battingFixedArtistModeLabel">랜덤 가중치 모드</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
<div class="fixed-artist-panel">
|
|
156
|
+
<div class="fixed-artist-head">
|
|
157
|
+
<div class="mode-status" id="battingFixedArtistModeLabel">랜덤 가중치 모드</div>
|
|
158
|
+
<button class="ghost-button" id="battingLoadWeightsButton" type="button">히스토리에서 가중치 고르기</button>
|
|
159
|
+
</div>
|
|
160
|
+
<p id="battingFixedArtistSummary">히스토리나 가중치 비교에서 마음에 드는 이미지의 가중치를 먼저 불러오세요.</p>
|
|
161
|
+
<div id="battingFixedArtistList" class="artist-weight-list fixed-artist-list"></div>
|
|
162
|
+
</div>
|
|
163
|
+
<div id="battingReadiness" class="inline-notice"></div>
|
|
164
|
+
<div id="battingSceneList" class="batting-scene-list"></div>
|
|
164
165
|
</section>
|
|
165
166
|
|
|
166
167
|
<section class="panel live-preview-panel">
|
|
@@ -219,24 +220,34 @@
|
|
|
219
220
|
<p>선택 태그 수가 비어 있으면 모든 태그를 포함합니다.</p>
|
|
220
221
|
</div>
|
|
221
222
|
</div>
|
|
222
|
-
<div class="form-grid compact">
|
|
223
|
-
<label><span>카테고리명</span><input id="catName" /></label>
|
|
224
|
-
<label><span>최소 가중치</span><input id="catMin" type="number" step="0.01" /></label>
|
|
225
|
-
<label><span>최대 가중치</span><input id="catMax" type="number" step="0.01" /></label>
|
|
226
|
-
<label
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
223
|
+
<div class="form-grid compact">
|
|
224
|
+
<label><span>카테고리명</span><input id="catName" /></label>
|
|
225
|
+
<label><span>최소 가중치</span><input id="catMin" type="number" step="0.01" /></label>
|
|
226
|
+
<label><span>최대 가중치</span><input id="catMax" type="number" step="0.01" /></label>
|
|
227
|
+
<label>
|
|
228
|
+
<span>가중치 간격</span>
|
|
229
|
+
<input id="catGranule" type="number" step="0.01" />
|
|
230
|
+
<small class="field-help">예: 0.05이면 1.00, 1.05, 1.10처럼 선택됩니다.</small>
|
|
231
|
+
</label>
|
|
232
|
+
<label>
|
|
233
|
+
<span>한 번에 사용할 태그 수</span>
|
|
234
|
+
<input id="catPicks" type="number" min="1" placeholder="비워두면 전체" />
|
|
235
|
+
<small class="field-help">비워두면 이 카테고리에 입력한 작가를 모두 사용합니다.</small>
|
|
236
|
+
</label>
|
|
237
|
+
</div>
|
|
238
|
+
<label class="text-label">
|
|
239
|
+
<span>태그 목록</span>
|
|
240
|
+
<textarea id="catTags" spellcheck="false" placeholder="artist:example_name 또는 1.2::artist:example_name ::, artist:another_name"></textarea>
|
|
241
|
+
<small class="field-help">쉼표로 붙여넣거나 한 줄에 하나씩 입력해도 artist:이름 부분만 자동 인식합니다.</small>
|
|
242
|
+
</label>
|
|
233
243
|
<div class="recognized-box">
|
|
234
244
|
<div>
|
|
235
|
-
<strong>인식된 artist 태그</strong>
|
|
236
|
-
<span id="recognizedCount">0개</span>
|
|
237
|
-
</div>
|
|
238
|
-
<div id="recognizedTags" class="tag-chips"></div>
|
|
239
|
-
</div>
|
|
245
|
+
<strong>인식된 artist 태그</strong>
|
|
246
|
+
<span id="recognizedCount">0개</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div id="recognizedTags" class="tag-chips"></div>
|
|
249
|
+
</div>
|
|
250
|
+
<div id="categoryWeightPreview" class="inline-notice compact-notice"></div>
|
|
240
251
|
<div class="button-row">
|
|
241
252
|
<button class="danger-button" id="deleteCategoryButton">카테고리 삭제</button>
|
|
242
253
|
</div>
|
|
@@ -256,9 +267,13 @@
|
|
|
256
267
|
</div>
|
|
257
268
|
<div id="baseList" class="item-list horizontal"></div>
|
|
258
269
|
<label class="text-label short"><span>프리셋 이름</span><input id="baseName" /></label>
|
|
259
|
-
<label class="text-label"><span>베이스 프롬프트</span><textarea id="basePrompt" spellcheck="false"></textarea></label>
|
|
260
|
-
<label class="text-label"><span>퀄리티 프롬프트</span><textarea id="qualityPrompt" spellcheck="false"></textarea></label>
|
|
261
|
-
<label class="text-label"
|
|
270
|
+
<label class="text-label"><span>베이스 프롬프트</span><textarea id="basePrompt" spellcheck="false"></textarea></label>
|
|
271
|
+
<label class="text-label"><span>퀄리티 프롬프트</span><textarea id="qualityPrompt" spellcheck="false"></textarea></label>
|
|
272
|
+
<label class="text-label">
|
|
273
|
+
<span>공용 퀄리티 덮어쓰기 (선택)</span>
|
|
274
|
+
<textarea id="qualityOverridePrompt" spellcheck="false" placeholder="비워두면 각 베이스 프리셋의 퀄리티 프롬프트를 사용합니다."></textarea>
|
|
275
|
+
<small class="field-help">값이 있으면 모든 베이스 프리셋의 퀄리티 프롬프트 대신 이 내용을 사용합니다.</small>
|
|
276
|
+
</label>
|
|
262
277
|
<button class="danger-button" id="deleteBaseButton">베이스 프리셋 삭제</button>
|
|
263
278
|
</section>
|
|
264
279
|
<section class="panel">
|
|
@@ -272,35 +287,54 @@
|
|
|
272
287
|
<button class="icon-button" id="addCharButton" title="새 캐릭터">+</button>
|
|
273
288
|
</div>
|
|
274
289
|
</div>
|
|
275
|
-
<div id="charList" class="item-list horizontal"></div>
|
|
276
|
-
<label class="text-label short"><span>프리셋 이름</span><input id="charName" /></label>
|
|
277
|
-
<div id="
|
|
290
|
+
<div id="charList" class="item-list horizontal"></div>
|
|
291
|
+
<label class="text-label short"><span>프리셋 이름</span><input id="charName" /></label>
|
|
292
|
+
<div id="charSlotTabs" class="segment-control" role="tablist" aria-label="캐릭터 선택"></div>
|
|
293
|
+
<div id="characterEditors" class="character-editors"></div>
|
|
278
294
|
<button class="danger-button" id="deleteCharButton">캐릭터 프리셋 삭제</button>
|
|
279
295
|
</section>
|
|
280
296
|
</div>
|
|
281
297
|
</section>
|
|
282
298
|
|
|
283
|
-
<section id="settings" class="tab-page">
|
|
284
|
-
<div class="grid two-one">
|
|
285
|
-
<section class="panel">
|
|
286
|
-
<div class="panel-header">
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
<section id="settings" class="tab-page">
|
|
300
|
+
<div class="grid two-one">
|
|
301
|
+
<section class="panel">
|
|
302
|
+
<div class="panel-header">
|
|
303
|
+
<div>
|
|
304
|
+
<h2>NovelAI 연결</h2>
|
|
305
|
+
<p>실제 이미지를 만들려면 API 토큰이 필요합니다.</p>
|
|
306
|
+
</div>
|
|
307
|
+
<span id="apiConnectionStatus" class="mode-status"></span>
|
|
308
|
+
</div>
|
|
309
|
+
<div id="apiPrimaryForm" class="form-grid"></div>
|
|
310
|
+
<label class="switch-row feature-switch">
|
|
311
|
+
<input id="mockMode" type="checkbox" />
|
|
312
|
+
<span>
|
|
313
|
+
<strong>API 없이 체험하기</strong>
|
|
314
|
+
<small>NovelAI에 요청하지 않고 색상 목업 이미지를 생성합니다.</small>
|
|
315
|
+
</span>
|
|
316
|
+
</label>
|
|
317
|
+
<div class="button-row">
|
|
318
|
+
<button class="primary-button" id="saveSettingsButton">토큰 및 설정 저장</button>
|
|
319
|
+
</div>
|
|
320
|
+
<details class="advanced-settings">
|
|
321
|
+
<summary>고급 API 설정</summary>
|
|
322
|
+
<p>NovelAI 기본값을 바꿔야 할 때만 열어보세요.</p>
|
|
323
|
+
<div id="apiAdvancedForm" class="form-grid compact"></div>
|
|
324
|
+
</details>
|
|
325
|
+
</section>
|
|
326
|
+
<section class="panel">
|
|
327
|
+
<div class="panel-header">
|
|
328
|
+
<div>
|
|
329
|
+
<h2>제외할 내용</h2>
|
|
330
|
+
<p>이미지에 나오지 않았으면 하는 요소를 입력합니다.</p>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<label class="text-label">
|
|
334
|
+
<span>공통 네거티브 프롬프트</span>
|
|
335
|
+
<textarea id="negativePrompt" spellcheck="false"></textarea>
|
|
336
|
+
<small class="field-help">모든 캐릭터에 공통으로 적용되며 NovelAI의 UC에도 같은 내용이 전송됩니다.</small>
|
|
337
|
+
</label>
|
|
304
338
|
</section>
|
|
305
339
|
</div>
|
|
306
340
|
</section>
|
|
@@ -393,7 +427,7 @@
|
|
|
393
427
|
</aside>
|
|
394
428
|
</section>
|
|
395
429
|
</div>
|
|
396
|
-
<div id="presetPickerModal" class="image-modal" hidden>
|
|
430
|
+
<div id="presetPickerModal" class="image-modal" hidden>
|
|
397
431
|
<div class="image-modal-backdrop" id="presetPickerBackdrop"></div>
|
|
398
432
|
<section class="preset-picker-panel" role="dialog" aria-modal="true" aria-labelledby="presetPickerTitle">
|
|
399
433
|
<div class="panel-header">
|
|
@@ -404,8 +438,9 @@
|
|
|
404
438
|
<button class="icon-button" id="presetPickerCloseButton" title="닫기">×</button>
|
|
405
439
|
</div>
|
|
406
440
|
<div id="presetPickerList" class="preset-picker-list"></div>
|
|
407
|
-
</section>
|
|
408
|
-
</div>
|
|
409
|
-
<
|
|
441
|
+
</section>
|
|
442
|
+
</div>
|
|
443
|
+
<div id="toastRegion" class="toast-region" aria-live="polite" aria-atomic="true"></div>
|
|
444
|
+
<script src="/web/main.js"></script>
|
|
410
445
|
</body>
|
|
411
446
|
</html>
|