dahara1 commited on
Commit
ebed1c6
·
verified ·
1 Parent(s): 6b0e3c1

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -784
app.py CHANGED
@@ -51,523 +51,6 @@ IS_COLAB = utils.is_google_colab() or os.getenv("IS_COLAB") == "1"
51
  HF_TOKEN = os.getenv("HF_TOKEN")
52
  CACHE_EXAMPLES = torch.cuda.is_available() and os.getenv("CACHE_EXAMPLES") == "1"
53
 
54
- # PyTorch settings for better performance and determinism
55
- #torch.backends.cudnn.deterministic = True
56
- #torch.backends.cudnn.benchmark = False
57
- #torch.backends.cuda.matmul.allow_tf32 = True
58
- #device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
59
- #logger.info(f"Using device: {device}")
60
-
61
- # グローバル変数としてパイプラインを定義
62
- pipe = None
63
- vae = None
64
-
65
- # スタイルリストから名前のみを抽出
66
- style_names = [style["name"] for style in style_list]
67
-
68
- @spaces.GPU(timeout_seconds=120)
69
- def initialize_llm():
70
- """アプリケーション起動時にLLMだけを初期化する関数"""
71
-
72
- if TEXT_TO_PROMPT_ENABLED:
73
- torch.backends.cudnn.deterministic = True
74
- torch.backends.cudnn.benchmark = False
75
- torch.backends.cuda.matmul.allow_tf32 = True
76
-
77
- logger.info("Loading LLM for prompt generation first...")
78
- prompt_generator.load_model()
79
- return "LLM loaded successfully"
80
- return "LLM loading skipped (disabled in config)"
81
-
82
-
83
- def cleanup_old_images(output_dir, max_age_hours=1):
84
- """
85
- 指定されたディレクトリ内の古い画像ファイル(PNG)を削除します
86
-
87
- Args:
88
- output_dir: 画像ファイルが保存されているディレクトリのパス
89
- max_age_hours: この時間(時間単位)より古いファイルを削除する
90
- """
91
- import os
92
- import time
93
- from datetime import datetime
94
-
95
- logger.info(f"Cleaning up images older than {max_age_hours} hours in {output_dir}")
96
- current_time = time.time()
97
- max_age_seconds = max_age_hours * 60 * 60
98
- deleted_count = 0
99
-
100
- # ディレクトリが存在しない場合は作成
101
- if not os.path.exists(output_dir):
102
- os.makedirs(output_dir, exist_ok=True)
103
- return 0
104
-
105
- # ディレクトリ内のすべてのファイルをチェック
106
- for filename in os.listdir(output_dir):
107
- if filename.lower().endswith('.png'):
108
- file_path = os.path.join(output_dir, filename)
109
- file_age = current_time - os.path.getmtime(file_path)
110
-
111
- # 指定された時間より古いファイルを削除
112
- if file_age > max_age_seconds:
113
- try:
114
- os.remove(file_path)
115
- deleted_count += 1
116
- except Exception as e:
117
- logger.error(f"Failed to delete {file_path}: {str(e)}")
118
-
119
- if deleted_count > 0:
120
- logger.info(f"Deleted {deleted_count} old image files from {output_dir}")
121
- return deleted_count
122
-
123
-
124
- # シリーズとキャラクターの紐付けを処理する
125
- def get_character_series_mapping():
126
- try:
127
- mapping = config.config.get('text_to_prompt', {}).get('character_series_mapping', {})
128
- return mapping
129
- except Exception as e:
130
- logger.error(f"Failed to get character-series mapping: {str(e)}")
131
- return {}
132
-
133
- # シリーズ名と表示名を分割
134
- def parse_series_list():
135
- series_dict = {}
136
- display_series_list = []
137
-
138
- for item in series_list:
139
- if '|' in item:
140
- code, display = item.split('|', 1)
141
- series_dict[code] = display
142
- display_series_list.append(f"{code} / {display}")
143
- else:
144
- series_dict[item] = item
145
- display_series_list.append(item)
146
-
147
- return series_dict, display_series_list
148
-
149
- # キャラクター名と表示名を分割
150
- def parse_character_list():
151
- character_dict = {}
152
- display_character_list = []
153
-
154
- for item in character_list:
155
- if '|' in item:
156
- code, display = item.split('|', 1)
157
- character_dict[code] = display
158
- display_character_list.append(f"{code} / {display}")
159
- else:
160
- character_dict[item] = item
161
- display_character_list.append(item)
162
-
163
- return character_dict, display_character_list
164
-
165
- # カテゴリー名と表示名を分割
166
- def parse_category_list():
167
- category_dict = {}
168
- display_category_list = []
169
-
170
- for item in category_list:
171
- # カテゴリの表示が英語のみなので、そのまま表示
172
- category_dict[item] = item
173
- display_category_list.append(item)
174
-
175
- return category_dict, display_category_list
176
-
177
- # 逆引き辞書の作成
178
- def create_reverse_dict(original_dict):
179
- return {v: k for k, v in original_dict.items()}
180
-
181
- # 表示名から内部コードを取得する関数
182
- def get_code_from_display(display_name, reverse_dict):
183
- # 表示名が "code / display" 形式の場合
184
- if " / " in display_name:
185
- code = display_name.split(" / ")[0]
186
- return code
187
- # 元のコードの場合はそのまま返す
188
- return reverse_dict.get(display_name, display_name)
189
-
190
- # 辞書とマッピングの作成
191
- series_dict, display_series_list = parse_series_list()
192
- character_dict, display_character_list = parse_character_list()
193
- category_dict, display_category_list = parse_category_list()
194
- reverse_series_dict = create_reverse_dict(series_dict)
195
- reverse_character_dict = create_reverse_dict(character_dict)
196
- character_series_mapping = get_character_series_mapping()
197
-
198
- # 特定のシリーズに属するキャラクターのリストを取得
199
- def get_characters_for_series(series_display_name):
200
- try:
201
- # 表示名からシリーズコードを取得
202
- series_code = get_code_from_display(series_display_name, reverse_series_dict)
203
-
204
- if not series_code:
205
- logger.warning(f"Unknown series: {series_display_name}")
206
- return display_character_list
207
-
208
- character_codes = character_series_mapping.get(series_code, [])
209
- if not character_codes:
210
- logger.warning(f"No characters found for series: {series_code}")
211
- return display_character_list
212
-
213
- # コードから表示名へ変換
214
- characters = [f"{code} / {character_dict.get(code, code)}" for code in character_codes]
215
- return characters
216
- except Exception as e:
217
- logger.error(f"Error getting characters for series: {str(e)}")
218
- return display_character_list
219
-
220
- class GenerationError(Exception):
221
- """Custom exception for generation errors"""
222
- pass
223
-
224
- def validate_prompt(prompt: str) -> str:
225
- """Validate and clean up the input prompt."""
226
- if not isinstance(prompt, str):
227
- raise GenerationError("Prompt must be a string")
228
- try:
229
- # Ensure proper UTF-8 encoding/decoding
230
- prompt = prompt.encode('utf-8').decode('utf-8')
231
- # Add space between ! and ,
232
- prompt = prompt.replace("!,", "! ,")
233
- except UnicodeError:
234
- raise GenerationError("Invalid characters in prompt")
235
-
236
- # Only check if the prompt is completely empty or only whitespace
237
- if not prompt or prompt.isspace():
238
- raise GenerationError("Prompt cannot be empty")
239
- return prompt.strip()
240
-
241
- def validate_dimensions(width: int, height: int) -> None:
242
- """Validate image dimensions."""
243
- if not MIN_IMAGE_SIZE <= width <= MAX_IMAGE_SIZE:
244
- raise GenerationError(f"Width must be between {MIN_IMAGE_SIZE} and {MAX_IMAGE_SIZE}")
245
-
246
- if not MIN_IMAGE_SIZE <= height <= MAX_IMAGE_SIZE:
247
- raise GenerationError(f"Height must be between {MIN_IMAGE_SIZE} and {MAX_IMAGE_SIZE}")
248
-
249
- def convert_text_to_prompt(
250
- novel_text: str,
251
- series_display_name: str = series_dict.get(DEFAULT_SERIES, DEFAULT_SERIES),
252
- character_display_name: str = character_dict.get(DEFAULT_CHARACTER, DEFAULT_CHARACTER),
253
- category: str = DEFAULT_CATEGORY,
254
- ) -> Tuple[str, str]:
255
- """テキストからプロンプトを生成する関数"""
256
- if not TEXT_TO_PROMPT_ENABLED:
257
- return "Text to Prompt機能は無効になっています", novel_text
258
-
259
- # 表示名からコードに変換
260
- series_name = get_code_from_display(series_display_name, reverse_series_dict)
261
- character_name = get_code_from_display(character_display_name, reverse_character_dict)
262
-
263
- try:
264
- logger.info(f"prompt_generator.generate_prompt")
265
- thinking, prompt = prompt_generator.generate_prompt(
266
- novel_text, series_name, character_name, category
267
- )
268
- return thinking, prompt
269
- except Exception as e:
270
- logger.error(f"Error in convert_text_to_prompt: {str(e)}")
271
- return f"エラーが発生しました: {str(e)}", novel_text
272
-
273
- @spaces.GPU
274
- def load_image_model(timeout_seconds=120):
275
- """画像生成モデルをロードする関数"""
276
- global pipe, vae
277
-
278
- # メモリ管理
279
- if utils.is_space_environment():
280
- torch.cuda.empty_cache()
281
- gc.collect()
282
-
283
- # LLMがロードされていれば解放
284
- if is_space_environment != True and TEXT_TO_PROMPT_ENABLED:
285
- prompt_generator.unload_model()
286
-
287
- logger.info("Loading image generation model...")
288
- styles = {style["name"]: (style["prompt"], style.get("negative_prompt", "")) for style in style_list}
289
-
290
- # VAEを明示的にロード - subfolder パラメータを使用
291
- vae = AutoencoderKL.from_pretrained(
292
- MODEL, # モデル名(例: "cagliostrolab/animagine-xl-4.0")
293
- subfolder="vae", # サブフォルダ名
294
- torch_dtype=torch.float16
295
- )
296
-
297
- # パイプラインにVAEを渡す
298
- pipe = utils.load_pipeline(MODEL, device, HF_TOKEN, vae=vae)
299
-
300
- if USE_TORCH_COMPILE:
301
- pipe.unet = torch.compile(pipe.unet, mode="reduce-overhead", fullgraph=True)
302
- logger.info("Model compiled with torch.compile")
303
-
304
- return "Image generation model loaded successfully"
305
-
306
- @spaces.GPU(timeout_seconds=300)
307
- def generate(
308
- prompt: str,
309
- negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
310
- seed: int = -1,
311
- custom_width: int = 1024,
312
- custom_height: int = 1024,
313
- guidance_scale: float = 5.0,
314
- num_inference_steps: int = 28,
315
- sampler: str = "Euler a",
316
- aspect_ratio_selector: str = DEFAULT_ASPECT_RATIO,
317
- style_selector: str = "(None)",
318
- use_upscaler: bool = False,
319
- upscaler_strength: float = 0.55,
320
- upscale_by: float = 1.5,
321
- add_quality_tags: bool = True,
322
- progress: gr.Progress = gr.Progress(track_tqdm=True),
323
- ) -> Tuple[List[str], Dict]:
324
- """Generate images based on the given parameters."""
325
- global pipe
326
-
327
- if pipe is None:
328
- load_image_model()
329
- logger.info(f"Loading image model status: {status}")
330
-
331
- start_time = time.time()
332
- upscaler_pipe = None
333
- backup_scheduler = None
334
- styles = {style["name"]: (style["prompt"], style.get("negative_prompt", "")) for style in style_list}
335
-
336
- try:
337
- # Memory management
338
- if is_space_environment != True :
339
- cleanup_old_images(OUTPUT_DIR)
340
- torch.cuda.empty_cache()
341
- gc.collect()
342
-
343
- # Input validation
344
- prompt = validate_prompt(prompt)
345
- if negative_prompt:
346
- negative_prompt = negative_prompt.encode('utf-8').decode('utf-8')
347
-
348
- validate_dimensions(custom_width, custom_height)
349
-
350
- # Set up generation
351
- if seed == 0: # 0が入力された場合、ランダムなシード値を生成
352
- seed = random.randint(0, utils.MAX_SEED)
353
- generator = utils.seed_everything(seed)
354
-
355
-
356
- width, height = utils.aspect_ratio_handler(
357
- aspect_ratio_selector,
358
- custom_width,
359
- custom_height,
360
- )
361
-
362
- # Process prompts
363
- if add_quality_tags:
364
- prompt = "{prompt}, masterpiece, high score, great score, absurdres".format(prompt=prompt)
365
-
366
- prompt, negative_prompt = utils.preprocess_prompt(
367
- styles, style_selector, prompt, negative_prompt
368
- )
369
-
370
- width, height = utils.preprocess_image_dimensions(width, height)
371
-
372
- # Set up pipeline
373
- backup_scheduler = pipe.scheduler
374
- pipe.scheduler = utils.get_scheduler(pipe.scheduler.config, sampler)
375
-
376
- if use_upscaler:
377
- upscaler_pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
378
-
379
- # Prepare metadata
380
- metadata = {
381
- "prompt": prompt,
382
- "negative_prompt": negative_prompt,
383
- "resolution": f"{width} x {height}",
384
- "guidance_scale": guidance_scale,
385
- "num_inference_steps": num_inference_steps,
386
- "style_preset": style_selector,
387
- "seed": seed,
388
- "sampler": sampler,
389
- "Model": "Animagine XL 4.0 Opt",
390
- "Model hash": "6327eca98b",
391
- }
392
-
393
- if use_upscaler:
394
- new_width = int(width * upscale_by)
395
- new_height = int(height * upscale_by)
396
- metadata["use_upscaler"] = {
397
- "upscale_method": "nearest-exact",
398
- "upscaler_strength": upscaler_strength,
399
- "upscale_by": upscale_by,
400
- "new_resolution": f"{new_width} x {new_height}",
401
- }
402
- else:
403
- metadata["use_upscaler"] = None
404
-
405
- logger.info(f"Starting generation with parameters: {json.dumps(metadata, indent=4)}")
406
-
407
- # Generate images
408
- if use_upscaler:
409
- latents = pipe(
410
- prompt=prompt,
411
- negative_prompt=negative_prompt,
412
- width=width,
413
- height=height,
414
- guidance_scale=guidance_scale,
415
- num_inference_steps=num_inference_steps,
416
- generator=generator,
417
- output_type="latent",
418
- ).images
419
- upscaled_latents = utils.upscale(latents, "nearest-exact", upscale_by)
420
- images = upscaler_pipe(
421
- prompt=prompt,
422
- negative_prompt=negative_prompt,
423
- image=upscaled_latents,
424
- guidance_scale=guidance_scale,
425
- num_inference_steps=num_inference_steps,
426
- strength=upscaler_strength,
427
- generator=generator,
428
- output_type="pil",
429
- ).images
430
- else:
431
- images = pipe(
432
- prompt=prompt,
433
- negative_prompt=negative_prompt,
434
- width=width,
435
- height=height,
436
- guidance_scale=guidance_scale,
437
- num_inference_steps=num_inference_steps,
438
- generator=generator,
439
- output_type="pil",
440
- ).images
441
-
442
- # Save images
443
- if images:
444
- total = len(images)
445
- image_paths = []
446
- for idx, image in enumerate(images, 1):
447
- progress(idx/total, desc="Saving images...")
448
- path = utils.save_image(image, metadata, OUTPUT_DIR, IS_COLAB)
449
- image_paths.append(path)
450
- logger.info(f"Image {idx}/{total} saved as {path}")
451
-
452
- generation_time = time.time() - start_time
453
- logger.info(f"Generation completed successfully in {generation_time:.2f} seconds")
454
- metadata["generation_time"] = f"{generation_time:.2f}s"
455
-
456
- return image_paths, metadata
457
-
458
- except GenerationError as e:
459
- logger.warning(f"Generation validation error: {str(e)}")
460
- raise gr.Error(str(e))
461
- except Exception as e:
462
- logger.exception("Unexpected error during generation")
463
- raise gr.Error(f"Generation failed: {str(e)}")
464
- finally:
465
- # Cleanup
466
- torch.cuda.empty_cache()
467
- gc.collect()
468
-
469
- if upscaler_pipe is not None:
470
- del upscaler_pipe
471
-
472
- if backup_scheduler is not None and pipe is not None:
473
- pipe.scheduler = backup_scheduler
474
-
475
- utils.free_memory()
476
-
477
- # シリーズが変更されたときに対応するキャラクターを更新
478
- def update_character_list(series_display_name):
479
- characters = get_characters_for_series(series_display_name)
480
- if characters and len(characters) > 0:
481
- default_character = characters[0]
482
- else:
483
- default_character = display_character_list[0]
484
-
485
- return gr.update(choices=characters, value=default_character)
486
-
487
- # テキストからプロンプトを生成する関数を追加
488
- @spaces.GPU(timeout_seconds=120)
489
- def process_text_to_prompt(
490
- novel_text: str,
491
- series_display_name: str = series_dict.get(DEFAULT_SERIES, DEFAULT_SERIES),
492
- character_display_name: str = character_dict.get(DEFAULT_CHARACTER, DEFAULT_CHARACTER),
493
- category: str = DEFAULT_CATEGORY,
494
- ) -> Tuple[str, str, Dict]:
495
- """テキストからプロンプトを生成して、UIに表示する関数"""
496
- try:
497
- # 必要に応じてLLMをロード
498
- if TEXT_TO_PROMPT_ENABLED and not hasattr(prompt_generator, "_model") or prompt_generator._model is None:
499
- prompt_generator.load_model()
500
-
501
- thinking, prompt_text = convert_text_to_prompt(novel_text, series_display_name, character_display_name, category)
502
-
503
- # プロンプト生成に関するメタデータ
504
- metadata = {
505
- "novel_text": novel_text[:100] + "..." if len(novel_text) > 100 else novel_text,
506
- "series": series_display_name,
507
- "character": character_display_name,
508
- "category": category,
509
- "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
510
- }
511
-
512
- return thinking, prompt_text, metadata
513
-
514
- except Exception as e:
515
- logger.exception("Error in process_text_to_prompt")
516
- error_message = f"プロンプト生成中にエラーが発生しました: {str(e)}"
517
- return error_message, "", {"error": str(e)}
518
-
519
- # 生成されたプロンプトを画像生成パラメータにコピーする関数
520
- def copy_prompt_to_generation(prompt_text):
521
- return prompt_text, gr.update(visible=False)
522
-
523
- # スタイルが変更された時にプロンプトを更新する関数
524
- def update_prompt_with_style(prompt_text, current_style, new_style):
525
- if prompt_text.strip() == "":
526
- return prompt_text
527
-
528
- # スタイル情報を取得
529
- styles = {style["name"]: (style["prompt"], style.get("negative_prompt", "")) for style in style_list}
530
-
531
- # 現在のスタイルのプロンプト部分を取得
532
- current_style_prompt = ""
533
- if current_style != "(None)":
534
- current_style_template = styles.get(current_style, ("", ""))[0]
535
- # {prompt} の部分を除外してスタイル部分だけを抽出
536
- if "{prompt}" in current_style_template:
537
- current_style_prompt = current_style_template.replace("{prompt}", "").strip()
538
- if current_style_prompt.startswith(","):
539
- current_style_prompt = current_style_prompt[1:].strip()
540
-
541
- # 新しいスタイルのプロンプト部分を取得
542
- new_style_prompt = ""
543
- if new_style != "(None)":
544
- new_style_template = styles.get(new_style, ("", ""))[0]
545
- # {prompt} の部分を除外してスタイル部分だけを抽出
546
- if "{prompt}" in new_style_template:
547
- new_style_prompt = new_style_template.replace("{prompt}", "").strip()
548
- if new_style_prompt.startswith(","):
549
- new_style_prompt = new_style_prompt[1:].strip()
550
-
551
- # 現在のプロンプトからスタイル部分を削除
552
- base_prompt = prompt_text
553
- if current_style_prompt:
554
- style_part = f", {current_style_prompt}"
555
- if style_part in base_prompt:
556
- base_prompt = base_prompt.replace(style_part, "")
557
-
558
- # 新しいスタイルを追加
559
- if new_style_prompt:
560
- if base_prompt.strip():
561
- base_prompt = f"{base_prompt.strip()}, {new_style_prompt}"
562
- else:
563
- base_prompt = new_style_prompt
564
-
565
- return base_prompt
566
-
567
-
568
- initialize_llm()
569
-
570
-
571
  # Create CSS with improved buttons and styling
572
  custom_css = """
573
  .header {
@@ -598,106 +81,29 @@ custom_css = """
598
  opacity: 0.9;
599
  }
600
 
601
- .section {
602
- background: white;
 
 
 
 
603
  border-radius: 10px;
604
- padding: 1.5rem;
605
- margin-bottom: 1.5rem;
606
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
607
- border: 1px solid #e1e4e8;
608
  }
609
 
610
- .section-title {
611
- font-size: 1.3rem;
612
- margin-top: 0;
613
- margin-bottom: 1.2rem;
614
- color: #4a69bd;
615
- border-bottom: 2px solid #e1e4e8;
616
- padding-bottom: 0.5rem;
617
- }
618
-
619
- /* Improved button styling */
620
- .primary-button {
621
- background-color: #4a69bd !important;
622
- color: white !important;
623
- font-weight: 600 !important;
624
- padding: 0.7rem 1.2rem !important;
625
- border-radius: 8px !important;
626
- border: none !important;
627
- cursor: pointer !important;
628
- transition: all 0.2s ease !important;
629
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1) !important;
630
- text-transform: uppercase !important;
631
- letter-spacing: 0.5px !important;
632
- }
633
-
634
- .primary-button:hover {
635
- background-color: #3a539b !important;
636
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
637
- transform: translateY(-1px) !important;
638
- }
639
-
640
- /* 思考プロセスとプロンプト出力のスタイルを改善 */
641
- .thinking-output-label {
642
- font-weight: 600 !important;
643
- color: #4285f4 !important;
644
- background-color: transparent !important;
645
- margin-bottom: 4px !important;
646
- }
647
-
648
- .thinking-output {
649
- background-color: #f0f7ff !important;
650
- border-left: 4px solid #4285f4 !important;
651
- padding: 12px !important;
652
- border-radius: 6px !important;
653
- font-size: 0.95rem !important;
654
- color: #333 !important;
655
- }
656
-
657
- .generated-prompt-label {
658
- font-weight: 600 !important;
659
- color: #34a853 !important;
660
- background-color: transparent !important;
661
- margin-bottom: 4px !important;
662
- margin-top: 12px !important;
663
- }
664
-
665
- .generated-prompt {
666
- background-color: #f0fff4 !important;
667
- border-left: 4px solid #34a853 !important;
668
- padding: 12px !important;
669
- border-radius: 6px !important;
670
- font-weight: 500 !important;
671
- font-size: 0.95rem !important;
672
- color: #333 !important;
673
- }
674
-
675
- .text-input-area {
676
- border: 1px solid #d0d7de;
677
- border-radius: 8px;
678
- }
679
-
680
- /* Add animation for loading states */
681
- @keyframes pulse {
682
- 0% { opacity: 1; }
683
- 50% { opacity: 0.7; }
684
- 100% { opacity: 1; }
685
  }
686
 
687
- .loading {
688
- animation: pulse 1.5s infinite;
689
  }
690
 
691
- /* Gallery improvements */
692
- .gallery-item {
693
- border-radius: 8px;
694
- overflow: hidden;
695
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
696
- transition: transform 0.2s ease;
697
- }
698
-
699
- .gallery-item:hover {
700
- transform: scale(1.02);
701
  }
702
  """
703
 
@@ -706,181 +112,20 @@ with gr.Blocks(css=custom_css) as demo:
706
  gr.HTML("<div class='header'><h1 class='title'>FanFic Illustrator <span class='subtitle-inline'>with Animagine XL 4.0 Opt</span></h1><p class='subtitle'>Illustrate your fan stories with beautiful AI-generated art<br>二次創作ファン小説にAIで魅力的な挿絵を</p></div>")
707
 
708
  with gr.Column():
709
- # Text Input Section
710
- with gr.Group(elem_classes=["section"]):
711
- gr.HTML("<h3 class='section-title'>1. Your Narrative / あなたの創作した物語</h3>")
712
- novel_text = gr.Textbox(
713
- label="",
714
- placeholder="Enter your fan story or narrative here... / ここにファンストーリーや物語を入力してください...",
715
- lines=10,
716
- elem_classes=["text-input-area"],
717
- )
718
-
719
- with gr.Row():
720
- with gr.Column(scale=1):
721
- series_selector = gr.Dropdown(
722
- choices=display_series_list,
723
- value=display_series_list[0] if display_series_list else "",
724
- label="Series / シリーズ",
725
- )
726
- with gr.Column(scale=1):
727
- character_selector = gr.Dropdown(
728
- choices=get_characters_for_series(display_series_list[0] if display_series_list else ""),
729
- value=display_character_list[0] if display_character_list else "",
730
- label="Character / キャラクター",
731
- )
732
-
733
- with gr.Row():
734
- category_selector = gr.Dropdown(
735
- choices=display_category_list,
736
- value=display_category_list[0] if display_category_list else "",
737
- label="Illustration Type / イラストタイプ",
738
- )
739
-
740
- convert_btn = gr.Button("Generate Prompt / プロンプト生成", elem_classes=["primary-button"])
741
-
742
- # Thinking Process & Generated Prompt Section
743
- with gr.Group(elem_classes=["section"]):
744
- gr.HTML("<h3 class='section-title'>2. AI Interpretation / AIの解釈結果</h3>")
745
-
746
- gr.HTML("<div class='thinking-output-label'>AI Thought Process / AIの思考過程</div>")
747
- thinking_output = gr.Textbox(
748
- label="",
749
- lines=6,
750
- elem_classes=["thinking-output"],
751
- visible=True
752
- )
753
-
754
- gr.HTML("<div class='generated-prompt-label'>Generated Prompt / 生成されたプロンプト</div>")
755
- prompt_output = gr.Textbox(
756
- label="",
757
- lines=3,
758
- elem_classes=["generated-prompt"],
759
- )
760
-
761
- use_prompt_btn = gr.Button("Create Illustration with This Prompt / このプロンプトでイラスト作成", elem_classes=["primary-button"])
762
-
763
- # Image Generation Section
764
- with gr.Group(elem_classes=["section"]):
765
- gr.HTML("<h3 class='section-title'>3. Illustration Generation / イラスト生成</h3>")
766
-
767
- # 生成イラストを一番上に配置
768
- output_gallery = gr.Gallery(label="Generated Illustrations / 生成されたイラスト", show_label=True)
769
-
770
- # プロンプト入力欄
771
- prompt = gr.Textbox(
772
- label="Prompt / プロンプト",
773
- placeholder="Enter your prompt here... / ここにプロンプトを入力してください...",
774
- lines=3,
775
- )
776
-
777
- # 詳細設定のアコーディオン - デフォルトでは閉じている
778
- with gr.Accordion("Advanced Options / 詳細設定", open=False):
779
-
780
- # スタイルセレクター
781
- current_style = gr.State("(None)") # 現在選択されているスタイルを保存
782
- style_selector = gr.Dropdown(
783
- choices=style_names,
784
- value="(None)",
785
- label="Style / スタイル",
786
- info="Select a style to apply to your prompt / プロンプトに適用するスタイルを選択",
787
- )
788
-
789
- # ネガティブプロンプト
790
- negative_prompt = gr.Textbox(
791
- label="Negative Prompt / ネガティブプロンプト",
792
- placeholder="What you don't want to see in the image / 画像に含めたくない要素",
793
- value=DEFAULT_NEGATIVE_PROMPT,
794
- lines=3,
795
- )
796
-
797
- # 「イラスト生成」を「イラスト再生成」に変更
798
- generate_btn = gr.Button("Regenerate Illustration / イラスト再生成", elem_classes=["primary-button"])
799
-
800
- # Setup event listeners
801
- # シリーズが変更されたときにキャラクターリストを更新するイベント
802
- series_selector.change(
803
- fn=update_character_list,
804
- inputs=[series_selector],
805
- outputs=[character_selector],
806
- )
807
-
808
- # スタイルが変更されたときにプロンプトを更新するイベント
809
- style_selector.change(
810
- fn=update_prompt_with_style,
811
- inputs=[prompt, current_style, style_selector],
812
- outputs=[prompt],
813
- ).then(
814
- fn=lambda x: x,
815
- inputs=[style_selector],
816
- outputs=[current_style],
817
- )
818
-
819
- # プロンプト生成ボタンのイベント
820
- convert_btn.click(
821
- fn=process_text_to_prompt,
822
- inputs=[
823
- novel_text,
824
- series_selector,
825
- character_selector,
826
- category_selector,
827
- ],
828
- outputs=[thinking_output, prompt_output, gr.JSON(visible=False)],
829
- )
830
-
831
- # プロンプトを画像生成に使用するボタンのイベント
832
- use_prompt_btn.click(
833
- fn=copy_prompt_to_generation,
834
- inputs=[prompt_output],
835
- outputs=[prompt, gr.Textbox(visible=False)],
836
- ).then(
837
- fn=load_image_model,
838
- inputs=[],
839
- outputs=[gr.Textbox(visible=False)],
840
- ).then(
841
- fn=lambda p, np, style: generate(
842
- prompt=p,
843
- negative_prompt=np,
844
- seed=0, # デフォルトのseed値
845
- custom_width=832, # デフォルトの幅
846
- custom_height=1216, # デフォルトの高さ
847
- guidance_scale=5.0, # デフォルトのguidance_scale
848
- num_inference_steps=28, # デフォルトのnum_inference_steps
849
- sampler="Euler a", # デフォルトのsampler
850
- aspect_ratio_selector=DEFAULT_ASPECT_RATIO, # デフォルトのアスペクト比
851
- style_selector=style, # スタイルセレクターの値を渡す
852
- use_upscaler=False, # デフォルトのupscaler設定
853
- upscaler_strength=0.55, # デフォルトのupscaler強度
854
- upscale_by=1.5, # デフォルトのupscale値
855
- add_quality_tags=True, # デフォルトの品質タグ設定
856
- ),
857
- inputs=[prompt, negative_prompt, style_selector],
858
- outputs=[output_gallery, gr.JSON(visible=False)],
859
- )
860
-
861
- # 画像生成ボタンのイベント
862
- generate_btn.click(
863
- fn=lambda p, np, style: generate(
864
- prompt=p,
865
- negative_prompt=np,
866
- seed=0,
867
- custom_width=832,
868
- custom_height=1216,
869
- guidance_scale=5.0,
870
- num_inference_steps=28,
871
- sampler="Euler a",
872
- aspect_ratio_selector=DEFAULT_ASPECT_RATIO,
873
- style_selector=style,
874
- use_upscaler=False,
875
- upscaler_strength=0.55,
876
- upscale_by=1.5,
877
- add_quality_tags=True,
878
- ),
879
- inputs=[prompt, negative_prompt, style_selector],
880
- outputs=[output_gallery, gr.JSON(visible=False)],
881
- )
882
-
883
 
884
  # Launch the app
885
  if __name__ == "__main__":
886
  demo.launch(server_name="0.0.0.0", share=IS_COLAB)
 
 
51
  HF_TOKEN = os.getenv("HF_TOKEN")
52
  CACHE_EXAMPLES = torch.cuda.is_available() and os.getenv("CACHE_EXAMPLES") == "1"
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  # Create CSS with improved buttons and styling
55
  custom_css = """
56
  .header {
 
81
  opacity: 0.9;
82
  }
83
 
84
+ .notification {
85
+ background-color: #fff8e1;
86
+ border-left: 5px solid #ffc107;
87
+ padding: 20px;
88
+ margin: 20px 0;
89
+ font-size: 1.2rem;
90
  border-radius: 10px;
91
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
 
 
 
92
  }
93
 
94
+ .notification-title {
95
+ color: #e65100;
96
+ font-size: 1.5rem;
97
+ margin-bottom: 10px;
98
+ font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
+ .en-message {
102
+ margin-bottom: 15px;
103
  }
104
 
105
+ .jp-message {
106
+ font-weight: 500;
 
 
 
 
 
 
 
 
107
  }
108
  """
109
 
 
112
  gr.HTML("<div class='header'><h1 class='title'>FanFic Illustrator <span class='subtitle-inline'>with Animagine XL 4.0 Opt</span></h1><p class='subtitle'>Illustrate your fan stories with beautiful AI-generated art<br>二次創作ファン小説にAIで魅力的な挿絵を</p></div>")
113
 
114
  with gr.Column():
115
+ # Service temporarily unavailable notification
116
+ gr.HTML("""
117
+ <div class="notification">
118
+ <div class="notification-title">Service Temporarily Suspended</div>
119
+ <div class="en-message">
120
+ This service has been temporarily suspended due to frequent ZERO GPU allocation failures.
121
+ </div>
122
+ <div class="jp-message">
123
+ ZERO GPUの割当に失敗する事が多すぎるので一時停止しました。
124
+ </div>
125
+ </div>
126
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  # Launch the app
129
  if __name__ == "__main__":
130
  demo.launch(server_name="0.0.0.0", share=IS_COLAB)
131
+