nai-aclab 1.1.0 → 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 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
  ![API 설정 메뉴](docs/screenshots/07-api-settings.png)
72
72
 
@@ -103,7 +103,8 @@ artist:sample_a
103
103
  - 퀄리티 프롬프트: 품질 관련 태그
104
104
  - 캐릭터 프롬프트: 캐릭터별 외형과 행동
105
105
  - 네거티브 프롬프트: 피하고 싶은 요소
106
- - UC 프롬프트: NovelAI의 legacy UC 입력값
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
- 생성 히스토리에는 이미지 표시와 작가 가중치 비교에 필요한 최소 정보만 저장합니다. 최종 프롬프트, 네거티브 프롬프트, UC 프롬프트, 생성 요청 JSON, 로컬 절대 경로는 별도로 저장하지 않습니다.
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-4-5-curated"
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)
@@ -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, uc_prompt: str, seed: int | None = None) -> dict:
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": uc_prompt,
472
- "negative_prompt": uc_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, uc_prompt: str, output_path: Path) -> None:
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, uc_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
- uc_box = ttk.LabelFrame(self.settings_tab, text="UC 프롬프트 (parameters.uc)", padding=8)
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, str, list[dict]]:
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
- uc_prompt = self.state_data.uc_prompt.strip() or negative_prompt
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, uc_prompt, artists = self.build_prompt()
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\n[UC]\n{uc_prompt}\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, uc_prompt, artists = self.build_prompt()
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, uc_prompt, path)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nai-aclab",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Local web UI for experimenting with NovelAI artist tag weight combinations.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/web/index.html CHANGED
@@ -130,14 +130,10 @@
130
130
  <strong>Prompt</strong>
131
131
  <pre id="promptPreview"></pre>
132
132
  </div>
133
- <div>
134
- <strong>Negative</strong>
135
- <pre id="negativePreview"></pre>
136
- </div>
137
- <div>
138
- <strong>UC</strong>
139
- <pre id="ucPreview"></pre>
140
- </div>
133
+ <div>
134
+ <strong>Negative / UC</strong>
135
+ <pre id="negativePreview"></pre>
136
+ </div>
141
137
  </div>
142
138
  </section>
143
139
 
@@ -337,12 +333,7 @@
337
333
  <label class="text-label">
338
334
  <span>공통 네거티브 프롬프트</span>
339
335
  <textarea id="negativePrompt" spellcheck="false"></textarea>
340
- <small class="field-help">모든 캐릭터에 공통으로 적용됩니다.</small>
341
- </label>
342
- <label class="text-label">
343
- <span>UC 프롬프트 (선택)</span>
344
- <textarea id="ucPrompt" spellcheck="false" placeholder="비워두면 공통 네거티브 프롬프트를 그대로 사용합니다."></textarea>
345
- <small class="field-help">NovelAI의 legacy UC에 별도 내용을 보내고 싶을 때만 입력하세요.</small>
336
+ <small class="field-help">모든 캐릭터에 공통으로 적용되며 NovelAI의 UC에도 같은 내용이 전송됩니다.</small>
346
337
  </label>
347
338
  </section>
348
339
  </div>
package/web/main.js CHANGED
@@ -256,8 +256,7 @@ function syncEditorsToState() {
256
256
  character.negatives = [0, 1, 2].map((i) => $(`#charNegative${i}`).value.trim());
257
257
  }
258
258
 
259
- state.negative_prompt = $("#negativePrompt").value.trim();
260
- state.uc_prompt = $("#ucPrompt").value.trim();
259
+ state.negative_prompt = $("#negativePrompt").value.trim();
261
260
 
262
261
  for (const [key, , type] of apiFields) {
263
262
  const input = $(`#api_${key}`);
@@ -827,7 +826,6 @@ function renderSettings() {
827
826
  ? "토큰 저장됨"
828
827
  : "토큰 필요";
829
828
  $("#negativePrompt").value = state.negative_prompt || "";
830
- $("#ucPrompt").value = state.uc_prompt || "";
831
829
  bindAutosaveInputs(primaryForm);
832
830
  bindAutosaveInputs(advancedForm);
833
831
  }
@@ -1551,10 +1549,9 @@ async function previewPrompt() {
1551
1549
  method: "POST",
1552
1550
  body: JSON.stringify({ state }),
1553
1551
  });
1554
- $("#promptPreview").textContent = data.prompt || "";
1555
- $("#negativePreview").textContent = data.negative || "";
1556
- $("#ucPreview").textContent = data.uc || "";
1557
- setSaveState("미리보기 갱신됨");
1552
+ $("#promptPreview").textContent = data.prompt || "";
1553
+ $("#negativePreview").textContent = data.negative || "";
1554
+ setSaveState("미리보기 갱신됨");
1558
1555
  }
1559
1556
 
1560
1557
  function setProgressActive(active, selector = ".progress") {
package/web_app.py CHANGED
@@ -78,7 +78,6 @@ def state_from_dict(data: dict) -> AppState:
78
78
  character_presets=[dataclass_from_dict(CharacterPreset, item) for item in data.get("character_presets", [])],
79
79
  quality_override_prompt=quality_override,
80
80
  negative_prompt=data.get("negative_prompt", ""),
81
- uc_prompt=data.get("uc_prompt", data.get("negative_prompt", "")),
82
81
  api=dataclass_from_dict(ApiSettings, data.get("api", {})),
83
82
  generation=dataclass_from_dict(GenerationSettings, data.get("generation", {})),
84
83
  batting_scenes=[dataclass_from_dict(BattingScene, item) for item in data.get("batting_scenes", [])],
@@ -210,7 +209,7 @@ def fixed_artist_tags(state: AppState) -> list[dict]:
210
209
  return result
211
210
 
212
211
 
213
- def build_prompt(state: AppState) -> tuple[str, str, str, list[dict]]:
212
+ def build_prompt(state: AppState) -> tuple[str, str, list[dict]]:
214
213
  base = selected_base(state)
215
214
  character = selected_character(state)
216
215
  artists = fixed_artist_tags(state) or random_artist_tags(state)
@@ -238,8 +237,7 @@ def build_prompt(state: AppState) -> tuple[str, str, str, list[dict]]:
238
237
  if character:
239
238
  negative.extend(item.strip() for item in character.negatives if item.strip())
240
239
  negative_prompt = ", ".join(item for item in negative if item)
241
- uc_prompt = state.uc_prompt.strip() or negative_prompt
242
- return " | ".join(prompt_parts), negative_prompt, uc_prompt, artists
240
+ return " | ".join(prompt_parts), negative_prompt, artists
243
241
 
244
242
 
245
243
  def save_incoming_state(data: dict) -> AppState:
@@ -313,7 +311,7 @@ def run_generation(job_id: str, state: AppState) -> None:
313
311
  cancelled = True
314
312
  JOBS[job_id]["log"].append("중지 요청으로 남은 생성을 건너뜁니다.")
315
313
  break
316
- prompt, negative, uc_prompt, artists = build_prompt(state)
314
+ prompt, negative, artists = build_prompt(state)
317
315
  path = out_dir / f"image_{idx + 1:03}.png"
318
316
  metadata = {
319
317
  "path": output_ref(str(path)),
@@ -321,7 +319,7 @@ def run_generation(job_id: str, state: AppState) -> None:
321
319
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
322
320
  }
323
321
  try:
324
- client.generate(prompt, negative, uc_prompt, path)
322
+ client.generate(prompt, negative, path)
325
323
  JOBS[job_id]["log"].append(f"{idx + 1}/{count} 완료: {path.name}")
326
324
  except Exception as exc:
327
325
  metadata["error"] = str(exc)
@@ -417,7 +415,7 @@ def run_batting_test(job_id: str, state: AppState) -> None:
417
415
  cancelled = True
418
416
  JOBS[job_id]["log"].append(f"{scene_name}: 중지 요청으로 남은 이미지를 건너뜁니다.")
419
417
  break
420
- prompt, negative, uc_prompt, artists = build_prompt(current)
418
+ prompt, negative, artists = build_prompt(current)
421
419
  path = scene_dir / f"image_{image_index + 1:03}.png"
422
420
  metadata = {
423
421
  "path": output_ref(str(path)),
@@ -432,7 +430,7 @@ def run_batting_test(job_id: str, state: AppState) -> None:
432
430
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
433
431
  }
434
432
  try:
435
- client.generate(prompt, negative, uc_prompt, path)
433
+ client.generate(prompt, negative, path)
436
434
  JOBS[job_id]["log"].append(f"{scene_name} {image_index + 1}/{count} 완료: {path.name}")
437
435
  except Exception as exc:
438
436
  metadata["error"] = str(exc)
@@ -536,8 +534,8 @@ class Handler(BaseHTTPRequestHandler):
536
534
  self.send_error(403)
537
535
  return
538
536
  state = load_state()
539
- prompt, negative, uc_prompt, artists = build_prompt(state)
540
- self.send_json({"prompt": prompt, "negative": negative, "uc": uc_prompt, "artists": artists})
537
+ prompt, negative, artists = build_prompt(state)
538
+ self.send_json({"prompt": prompt, "negative": negative, "artists": artists})
541
539
  elif route == "/api/job":
542
540
  if not self.authorized_api_request():
543
541
  self.send_error(403)
@@ -566,8 +564,8 @@ class Handler(BaseHTTPRequestHandler):
566
564
  self.send_json({"state": state_payload(state)})
567
565
  elif route == "/api/preview":
568
566
  state = save_incoming_state(data.get("state", data))
569
- prompt, negative, uc_prompt, artists = build_prompt(state)
570
- self.send_json({"prompt": prompt, "negative": negative, "uc": uc_prompt, "artists": artists})
567
+ prompt, negative, artists = build_prompt(state)
568
+ self.send_json({"prompt": prompt, "negative": negative, "artists": artists})
571
569
  elif route == "/api/generate":
572
570
  if active_job():
573
571
  self.send_json({"error": "이미 생성 작업이 진행 중입니다."}, status=409)